网站首页 > 开源技术 正文
目录
- 分布式锁概念
- 分布式锁4种雷区
- 分布式锁特性
- 错误案例集
- 正确实现及实现原理
- 锁超时并发执行 解决方案
- 集群容错 解决方案
学习目标
- 分布式锁概念
- 分布式锁4种雷区
- 分布式锁特性
分布式锁概念
在分布式系统中,同一时间只允许一个线程/进程对共享资源进行操作。例如:秒杀、积分扣减、抢红包、定时任务执行等等。
分布式锁4种雷区
- 死锁:加锁成功后,不知什么原因导致服务器出现宕机,未能成功释放,出现死锁。正确做法:设置超时时间
- 锁误删:只有持有当前锁的线程,才能删除锁,即:解铃还需系铃人。正确做法:唯一id标识当前线程
- 锁超时并发执行:加锁成功后,由于代码执行非常耗时、下游服务执行慢、调用链太长或GC耗时等原因导致锁超时,其他线程获得锁出现并发执行,后面详细分析。
- 集群容错:成功在master加锁,未能及时同步到slave节点,此时出现脑裂存在多个master节点,其他节点也可以加锁成功,后面详细分析。
分布式锁特性
- 互斥性:当一个线程/进程加锁成功后,其他线程/进程无法加锁,具有排他性。
- 锁失效机制:加锁成功后,服务器宕机导致锁未能释放,服务恢复后一直获取不到锁。应设置超时时间,防止出现类似死锁情况。
- 阻塞锁(可选):当前资源已被加锁,其他线程/进程来加锁是否阻塞等待,还是立即返回。
- 可重入性(可选):当前锁的持有者是否能再次进入。
- 公平性(可选):加锁顺序和请求加锁顺序是否一致,还是随机抢锁。
基于redis的分布式锁
错误案例集
加锁-错误案例1
public void lock_error1(String lockKey, String requestId, int expireTime) {
RedisCache cache = redisFactory.getRedisCacheInstance(name);
Long result = cache.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
cache.expireSeconds(lockKey, expireTime);
}
}
setnx和expire两个操作非原子性,expire操作之前程序崩溃,会发生死锁。
加锁-错误案例2(严格意义属于错误案例)
public String lock_error2(String lockKey, int expireTime) {
RedisCache cache = redisFactory.getRedisCacheInstance(name);
long lockExpireTime = System.currentTimeMillis() + expireTime * 1000 + 1;//锁超时时间
String stringOfLockExpireTime = String.valueOf(lockExpireTime);
if (cache.setnx(lockKey, stringOfLockExpireTime) == 1) { // 获取到锁
//成功获取到锁, 设置相关标识
return stringOfLockExpireTime;
}
String value = cache.get(lockKey);
if (value != null && isTimeExpired(value)) { // lock is expired
// 假设多个线程(非单jvm)同时走到这里
String oldValue = cache.getSet(lockKey, stringOfLockExpireTime); // getset is atomic
// 但是走到这里时每个线程拿到的oldValue肯定不可能一样(因为getset是原子性的)
// 假如拿到的oldValue依然是expired的,那么就说明拿到锁了
if (oldValue != null && isTimeExpired(oldValue)) {
//成功获取到锁, 设置相关标识
return stringOfLockExpireTime;
}
}
return null;
}
这种加锁方式解决的问题是程序崩溃/超时,未能释放导致死锁,使用该方案的前提是:各个服务器时间必须同步,在cache.getSet(lockKey, stringOfLockExpireTime)时会出现时间覆盖问题,只要各个服务器时间同步,时间覆盖也不影响加锁效果,不应该属于错误案例,因为出现时间覆盖了,严格来说就是错误的,主要看怎么定义了。我们原来一直用的是这种方式。
解锁-错误案例1
public void unLock_error1(String lockKey) {
RedisCache cache = redisFactory.getRedisCacheInstance(name);
cache.del(lockKey);
}
不分是不是自己持有的锁,上来就删除,导致锁误删除。
解锁-错误案例2
public void unLock_error2(String lockKey, String requestId) {
RedisCache cache = redisFactory.getRedisCacheInstance(name);
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(cache.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
cache.del(lockKey);
}
}
如代码注释,问题在于如果调用cache.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行cache.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了,这也是锁误删除的例子。
正确实现
实现原理
加锁:使用set扩展命令,key:锁标识,value:持有当前锁线程标识,PX:超时时间(毫秒)。
解锁:只有当前锁的持有者才可以执行删除操作,通过lua脚本保证了get和del命令执行的原子性操作。
# 加锁命令
set key value NX PX milliseconds
# 解锁命令
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
实现代码
// 加锁
public boolean lockByLua(String lockKey, String requestId, int expireTime) {
RedisCache cache = redisFactory.getRedisCacheInstance(name);
String result = cache.set(lockKey,requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
// 解锁
public boolean unLockByLua(String lockKey, String requestId) {
Long success = 1L;
RedisCache cache = redisFactory.getRedisCacheInstance(name);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = cache.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (success.equals(result)) {
return true;
}
return false;
}
不足之处
- 未实现可阻塞,可重入性,公平性。
- 未能解决锁超时并发执行,集群容错。
锁超时并发执行 解决方案
问题现象
A成功获取锁后并设置超时时间5秒,但是A业务执行超过了5秒,A持有锁过期自动释放,B获取到锁,A和B并发执行。
A和B并发执行显然是不允许的,一般两种解决方式:
- 设置足够长的时间来保证业务执行完成。
- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
实现方案
守护线程自动续期代码实现:
@Slf4j
public class ExpireDelayThread implements Runnable {
/**
* 锁
*/
private String lockKey;
/**
* 持锁线程标识id
*/
private String requestId;
/**
* 过期时间(单位:毫秒)
*/
private Integer expireTime;
private RedisClient redisClient;
private volatile boolean isRun = true;
public ExpireDelayThread(String lockKey, String requestId, Integer expireTime, RedisClient redisClient){
this.lockKey = lockKey;
this.requestId = requestId;
this.expireTime = expireTime;
this.redisClient = redisClient;
}
public void stop(){
isRun = false;
}
@Override
public void run() {
int waitTime = Math.max(1, expireTime * 2 /3);
while (isRun) {
try {
Thread.sleep(waitTime);
if (redisClient.exprieDelayByLua(lockKey,requestId)){
log.info("lock key:{}, thread requestId:{}, waitTime:{}, exprie delay time:{}",lockKey,requestId,waitTime,expireTime);
} else {
log.info("lock key:{}, thread requestId:{}, waitTime:{}, exprie delay time failed!",lockKey,requestId,waitTime);
this.stop();
}
} catch (InterruptedException e) {
log.error("lock key:{}, thread requestId:{}, waitTime:{}, InterruptedException!",lockKey,requestId,waitTime);
} catch (Exception ex) {
log.error("lock key:"+ lockKey +", thread requestId:"+ expireTime +", waitTime:"+ waitTime +", error!",ex);
}
}
}
}
集群容错 解决方案
问题现象
- 主从切换:在sentinel集群部署中,当主节点挂掉时,从节点会取而代之,但客户端无明显感知。当客户端 A 成功加锁,指令还未同步,此时主节点挂掉,从节点提升为主节点,新的主节点没有锁的数据,当客户端 B 加锁时就会成功。
- 集群脑裂:集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。
当不同的客户端连接不同的 master 节点时,两个客户端可以同时拥有同一把锁。如下:
实现方案
参考RedLock算法
猜你喜欢
- 2024-10-27 Golang 入门系列(七)整合Redis详解,实战
- 2024-10-27 几个小技巧,让你的Redis程序快如闪电
- 2024-10-27 掌握这些 Redis 技巧,百亿数据量不在话下
- 2024-10-27 「高频 Redis 面试题」Redis 事务是否具备原子性?
- 2024-10-27 Spring系列之Redis的两种集成方式
- 2024-10-27 架构篇-一分钟掌握可扩展架构(可扩展是什么意思)
- 2024-10-27 图解Redis-命令系统设计(redis命令大全)
- 2024-10-27 Redis 数据持久化与发布订阅(redis持久化aof)
- 2024-10-27 依赖倒置原则详解(对依赖倒置的表述错误的是)
- 2024-10-27 五种分布式锁(分布式锁最佳实践)
你 发表评论:
欢迎- 03-19基于layui+springcloud的企业级微服务框架
- 03-19开箱即用的前端开发模板,扩展Layui原生UI样式,集成第三方组件
- 03-19SpringMVC +Spring +Mybatis + Layui通用后台管理系统OneManageV2.1
- 03-19SpringBoot+LayUI后台管理系统开发脚手架
- 03-19layui下拉菜单form.render局部刷新方法亲测有效
- 03-19Layui 遇到的坑(记录贴)(layui chm)
- 03-19基于ASP.NET MVC + Layui的通用后台开发框架
- 03-19LayUi自定义模块的定义与使用(layui自定义表格)
- 最近发表
-
- 基于layui+springcloud的企业级微服务框架
- 开箱即用的前端开发模板,扩展Layui原生UI样式,集成第三方组件
- SpringMVC +Spring +Mybatis + Layui通用后台管理系统OneManageV2.1
- SpringBoot+LayUI后台管理系统开发脚手架
- layui下拉菜单form.render局部刷新方法亲测有效
- Layui 遇到的坑(记录贴)(layui chm)
- 基于ASP.NET MVC + Layui的通用后台开发框架
- LayUi自定义模块的定义与使用(layui自定义表格)
- Layui 2.9.11正式发布(layui2.6)
- Layui 2.9.13正式发布(layui2.6)
- 标签列表
-
- jdk (81)
- putty (66)
- rufus (78)
- 内网穿透 (89)
- okhttp (70)
- powertoys (74)
- windowsterminal (81)
- netcat (65)
- ghostscript (65)
- veracrypt (65)
- asp.netcore (70)
- wrk (67)
- aspose.words (80)
- itk (80)
- ajaxfileupload.js (66)
- sqlhelper (67)
- express.js (67)
- phpmailer (67)
- xjar (70)
- redisclient (78)
- wakeonlan (66)
- tinygo (85)
- startbbs (72)
- webftp (82)
- vsvim (79)
本文暂时没有评论,来添加一个吧(●'◡'●)