分布式锁在分布式业务场景中经常用到。想要实现分布式锁,必须要求 Redis 有「互斥」的能力,我们可以使用 SETNX
命令,这个命令表示SET if Not Exists
,即如果 key 不存在,才会设置它的值,否则什么也不做。
设置锁
客户端1:
127.0.0.1:6379> setnx lock l
(integer) 1
客户端2:
127.0.0.1:6379> setnx lock l
(integer) 0
当客户端1设置一个锁,客户端2想设置同一个锁的时候结果为“0
”并未成功,这就是setnx的作用。
设置超时时间
客户端1:
127.0.0.1:6379> setnx lock l
(integer) 1
# 设置超时时间10s
127.0.0.1:6379> EXPIRE lock 10
(integer) 1
客户端2:
127.0.0.1:6379> setnx lock l
(integer) 0
# 客户端1设置了10s超时,在执行上锁成功
127.0.0.1:6379> setnx lock l
(integer) 1
这有个问题,①设置锁
②设置超时时间
,这是两个命令不是原子性的,极易造成死锁
(锁设置成功,超时时间设置失败),所以我们需要一个原子性
的语句:
SET key value EX 秒 PX 毫秒 NX XX
127.0.0.1:6379> set lock l ex 10 nx
OK
如何避免锁被别人释放
在分布式系统中,我们通常会使用业务中的唯一标识作为锁的 key。但这种方式存在一个严重的问题:客户端在释放锁时,往往是“无脑”释放,并没有检查这把锁是否仍然归自己持有。
这种做法带来的风险是显而易见的:可能会误删其他客户端持有的锁,导致并发安全问题。显然,这样的解锁流程并不严谨。
在客户端加锁时,不仅要设置锁的 key,还应写入一个只有当前客户端知道的唯一标识
作为 value。
# 加锁
SET lock_key uuid_value NX EX 10
# 解锁
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Java代码实现分布式锁
/**
* 分布式锁的实现
*/
@Component
public class RedisDistLock implements Lock {
private final static int LOCK_TIME = 5_000;
private final static String DISTLOCK_KEY = "distkey:";
private final static String RELEASE_LOCK_LUA =
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" return redis.call('del', KEYS[1])\n" +
" else return 0 end";
/*保存每个线程的独有的ID值*/
private ThreadLocal<String> lockerId = new ThreadLocal<>();
/*解决锁的重入*/
private Thread ownerThread;
private String lockName = "lock";
@Autowired
private JedisPool jedisPool;
public void setOwnerThread(Thread ownerThread) {
this.ownerThread = ownerThread;
}
@Override
public void lock() {
while(!tryLock()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException("不支持可中断获取锁!");
}
@Override
public boolean tryLock() {
Thread t = Thread.currentThread();
if(ownerThread==t){/*说明本线程持有锁*/
return true;
}else if(ownerThread!=null){/*本进程里有其他线程持有分布式锁*/
return false;
}
Jedis jedis = null;
try {
String id = UUID.randomUUID().toString();
SetParams params = new SetParams();
params.px(LOCK_TIME);
params.nx();
synchronized (this){/*线程们,本地抢锁*/
if((ownerThread==null)&&
"OK".equals(jedis.set(DISTLOCK_KEY+lockName,id,params))){
lockerId.set(id);
setOwnerThread(t);
return true;
}else{
return false;
}
}
} catch (Exception e) {
throw new RuntimeException("分布式锁尝试加锁失败!");
} finally {
jedis.close();
}
}
@Override
public boolean tryLock(long time, TimeUnit unit){
throw new UnsupportedOperationException("不支持等待尝试获取锁!");
}
@Override
public void unlock() {
if(ownerThread!=Thread.currentThread()) {
throw new RuntimeException("试图释放无所有权的锁!");
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,
Arrays.asList(DISTLOCK_KEY+lockName),
Arrays.asList(lockerId.get()));
if(result.longValue()!=0L){
System.out.println("Redis上的锁已释放!");
}else{
System.out.println("Redis上的锁释放失败!");
}
} catch (Exception e) {
throw new RuntimeException("释放锁失败!",e);
} finally {
if(jedis!=null) jedis.close();
lockerId.remove();
setOwnerThread(null);
System.out.println("本地锁所有权已释放!");
}
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("不支持等待通知操作!");
}
}
分布式锁自动续期
一般我们的业务场景中往往会出现处理时长超过我们设置的分布式锁的期限的场景,此时,我们需要对锁进行续期操作。操作流程如下:
加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。
这个守护线程我们一般也把它叫做「看门狗」线程。
/**
* 分布式锁,附带看门狗线程的实现:加锁,保持锁1秒
*/
@Component
public class RedisDistLockWithDog implements Lock {
private final static int LOCK_TIME = 1_000;
private final static String LOCK_TIME_STR = String.valueOf(LOCK_TIME);
private final static String DISTLOCK_KEY = "distkey2:";
private final static String RELEASE_LOCK_LUA =
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" return redis.call('del', KEYS[1])\n" +
" else return 0 end";
/*还有并发问题,考虑ThreadLocal*/
private ThreadLocal<String> lockerId = new ThreadLocal<>();
private Thread ownerThread;
private String lockName = "lock";
@Autowired
private JedisPool jedisPool;
public void setOwnerThread(Thread ownerThread) {
this.ownerThread = ownerThread;
}
@Override
public void lock() {
while(!tryLock()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new UnsupportedOperationException("不支持可中断获取锁!");
}
@Override
public boolean tryLock() {
Thread t=Thread.currentThread();
/*说明本线程正在持有锁*/
if(ownerThread==t) {
return true;
}else if(ownerThread!=null){/*说明本进程中有别的线程正在持有分布式锁*/
return false;
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
/*每一个锁的持有人都分配一个唯一的id,也可采用snowflake算法*/
String id = UUID.randomUUID().toString();
SetParams params = new SetParams();
params.px(LOCK_TIME); //加锁时间1s
params.nx();
synchronized (this){
if ((ownerThread==null)&&
"OK".equals(jedis.set(DISTLOCK_KEY+lockName,id,params))) {
lockerId.set(id);
setOwnerThread(t);
if(expireThread == null){//看门狗线程启动
expireThread = new Thread(new ExpireTask(),"expireThread");
expireThread.setDaemon(true);
expireThread.start();
}
//往延迟阻塞队列中加入元素(让看门口可以在过期之前一点点的时间去做锁的续期)
delayDog.add(new ItemVo<>((int)LOCK_TIME,new LockItem(lockName,id)));
System.out.println(Thread.currentThread().getName()+"已获得锁----");
return true;
}else{
System.out.println(Thread.currentThread().getName()+"无法获得锁----");
return false;
}
}
} catch (Exception e) {
throw new RuntimeException("分布式锁尝试加锁失败!",e);
} finally {
jedis.close();
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException("不支持等待尝试获取锁!");
}
@Override
public void unlock() {
if(ownerThread!=Thread.currentThread()) {
throw new RuntimeException("试图释放无所有权的锁!");
}
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long result = (Long)jedis.eval(RELEASE_LOCK_LUA,
Arrays.asList(DISTLOCK_KEY+lockName),
Arrays.asList(lockerId.get()));
System.out.println(result);
if(result.longValue()!=0L){
System.out.println("Redis上的锁已释放!");
}else{
System.out.println("Redis上的锁释放失败!");
}
} catch (Exception e) {
throw new RuntimeException("释放锁失败!",e);
} finally {
if(jedis!=null) jedis.close();
lockerId.remove();
setOwnerThread(null);
}
}
@Override
public Condition newCondition() {
throw new UnsupportedOperationException("不支持等待通知操作!");
}
/*看门狗线程*/
private Thread expireThread;
//通过delayDog 避免无谓的轮询,减少看门狗线程的轮序次数 阻塞延迟队列 刷1 没有刷2
private static DelayQueue<ItemVo<LockItem>> delayDog = new DelayQueue<>();
//续锁逻辑:判断是持有锁的线程才能续锁
private final static String DELAY_LOCK_LUA =
"if redis.call('get',KEYS[1])==ARGV[1] then\n" +
" return redis.call('pexpire', KEYS[1],ARGV[2])\n" +
" else return 0 end";
private class ExpireTask implements Runnable{
@Override
public void run() {
System.out.println("看门狗线程已启动......");
while(!Thread.currentThread().isInterrupted()) {
try {
LockItem lockItem = delayDog.take().getData();//只有元素快到期了才能take到 0.9s
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Long result = (Long)jedis.eval(DELAY_LOCK_LUA,
Arrays.asList(DISTLOCK_KEY+lockItem.getKey ()),
Arrays.asList(lockItem.getValue(),LOCK_TIME_STR));
if(result.longValue()==0L){
System.out.println("Redis上的锁已释放,无需续期!");
}else{
delayDog.add(new ItemVo<>((int)LOCK_TIME,
new LockItem(lockItem.getKey(),lockItem.getValue())));
System.out.println("Redis上的锁已续期:"+LOCK_TIME);
}
} catch (Exception e) {
throw new RuntimeException("锁续期失败!",e);
} finally {
if(jedis!=null) jedis.close();
}
} catch (InterruptedException e) {
System.out.println("看门狗线程被中断");
break;
}
}
System.out.println("看门狗线程准备关闭......");
}
}
@PreDestroy
public void closeExpireThread(){
if(null!=expireThread){
expireThread.interrupt();
}
}
}
RedLock
之前分析的场景都是,锁在单个Redis实例
中可能产生的问题,并没有涉及到 Redis 的部署架构细节。
而我们在使用 Redis 时,一般会采用主从集群 +哨兵
的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现「故障自动切换」,把从库提升为主库,继续提供服务,以此保证可用性。
但是因为主从复制是异步的,那么就不可避免会发生的锁数据丢失问题(加了锁却没来得及同步过来)。从库被哨兵提升为新主库,这个锁在新的主库上,丢失了!
Redis 作者提出的 Redlock
方案,是如何解决主从切换后,锁失效问题的。
Redlock 的方案基于一个前提:
- 不再需要部署从库和哨兵实例,只部署主库;但主库要部署多个,官方推荐至少 5 个实例。
- 注意:不是部署 Redis Cluster,就是部署 5 个简单的 Redis 实例。它们之间没有任何关系,都是一个个孤立的实例。
RedLock的争议点
Dr. Martin Kleppmann:How to do distributed locking
- 客户端1获取锁,但是执行业务逻辑的同时,发生GC;
- 锁超时发生在在GC执行完毕之前;
- 此时客户端2已经重新获取了锁并执行了更新数据的操作;
- 客户端1的GC执行完毕,继续执行更新操作;
- 客户端1将旧数据更新
Dr. Martin Kleppmann 提出了一个解决办法,请至原文查看。
Redisson
前面介绍了直接使用 Redis 命令,如 SET lock_key uuid NX PX 30000
,再加一些 Lua 脚本辅助释放锁,支持简单自动续期逻辑。
Redisson
是基于 Redis
封装的一整套分布式协调工具,支持分布式锁(RLock)、看门狗、异步API、限流、延迟队列等,内部集成了重试、自动续期、RedLock 多节点机制等。
维度 | 🧰 自己实现 Redis 分布式锁 | 🚀 Redisson 分布式锁 |
---|---|---|
基本实现方式 | 使用命令 SET NX EX + Lua 脚本 | 封装多种 Redis 操作 + 看门狗 + 多种锁类型 |
自动续期(看门狗) | ❌ 需要自己实现定时任务续期 | ✅ 内置,默认自动续期(默认 30s) |
加锁唯一标识(如 UUID) | ✅ 可自行加 UUID | ✅ 内部自动管理唯一标识 |
释放锁前校验持有者身份 | ❌ 容易遗漏,需手动校验 | ✅ 自动校验,只能释放自己加的锁 |
异常恢复能力(宕机、GC卡顿等) | ❌ 容易死锁 / 锁丢失 | ✅ 看门狗保障长期持锁,线程意外终止也会安全释放 |
支持的锁类型 | 🚫 只支持互斥锁 | ✅ 支持可重入锁、读写锁、公平锁、信号量等 |
集群/哨兵兼容性 | ❌ 自己处理 Redis 路由、主从切换 | ✅ 自动感知主节点变动,稳定性更强 |
RedLock 多主容灾支持 | 🚫 不支持,需要自己实现 | ✅ 提供 RedissonRedLock 实现(多主 Redis) |
连接管理(连接池、故障转移) | ❌ 自己手动处理 | ✅ 内部封装 Netty 连接池,容错好 |
API 使用体验 | 🧱 低级命令,需写 Lua 和处理异常 | 🧼 高级 API,支持 tryLock、lockAsync、时间配置等 |
学习和集成成本 | ✅ 起步简单 | ❌ 依赖大,需要理解配置和生命周期 |
性能 | ✅ 更轻量,无额外封装 | ⚠️ 略有封装成本,但通常影响可接受 |
可靠性 | ⚠️ 靠实现质量 | ✅ 社区验证,功能完善,可靠性强 |
适合的业务场景 | 简单锁、低并发、不要求容灾 | 中高并发、线上核心流程、容错要求高 |
额外功能 | ❌ 只有锁 | ✅ 内置布隆过滤器、延迟队列、限流器等 |