一、什么是分布式锁
在单体项目中,我们可以直接使用synchronized或者reentrantlock实现锁,控制多线程并发访问共享资源。 但是分布式环境下,服务部署在多个不同服务器,jvm 之间相互隔离,本地锁失效。 分布式锁就是用来在多个独立服务实例之间,实现互斥访问共享资源的锁方案。
主流分布式锁实现对比
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| Mysql | 唯一索引 / 悲观锁 | 实现简单 | 性能差,容易死锁,超时不好处理 |
| Redis | setnx + 过期时间 | 高性能,实现简单 | 需要自己处理锁续期、锁误删问题 |
| Zookeeper | 临时有序节点 | 可靠性高 | 性能偏低,重连会释放锁,运维复杂 |
项目中最常用的就是Redis 分布式锁,下面一步步从零实现,逐个解决遇到的坑。
// 先setnx,再expire,两条独立命令,不具备原子性
boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if(lock){
// 如果这里程序崩溃,expire没有执行,锁永远不会释放!
redisTemplate.expire("lock",30,timeunit.seconds);
//执行业务
}
❗问题:
setnx和expire是两条命令,中间一旦服务宕机,锁永久存在,死锁。
正确原子命令(Redis 单命令保证原子性)
// set key value nx ex 过期时间,一条命令完成加锁+设置过期时间
boolean ok = redisTemplate.opsForValue()
.setIfAbsent("lock", uuid, 30, timeunit.seconds);
nx:not exist,不存在才写入;ex:设置过期时间。
这里 value 存入 uuid,不是随便写死数字,为了区分锁是谁加的,防止线程误删别人的锁。
三、第一个大坑:锁被别的线程释放
场景还原:
- 线程 a 拿到锁,过期时间 30s
- 业务执行很慢,超过 30 秒,redis 自动把锁释放
- 线程 b 成功获取到同一个 lock 锁,开始执行业务
- 线程 a 业务终于跑完,执行释放锁代码,直接 del lock,把线程 b 持有的锁删掉了!
解决方案:释放锁前,先判断锁的 value 是不是自己的 uuid,只有自己的锁,才允许删除。
// 伪代码:获取当前锁的值
string val = redisTemplate.opsForValue().get("lock");
if(uuid.equals(val)){
// 确认是自己的锁,再删除
redisTemplate.delete("lock");
}
但是!这里依然存在新问题:get 和 delete 是两条命令,非原子操作 极端情况:
- 判断 value 相等之后,准备执行 delete
- 此时锁刚好过期,redis 自动释放锁,其他线程拿到锁
- 当前线程执行 del,删掉别人新锁。
👉get+del 必须保证原子性,这时候就需要 Lua 脚本。
四、Lua 脚本解决原子性问题
Redis 执行 Lua 脚本时,整个脚本作为一个原子操作执行,不会被其他命令打断,完美解决多条命令原子性问题。
Lua 脚本(释放锁脚本)
-- 判断key对应value是否等于传入的uuid,相等才删除,返回1;否则返回0
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
java 代码调用脚本:
string script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisscript<long> redisscript = redisscript.of(script);
// keys集合放lock key,args放当前线程的uuid
long result = redistemplate.execute(redisscript, lists.newarraylist("lock"), uuid);
五、第二个大坑:锁超时,业务还没执行完(锁续期问题)
场景:锁过期时间 30s,但是业务执行时间超过 30 秒,redis 自动释放锁,其他线程抢占锁。 方案:看门狗(watchdog)自动续期 原理:
- 获取锁成功后,开启一个后台定时线程
- 每隔一段时间(一般是过期时间的 1/3,比如 30s 过期,每 10s 执行一次)
- 判断锁还存在,就延长锁的过期时间
- 业务执行完毕,关闭定时任务,释放锁
手写看门狗比较繁琐,生产环境一般直接使用 redisson 框架,内置看门狗。
六、Redisson 框架快速实现分布式锁
redisson 对 redis 分布式锁做了封装,自动处理:原子加锁、lua 释放锁、看门狗自动续期、可重入锁。
1. maven 依赖
<dependency>
<groupid>org.redisson</groupid>
<artifactid>redisson-spring-boot-starter</artifactid>
<version>3.17.7</version>
</dependency>
2. redisson 配置类
@configuration
public class redissonconfig {
@bean
public redissonclient redissonclient(){
config config = new config();
// 配置redis地址
config.usesingleserver().setaddress("redis://127.0.0.1:6379");
return redisson.create(config);
}
}
3. 业务代码使用
@autowired
private redissonclient redissonclient;
public void testlock(){
rlock lock = redissonclient.getlock("product:lock");
try{
// 尝试获取锁,最多等待10秒,锁默认30s过期,自动看门狗续期
boolean isget = lock.trylock(10,30, timeunit.seconds);
if(isget){
// 执行业务逻辑
}
}catch (exception e){
e.printstacktrace();
}finally {
// 判断当前线程持有锁,才释放
if(lock.isheldbycurrentthread()){
lock.unlock();
}
}
}
edisson 的 rlock 是可重入锁,同一个线程可以多次加锁,底层也是 lua 脚本实现计数。
七、第三个大坑:redis 主从集群下锁失效(主从同步问题)
场景:
- 客户端在 redis 主节点成功 set lock,拿到锁
- 主节点还没来得及把数据同步给从节点,主节点宕机
- 从节点升级成新主节点,新主没有 lock 这条数据,其他客户端又拿到锁
结果:两个客户端同时持有锁,锁失效!
解决方案:
- redlock 红锁方案(多个独立 redis 节点,过半节点加锁成功才算拿到锁)
- 业务层面做兜底,数据库增加唯一约束,防止超卖等严重业务问题
红锁代价高,生产中很少用;大部分业务,主从延迟带来的风险可以接受,配合数据库兜底。
八、总结
- 基础分布式锁,加锁和过期时间必须一条原子命令,不能分两次执行
- 释放锁不能直接 del,必须校验锁归属,get+del 要使用 lua 脚本保证原子性
- 业务执行时间超过锁过期时间 → 需要看门狗自动续期,推荐 redisson
- redis 主从切换会存在锁失效风险,重要业务必须增加数据库兜底方案
- 分布式锁核心目标:保证同一时刻,只有一个线程执行共享资源操作
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2601_96029214/article/details/167399152




