【PmHub后端篇】Redis分布式锁:保障PmHub流程状态更新的关键
在分布式系统中,确保数据一致性和操作的正确执行是至关重要的。PmHub项目中,通过集成Redis分布式锁来保障流程状态更新,这是一个非常关键的技术点,以下将详细介绍其原理、实现。
1 本地锁的问题
1.1 常见的本地锁
在Java中,常见的本地锁有两种,分别是synchronized锁和Lock锁。
在单机环境下,synchronized锁是Java提供的一种内置锁,在单个JVM进程中提供线程之间的锁定机制,可控制多线程并发。
1.2 为什么单机锁锁不住
然而,在分布式系统中,当流程服务部署了多个实例且位于不同服务器时,synchronized锁不再适用。
- 假如现在有多个流程服务实例,那么synchronized这种单机锁的锁对象是字节码文件(或者是Java对象实例),那么其实都会在每一个流程服务中存在一个锁对象。
- 现在假如浏览器往网关发起了若干请求,由于网关中是动态路由,那么就会把这些请求路由给各个流程服务。又由于商品服务中对访问数据库进行了加锁,锁对象是当前类的字节码文件,在每一个流程服务中,都会有这个类的字节码文件存在。
- 也就说,本地锁,在这种场景下,只能保证每一个流程服务同时只有一个线程访问数据库,不能保证数据库在某一个时刻,只有一个线程访问。
2 分布式锁的概念
基于本地锁在分布式环境中的问题,需要一种能在分布式集群环境下的锁机制。分布式锁是控制分布式系统不同进程访问共享资源的一种锁机制,能确保同一时刻只有一个访问可以调用,避免多个调用者竞争调用和数据不一致问题。
所谓分布式锁,其实就是锁对象不在服务实例中,而是在服务实例外部。
3 分布式锁的特性
- 互斥性:同一时刻只能一个节点服务拥有该锁,不同节点之间具有互斥性。
- 超时机制:为防止锁变成死锁,需要设置锁的超时时间。正常情况下,请求获取锁后处理任务并释放锁;若发生服务异常或网络异常导致锁无法释放,过了超时时间后,锁自动释放,其他请求能正常获取锁。
- 自动续期:锁设置了超时机制后,若持有锁的节点处理任务时间过长超过了超时时间,会发生线程未处理完任务锁就被释放的情况,其他线程就能获取到该锁,导致多个节点同时访问共享资源。因此,需要开启一个监听线程,定时监听任务,若任务线程存活则延长超时时间;当任务完成或发生异常则不继续延长超时时间。
4 分布式锁的实现方式
4.1 分布式锁的实现
分布式锁主要有三种实现方式:数据库、Zookeeper、Redis。其中,Redis实现分布式锁性能最高。Redis实现分布式锁有两种方式,一种是利用Redis的SetNX,但存在死锁问题;线上多采用Redission实现分布式锁。
4.2 Radission分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid),不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson基于netty通信框架实现,支持非阻塞通信,性能优于Jedis。
Redisson分布式锁具有四层保护:防死锁、防误删、可重入、自动续期。同时,Redisson实现Redis分布式锁,支持单机和集群模式。
5 PmHub项目实战
5.1 业务流程
PmHub分布式锁业务流程如下:
- 项目服务操作
- 首先由用户发起操作,在项目服务中依次进行“新建项目”和“新建任务” 。
- 接着进行“任务审批设置” ,在此环节,若涉及更新设置操作,需等待分布式锁释放才能执行。
- 流程服务操作
- 当项目服务完成“任务审批设置”后,流程服务开始介入,先执行“更新审批流程” 。
- 然后依次进行“流程状态通知”和“流程状态回调” ,整个过程中分布式锁起到协调和控制资源访问的作用,避免并发操作带来的数据不一致等问题 。
5.2 实现步骤
5.2.1 添加依赖
在PmHub项目中,添加依赖pmhub-base/pmhub-base-security/pom.xml
<!-- Redisson 分布式锁功能 --><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.16.2</version></dependency>
5.2.2 添加配置
并在nacos的pmhub-workflow中添加配置,配置Redisson单机模式。
# Redisson 分布式锁配置
redisson:codec: org.redisson.codec.JsonJacksonCodecthreads: 4netty:threads: 4single-server-config:address: "redis://localhost:6379"password: nulldatabase: 0
5.2.3 读取配置
添加RedissionConfig配置类来读取配置文件:com.laigeoffer.pmhub.base.security.config.RedissonConfig
@Configuration
public class RedissonConfig {@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.port}")private int redisPort;@Value("${spring.redis.password:}") // 如果没有密码,默认值为空private String redisPassword;@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort).setPassword(redisPassword.isEmpty() ? null : redisPassword).setDatabase(0);return Redisson.create(config);}
}
5.2.4 定义ILock锁对象
com.laigeoffer.pmhub.base.security.pojo.ILock
@AllArgsConstructor
public class ILock implements AutoCloseable {/*** 持有的锁对象*/@Getterprivate Object lock;/*** 分布式锁接口*/@Getterprivate IDistributedLock distributedLock;@Overridepublic void close() throws Exception {if (Objects.nonNull(lock)) {distributedLock.unLock(lock);}}
}
5.2.5 定义IDistributedLock分布式锁接口及其实现类
com.laigeoffer.pmhub.base.security.service.redisson.IDistributedLock
public interface IDistributedLock {/*** 获取锁,默认30秒失效,失败一直等待直到获取锁** @param key 锁的key* @return 锁对象*/ILock lock(String key);/*** 获取锁,失败一直等待直到获取锁** @param key 锁的key* @param lockTime 加锁的时间,超过这个时间后锁便自动解锁; 如果lockTime为-1,则保持锁定直到显式解锁* @param unit {@code lockTime} 参数的时间单位* @param fair 是否公平锁* @return 锁对象*/ILock lock(String key, long lockTime, TimeUnit unit, boolean fair);/*** 尝试获取锁,30秒获取不到超时异常,锁默认30秒失效** @param key 锁的key* @param tryTime 获取锁的最大尝试时间* @return* @throws Exception*/ILock tryLock(String key, long tryTime) throws Exception;/*** 尝试获取锁,获取不到超时异常** @param key 锁的key* @param tryTime 获取锁的最大尝试时间* @param lockTime 加锁的时间* @param unit {@code tryTime @code lockTime} 参数的时间单位* @param fair 是否公平锁* @return* @throws Exception*/ILock tryLock(String key, long tryTime, long lockTime, TimeUnit unit, boolean fair) throws Exception;/*** 解锁** @param lock* @throws Exception*/void unLock(Object lock);/*** 释放锁** @param lock* @throws Exception*/default void unLock(ILock lock) {if (lock != null) {unLock(lock.getLock());}}}
com.laigeoffer.pmhub.base.security.service.redisson.RedissonDistributedLock
@Slf4j
@Component
public class RedissonDistributedLock implements IDistributedLock {@Resourceprivate RedissonClient redissonClient;/*** 统一前缀*/@Value("${redisson.lock.prefix:bi:distributed:lock}")private String prefix;@Overridepublic ILock lock(String key) {return this.lock(key, 0L, TimeUnit.SECONDS, false);}@Overridepublic ILock lock(String key, long lockTime, TimeUnit unit, boolean fair) {RLock lock = getLock(key, fair);// 获取锁,失败一直等待,直到获取锁,不支持自动续期if (lockTime > 0L) {lock.lock(lockTime, unit);} else {// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30slock.lock();}return new ILock(lock, this);}@Overridepublic ILock tryLock(String key, long tryTime) throws Exception {return this.tryLock(key, tryTime, 0L, TimeUnit.SECONDS, false);}@Overridepublic ILock tryLock(String key, long tryTime, long lockTime, TimeUnit unit, boolean fair)throws Exception {RLock lock = getLock(key, fair);boolean lockAcquired;// 尝试获取锁,获取不到超时异常,不支持自动续期if (lockTime > 0L) {lockAcquired = lock.tryLock(tryTime, lockTime, unit);} else {// 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30slockAcquired = lock.tryLock(tryTime, unit);}if (lockAcquired) {return new ILock(lock, this);}return null;}/*** 获取锁** @param key* @param fair* @return*/private RLock getLock(String key, boolean fair) {RLock lock;String lockKey = prefix + ":" + key;if (fair) {// 获取公平锁lock = redissonClient.getFairLock(lockKey);} else {// 获取普通锁lock = redissonClient.getLock(lockKey);}return lock;}@Overridepublic void unLock(Object lock) {if (!(lock instanceof RLock)) {throw new IllegalArgumentException("Invalid lock object");}RLock rLock = (RLock) lock;if (rLock.isLocked()) {try {rLock.unlock();} catch (IllegalMonitorStateException e) {log.error("释放分布式锁异常", e);}}}
}
5.2.6 定义DistributedLock注解
com.laigeoffer.pmhub.base.security.annotation.DistributedLock
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {/*** 保证业务接口的key的唯一性,否则失去了分布式锁的意义 锁key* 支持使用spEl表达式*/String key();/*** 保证业务接口的key的唯一性,否则失去了分布式锁的意义 锁key 前缀*/String keyPrefix() default "";/*** 是否在等待时间内获取锁,如果在等待时间内无法获取到锁,则返回失败*/boolean tryLok() default false;/*** 获取锁的最大尝试时间 ,会尝试tryTime时间获取锁,在该时间内获取成功则返回,否则抛出获取锁超时异常,tryLok=true时,该值必须大于0。**/long tryTime() default 0;/*** 加锁的时间,超过这个时间后锁便自动解锁*/long lockTime() default 30;/*** tryTime 和 lockTime的时间单位*/TimeUnit unit() default TimeUnit.SECONDS;/*** 是否公平锁,false:非公平锁,true:公平锁*/boolean fair() default false;
}
5.2.7 AOP切面控制
com.laigeoffer.pmhub.base.security.aspect.DistributedLockAspect
@Aspect
@Slf4j
@Component
public class DistributedLockAspect {@Resourceprivate IDistributedLock distributedLock;/*** SpEL表达式解析*/private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();/*** 用于获取方法参数名字*/private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();@Pointcut("@annotation(com.laigeoffer.pmhub.base.security.annotation.DistributedLock)")public void distributorLock() {}@Around("distributorLock()")public Object around(ProceedingJoinPoint pjp) throws Throwable {// 获取DistributedLockDistributedLock distributedLock = this.getDistributedLock(pjp);// 获取 lockKeyString lockKey = this.getLockKey(pjp, distributedLock);ILock lockObj = null;try {// 加锁,tryLok = true,并且tryTime > 0时,尝试获取锁,获取不到超时异常if (distributedLock.tryLok()) {if(distributedLock.tryTime() <= 0){throw new UtilException("tryTime must be greater than 0");}lockObj = this.distributedLock.tryLock(lockKey, distributedLock.tryTime(), distributedLock.lockTime(), distributedLock.unit(), distributedLock.fair());} else {lockObj = this.distributedLock.lock(lockKey, distributedLock.lockTime(), distributedLock.unit(), distributedLock.fair());}if (Objects.isNull(lockObj)) {throw new UtilException("Duplicate request for method still in process");}return pjp.proceed();} catch (Exception e) {throw e;} finally {// 解锁this.unLock(lockObj);}}/*** @param pjp* @return* @throws NoSuchMethodException*/private DistributedLock getDistributedLock(ProceedingJoinPoint pjp) throws NoSuchMethodException {String methodName = pjp.getSignature().getName();Class clazz = pjp.getTarget().getClass();Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();Method lockMethod = clazz.getMethod(methodName, par);DistributedLock distributedLock = lockMethod.getAnnotation(DistributedLock.class);return distributedLock;}/*** 解锁** @param lockObj*/private void unLock(ILock lockObj) {if (Objects.isNull(lockObj)) {return;}try {this.distributedLock.unLock(lockObj);} catch (Exception e) {log.error("分布式锁解锁异常", e);}}/*** 获取 lockKey** @param pjp* @param distributedLock* @return*/private String getLockKey(ProceedingJoinPoint pjp, DistributedLock distributedLock) {String lockKey = distributedLock.key();String keyPrefix = distributedLock.keyPrefix();if (StringUtils.isBlank(lockKey)) {throw new UtilException("Lok key cannot be empty");}if (lockKey.contains("#")) {this.checkSpEL(lockKey);MethodSignature methodSignature = (MethodSignature) pjp.getSignature();// 获取方法参数值Object[] args = pjp.getArgs();lockKey = getValBySpEL(lockKey, methodSignature, args);}lockKey = StringUtils.isBlank(keyPrefix) ? lockKey : keyPrefix + lockKey;return lockKey;}/*** 解析spEL表达式** @param spEL* @param methodSignature* @param args* @return*/private String getValBySpEL(String spEL, MethodSignature methodSignature, Object[] args) {// 获取方法形参名数组String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());if (paramNames == null || paramNames.length < 1) {throw new UtilException("Lok key cannot be empty");}Expression expression = spelExpressionParser.parseExpression(spEL);// spring的表达式上下文对象EvaluationContext context = new StandardEvaluationContext();// 给上下文赋值for (int i = 0; i < args.length; i++) {context.setVariable(paramNames[i], args[i]);}Object value = expression.getValue(context);if (value == null) {throw new UtilException("The parameter value cannot be null");}return value.toString();}/*** SpEL 表达式校验** @param spEL* @return*/private void checkSpEL(String spEL) {try {ExpressionParser parser = new SpelExpressionParser();parser.parseExpression(spEL, new TemplateParserContext());} catch (Exception e) {log.error("spEL表达式解析异常", e);throw new UtilException("Invalid SpEL expression [" + spEL + "]");}}
}
5.2.8 定义分布式锁启动元注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({DistributedLockAspect.class})
public @interface EnableDistributedLock {
}
5.3 实战使用
5.3.1 启用分布式锁
com.laigeoffer.pmhub.workflow.PmHubWorkflowApplication
5.3.2 更新审批设置接口添加注解
在具体业务场景中,如更新项目和任务审批设置时,为保证同一时刻只有一个服务能拿到锁,在WfDeployController的updateApprovalSet方法中启用分布式锁并添加注解。
com.laigeoffer.pmhub.workflow.controller.WfDeployController#updateApprovalSet
/*** 更新审批设置* @param approvalSetDTO* @return*/
@InnerAuth
@PostMapping("/updateApprovalSet")
@DistributedLock(key = "#approvalSetDTO.approved", lockTime = 10L, keyPrefix = "workflow-approve-")
public R<?> updateApprovalSet(@RequestBody ApprovalSetDTO approvalSetDTO) {return R.ok(deployService.updateApprovalSet(approvalSetDTO, ProjectStatusEnum.PROJECT.getStatusName()));
}
6 总结
本文围绕PmHub项目,先对比本地锁和分布式锁,阐述分布式锁特性。实现方式中Redis性能高,Redisson有四层保护。项目实战从添加依赖到切面控制,最后在具体接口添加注解,保证数据一致性和操作正确执行。
7 参考链接
- PmHub集成Redis分布式锁保障流程状态更新
- 项目仓库(GitHub)
- 项目仓库(码云): (国内访问速度更快)
相关文章:

【PmHub后端篇】Redis分布式锁:保障PmHub流程状态更新的关键
在分布式系统中,确保数据一致性和操作的正确执行是至关重要的。PmHub项目中,通过集成Redis分布式锁来保障流程状态更新,这是一个非常关键的技术点,以下将详细介绍其原理、实现。 1 本地锁的问题 1.1 常见的本地锁 在Java中&…...
MySQL基础入门:MySQL简介与环境搭建
引言 在数字化转型浪潮中,MySQL作为数据存储的"基石引擎",支撑着从电商交易到金融风控的各类核心业务。其高并发处理能力、灵活的架构设计及跨平台兼容性,使其成为开发者技术栈中的"常青树"。本章节将通过历史溯源、技术…...
力扣-543.二叉树的直径
题目描述 给你一棵二叉树的根节点,返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 class Solution { public:int maxLength(TreeNode *…...

Starrocks的主键表涉及到的MOR Delete+Insert更新策略
背景 写这个文章的作用主要是做一些总结和梳理,特别是正对大数据场景下的实时写入更新策略 COW 和 MOR 以及 DeleteInsert 的技术策略的演进, 这也适用于其他大数据的计算存储系统。该文章主要参考了Primary Key table. 分析总结 Starrocks 的主键表主…...

《操作系统真象还原》第十四章(2)——文件描述符、文件操作基础函数
文章目录 前言文件描述符简介文件描述符原理文件描述符实现修改thread.h修改thread.c 文件操作相关的基础函数inode操作相关函数文件相关函数编写file.h编写file.c 目录相关函数完善fs/dir.h编写fs/dir.c 路径解析相关函数实现文件检索功能修改fs.h继续完善fs.c makefile 结语 …...

EMQX v5.0通过连接器和规则同步数据
1 概述 EMQX数据集成功能,帮助用户将所有的业务数据无需额外编写代码即可快速完成处理与分发。 数据集成能力由连接器和规则两部分组成,用户可以使用数据桥接或 MQTT 主题来接入数据,使用规则处理数据后,再通过数据桥接将数据发…...

2. 盒模型/布局模块 - 响应式产品展示页_案例:电商产品网格布局
2. 盒模型/布局模块 - 响应式产品展示页 案例:电商产品网格布局 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><style type"text/css">:root {--primary-color…...

LVGL的三层屏幕结构
文章目录 🌟 LVGL 的三层屏幕架构1. **Top Layer(顶层)**2. **System Layer(系统层)**3. **Active Screen(当前屏幕层)** 🧠 总结对比🔍 整体作用✅ 普通屏幕层对象&…...

【PDF】使用Adobe Acrobat dc添加水印和加密
【PDF】使用Adobe Acrobat dc添加水印和加密 文章目录 [TOC](文章目录) 前言一、添加保护加密口令二、添加水印三、实验四、参考文章总结 实验工具: 1.Adobe Acrobat dc 前言 提示:以下是本篇文章正文内容,下面案例可供参考 一、添加保护加…...
AI 搜索引擎 MindSearch
背景 RAG是一种利用文档减少大模型的幻觉,AI搜索也是 AI 搜索引擎 MindSearch 是一个开源的 AI 搜索引擎框架,具有与 Perplexity.ai Pro 相同的性能。您可以轻松部署它来构建您自己的搜索引擎,可以使用闭源 LLM(如 GPT、Claude…...

Windows下安装mysql8.0
一、下载安装离线安装包 (下载过了,可以跳过) 下载网站:MySQL :: Download MySQL Installerhttps://dev.mysql.com/downloads/installer/ 二、安装mysql 三、安装完成验证...
【android bluetooth 框架分析 02】【Module详解 7】【VendorSpecificEventManager 模块介绍】
1. 背景 我们在 gd_shim_module 介绍章节中,看到 我们将 VendorSpecificEventManager 模块加入到了 modules 中。 // system/main/shim/stack.cc modules.add<hci::VendorSpecificEventManager>();在 ModuleRegistry::Start 函数中我们对 加入的所有 module…...

水滴Android面经及参考答案
static 关键字有什么作用,它修饰的方法可以使用非静态的成员变量吗? static关键字在 Java 中有多种作用。首先,它可以用来修饰变量,被static修饰的变量称为静态变量。静态变量属于类,而不属于类的某个具体实例…...

工程师必读! 3 个最常被忽略的 TDR 测试关键细节与原理
TDR真的是一个用来看阻抗跟Delay的好工具,通过一个Port的测试就可以看到通道各个位置的阻抗变化。 可是使用上其实没这么单纯,有很多细节需要非常地小心,才可以真正地看到您想看的信息! 就让我们整理3个极为重要的TDR使用小细节&…...

C++中的各式类型转换
隐式转换: 基本类型的隐式转换: 当函数参数类型非精确匹配,但是可以转换的时候发生 如: void func1(double x){cout << x << endl; }void func2(char c){cout << c << endl; }int main(){func1(2);//…...
2025年阿里云ACP人工智能高级工程师认证模拟试题(附答案解析)
这篇文章的内容是阿里云ACP人工智能高级工程师认证考试的模拟试题。 所有模拟试题由AI自动生成,主要为了练习和巩固知识,并非所谓的 “题库”,考试中如果出现同样试题那真是纯属巧合。 1、在PAl-Studio实验运行完毕后,可以右键单…...
如何使用scp命令拉取其他虚拟机中的文件
使用 SCP 命令拉取远程虚拟机文件 scp(Secure Copy)是基于 SSH 协议的安全文件传输工具,可以在本地与远程主机之间复制文件。以下是使用scp从其他虚拟机拉取文件的详细指南: 一、基本语法 bash # 从远程主机复制到本地 scp [选…...

Nacos源码—9.Nacos升级gRPC分析七
大纲 10.gRPC客户端初始化分析 11.gRPC客户端的心跳机制(健康检查) 12.gRPC服务端如何处理客户端的建立连接请求 13.gRPC服务端如何映射各种请求与对应的Handler处理类 14.gRPC简单介绍 10.gRPC客户端初始化分析 (1)gRPC客户端代理初始化的源码 (2)gRPC客户端启动的源码…...
从入门到精通:Drools全攻略
目录 一、Drools 初相识二、快速上手 Drools2.1 环境搭建2.2 第一个 Drools 程序 三、深入理解 Drools 核心概念3.1 规则(Rule)3.2 工作内存(Working Memory)3.3 知识库(Knowledge Base, KieBase)3.4 会话&…...
最大子段和(递推)
题目描述 给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。 输入格式 第一行是一个整数,表示序列的长度 n。 第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字 ai。 输出格式 输出一行一个整数表示答案。 输…...

【计算机视觉】基于深度学习的实时情绪检测系统:emotion-detection项目深度解析
基于深度学习的实时情绪检测系统:emotion-detection项目深度解析 1. 项目概述2. 技术原理与模型架构2.1 核心算法1) 数据预处理流程2) 改进型MobileNetV2 2.2 系统架构 3. 实战部署指南3.1 环境配置3.2 数据集准备3.3 模型训练3.4 实时推理 4. 常见问题与解决方案4.…...
Windows CMD通过adb检查触摸屏Linux驱动是否被编译
检查 CONFIG_TOUCHSCREEN_GT9XX 是否启用,检查内核是否编译了Goodix GT9XX系列触摸屏的驱动支持 Windows CMD.exe输入: adb shell “zcat /proc/config.gz | grep CONFIG_TOUCHSCREEN_GT9XX” 如果返回CONFIG_TOUCHSCREEN_GT9XXy,表示驱动已编…...

【图像处理基石】什么是油画感?
在图像处理中,“油画感”通常指图像呈现出类似油画的块状纹理、笔触痕迹或色彩过渡不自然的现象,表现为细节模糊、边缘不锐利、颜色断层或人工纹理明显。这种问题常见于照片处理、视频帧截图或压缩后的图像,本质是画质受损的一种表现。以下是…...

AD PCB布线的常用命令
PCB布线顺序:先信号,再电源,再GNG 1.多根走线的应用 将IC上的引脚分类 更改一类引脚以及引线的颜色,画出走线(将脚引出) 选中这些走线,点击‘交互式总线布线’,便可以多根拉线 shi…...
Python操作Elasticsearch实战指南:从安装到性能调优的全链路解析
一、引言:为什么选择Python+Elasticsearch? Elasticsearch作为分布式搜索引擎,在日志分析、全文检索等场景中表现卓越。Python凭借其简洁语法和丰富生态,成为操作ES的首选语言。本文将带您从环境搭建到性能调优,系统掌握Python操作ES的核心技能。 二、环境准备:三步完成…...

【3-2】HDLC
前言 前面我们提到了 PSTN(Public Switched Telephone Network) ,今天介绍一种很少见的数据链路层的协议,HDLC! 文章目录 前言1. 定义2. 帧边界3. 零比特填充4. 控制字段4.1. 信息帧(I帧)4.2. …...

MySQL 学习(八)如何打开binlog日志
目录 一、默认状态二、如何检查 binlog 状态三、如何开启 binlog3.1 临时开启(重启后失效)3.2 永久开启(需修改配置文件)3.3 验证是否开启成功3.4 查看 binlog 内容 四、高级配置建议五、注意事项六、开启后的日常维护 知识回顾&a…...
《数据库原理》部分习题解析
《数据库原理》部分习题解析 1. 课本pg196.第1题。 (1)函数依赖 若对关系模式 R(U) 的任何可能的关系 r,对于任意两个元组 t₁ 和 t₂,若 t₁[X] t₂[X],则必须有 t₁[Y] t₂[Y],则称属性集 Y 函数依赖…...

OpenCV进阶操作:光流估计
文章目录 前言一、光流估计1、光流估计是什么?2、光流估计的前提?1)亮度恒定2)小运动3)空间一致 3、OpenCV中的经典光流算法1)Lucas-Kanade方法(稀疏光流)2) Farneback方…...
uniapp+vue3开发项目之引入vuex状态管理工具
前言: 我们在vue2的时候常用的状态管理工具就是vuex,vue3开发以后,又多了一个pinia的选项,相对更轻便,但是vuex也用的非常多的,这里简单说下在uni-app中vuex的使用。 实现步骤: 1、安装&#x…...