redis分布式锁各种坑,一文全部搞懂
mhr18 2024-10-22 12:36 23 浏览 0 评论
目录
- 分布式锁概念
- 分布式锁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算法
相关推荐
- Java培训机构,你选对了吗?(java培训机构官网)
-
如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...
- 产品经理MacBook软件清单-20个实用软件
-
三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...
- RAD Studio(Delphi) 本月隆重推出新的版本12.3
-
#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...
- 图解Java垃圾回收机制,写得非常好
-
什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...
- Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷
-
1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...
- 半虚拟化如何提高服务器性能(虚拟化 半虚拟化)
-
半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...
- HashMap底层实现原理以及线程安全实现
-
HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...
- long和double类型操作的非原子性探究
-
前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...
- 数据库DELETE 语句,还保存原有的磁盘空间
-
MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...
- 线程池—ThreadPoolExecutor详解(线程池实战)
-
一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...
- navicat如何使用orcale(详细步骤)
-
前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...
- 你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开
-
各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...
- 大数据正当时,理解这几个术语很重要
-
目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...
- 秒懂列式数据库和行式数据库(列式数据库的特点)
-
行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...
- AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4
-
AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle 空为0 (51)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)