Redis分布式锁的实现(Redission)
写在前面
本人在学习Redis过程中学习到分布式锁时太多困惑和疑难杂点 需要总结梳理思路 以下思路都是最简单最基本的思路 主要用到Redission工具类 会涉及到看门狗机制等 本文内容部分引自Javaguide,小林coding等热门八股 用于个人学习用途
分布式锁介绍
对于单机多线程来说 通常用ReetrantLock类 synchronized关键字这类JDK自带的本地锁来控制一个JVM进程内多线程对本地共享资源的访问
图解引自JavaGuide:
线程访问共享资源是 互斥 的!也就是同一时刻只有单线程可获取本地锁访问共享资源
在分布式系统下 不同服务或者客户端通常运行在多个独立的JVM进程上 若多个JVM进程共享同份资源 使用本地锁就没办法实现资源的互斥访问了 为了解决这种问题 分布式锁 诞生了
举例子:下单服务一共部署 3 份 都对外提供服务 用户下订单之前后台要查库存 为了防止超卖 就需要加锁来实现对检查库存操作的同步访问 由于订单服务位于不同JVM进程中 本地锁在这种情况下就无法正常工作 因此要用到分布式锁 才能解决多线程不在同个JVM进程下也能获取同把锁 劲儿实现共享资源的互斥访问
图解引自JavaGuide:

可以看出来两者区别就在于 分布式锁就是联系起不同的JVM进程并保证所有JVM都可以对共享资源进行同步操作 基本的要求还是大体一致的 同样 独立进程里的线程访问共享资源是互斥的 某一时刻只有单线程可获取到分布式锁访问共享资源
分布式锁最基本要满足:
1.互斥:任何一个时刻 锁只能被一个线程持有(保证原子操作以免造成数据不一致问题)
2.高可用:锁的服务是高可用的 即使客户端释放锁的代码逻辑出现问题 锁最后还是会被释放 不影响其他线程中进程对共享资源的访问(一定可以释放锁 无论是异常还是出错 锁的效率很高)
3.可重入:一个节点获取了锁以后还可以再次获取锁(多次使用客户端一个功能 增删改)
实现分布式锁:
Redis 或 ZooKeeper实现分布式锁 以Redis为例
how to 实现?首先 从分布式锁的定义出发 无论是本地锁还是分布式锁 都有共性--“互斥”
在Redis里 setnx命令可以实现互斥 setnx(set if Not exist)对应Java里的setIfAbsent方法 如果key不存在 才会设置key的值 若key存在就什么都不做
> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
释放锁 用del删除key就行
> DEL lockKey
(integer) 1
但是这里有很多可能 在自学过程中 想到过若在当前线程已经获取锁的情况下
如果使用多个命令(如 setnx 和 expire设置过期时间),可能会出现:
1.成功获取锁 但是设置过期时间失败 导致锁无法正常释放
2.释放锁时 误删其他线程的锁(如锁已过期,但当前线程误删了新线程的锁)
为了解决特殊情况 需要保证数据前后操作一致性 也就是原子性 最常见的就是用lua脚本通过key对应的value(唯一值)来判断
lua脚本基于c语言 保证了解锁操作的原子性 因为Redis在执行lua脚本时可以用原子性的方式执行 从而保证锁释放操作的原子性
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
图解引自JavaGuide:

这样做有两个好处 用value判断是否为对应线程的锁 是则释放 否则error 防止误删其他锁
同时 用lua脚本保持原子操作 确保锁释放不会被其他线程打断
技术往往都是双刃剑 同样这种锁也有一些问题 要是程序遇到问题 比如说释放锁的逻辑突然挂掉可能导致锁无法正常释放 进而造成共享资源无法被其他进程或者线程访问 所以 为了避免这种问题 需要给锁设置一个过期时间
分布式锁设置过期时间
为了避免这种因外部原因或者突然断电异常 可以给这个key(这个分布式锁)设置一个过期时间
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
lockKey:加锁的锁名 uniqueValue 能够唯一标识锁的随机字符串(可以用UUID)
NX:只有当lockKey对应的key值不存在时才能set成功
EX:过期时间设置(秒为单位) EX 3 表示这个锁有三秒的自动过期时间 与EX对应的是PX(毫秒)两个都是过期时间设置
一定需要保证设置指定key的值和过期时间是原子操作!!!!!!否则锁无法被释放
但是我怎么知道要设置多长过期时间 我写完这个文章我难道要给自己设置个过期时间 到九点前我写完了 那我网页还挂着为啥不关机 九点之前没写完 我电脑直接关机了我怎么写
对于这种分布式锁也是一样的道理 一般正常的操作都是毫秒级别 过期时间小于线程对共享资源的操作时间 就回出现锁提前过期的问题 进而导致分布式锁直接失效 过期时间设置过长又会浪费性能
要是能自动设置就好了!
如何实现优雅的续期....?
总会有人想到的 这里就引入Redis里现成的方案 Redission
Redission自动续期机制
Redission就可以做到自动续期机制 其底层实现上 使用了看门狗机制(Watch Dog)
在共享资源的线程操作还未完成的情况下 看门狗会一直延长过期时间(默认情况下存活30s
每10s更新一次过期时间)进而保证锁不会因为超时而被释放
多好的看门狗 多希望自己学习也是这
图解引自JavaGuide:
看门狗名字的由来于 getLockWatchdogTimeout() 方法,此方法返回看门狗给锁续期的过期时间(默认30s)
//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
this.lockWatchdogTimeout = lockWatchdogTimeout;
return this;
}
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
方法内用lua脚本保证操作原子性 renewExpiration方法包含主要逻辑
private void renewExpiration() {
//......
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
// 异步续期,基于 Lua 脚本
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 无法续期
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 递归调用实现续期
renewExpiration();
} else {
// 取消续期
cancelExpirationRenewal(null);
}
});
}
// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
JavaGuide的源码分析很到位 我之前做过看门狗机制的笔记 比较凌乱 基本上是这样的流程
有ttl和time(ttl 锁有效时间 time获取锁的等待时间) 需要确保业务正常运行释放锁 不能因为阻塞异常释放锁 引出看门狗机制
这里的ee对象封装了当前线程id与当前定时的任务(该定时任务里用lua脚本对redis有效期定时更新 在看门狗机制30s的情况下 每30/3 = 10秒后会重启任务 通过内部递归重复调用方法执行)
Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
这里的renewExpirationAsync方法实现了续期 用lua脚本主要是保证操作原子性
JavaGuide里用Redission分布式 可重入锁RLock来说明如何使用Redission实现分布式锁
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);
注意 不设置锁过期时间时 才会触发看门狗自动续期机制
如何实现可重入锁?
什么是可重入锁?
单线程可以多次获取同一把锁 例如一个线程在执行一个带锁的方法 在方法中又调用了另一个同样需要相同锁的方法 则改成可以直接执行调用的方法 即可重入 相同线程多次进方法内的锁
Java中 synchronized(JVM内置监视器锁)与ReentrantLock(Lock实现类锁)都是可重入锁
不可重入的分布式锁基本可以满足绝大部分场景 特殊情况下还是要用可重入分布式锁
可重入分布式锁实现核心思路是 线程在获取锁时判断是否为自己的锁 如果是 就不用重新获取
所以 可以给每个锁关联一个可重入计数器 和 一个 占有他的线程(给线程上标识)当可重入计数器
大于0时 则锁被占有 需要判断占有该所的线程和请求获取锁的线程是否一致(是否有相同标识)
实际上 总有人替你负重前行 Redission内部内置了多种类型的锁比如可重入锁(ReentrantLock)自旋锁(SpinLock) 公平锁(Fair Lock)多重锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)
要是都用一个Redis服务 Redis爆炸了怎么办?所以一般都是集群服务 所以..
集群模式下如何实现分布式锁?如何保证分布式锁可靠性?
Redis集群数据同步到每个节点 会是异步还是同步? 一定是异步操作 同步太慢 那如果Redis主节点获取到锁后 在未能同步到其他节点时主服务宕机了.. 此时 新的Redis主节点还是能获取锁 变成新老大 多个应用服务还是可以同时获取锁
图解引自JavaGuide:

针对这种突然崩溃的情况 还是有人替我负重前行 用RedLock(红锁)来解决
RedLock算法思想 是 让客户端向Redis集群中多个独立的Redis实例依次请求申请加锁 若客户端能和多半数实例成功完成加锁操作 那么就宏观上认为:客户端成功获得分布式锁 否则加锁失败
即使部分Redis节点出现问题 只要保证Redis集群中有多半数Redis节点可用 分布式锁服务就ok
RedLock是直接操作Redis节点的 并不是通过Redis集群操作的 这样才可以避免Redis集群主从切换导致锁丢失问题
贴上锁优化时的问题

哨兵会让从节点变为主节点 但是原锁就会失效 可以在新主节点进新锁 会引发线程安全问题
以及从开始到现在我遇到的分布式锁的优缺点和优化过程 今天又看到RedLock


基本上到这里也是个人项目的内容
面试拷打:如何在项目中实现分布式锁?采用了什么方法?
Redission实现分布式锁实现具有以下特点:
- 支持可重入锁,同一个线程可以多次获取锁。
- 支持锁的自动续期,避免业务执行时间过长导致锁过期。
- 提供公平锁和非公平锁的选择。
- 支持锁的超时获取和尝试获取。
引入maven 略
创建redission客户端
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
// 初始化Redisson客户端(单例,实际项目建议注入Spring容器)
public class RedissonConfig {
public static RedissonClient getRedissonClient() {
Config config = new Config();
// 单机Redis配置(集群/哨兵可参考Redisson官方文档修改)
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379") // Redis地址
.setPassword("your_redis_password") // 如有密码请填写
.setDatabase(0); // 数据库编号
return Redisson.create(config);
}
}
可重入指同一线程可以重复获取同把锁 不会因为重复获取锁产生死锁
Redisson 的RLock默认就是可重入锁 需要用锁计数器 累计获取锁次数
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public class ReentrantLockDemo {
public static void main(String[] args) {
// 获取Redisson客户端
RedissonClient redissonClient = RedissonConfig.getRedissonClient();
// 定义锁名称(业务唯一标识,比如订单ID)
String lockKey = "order:lock:10086";
RLock lock = redissonClient.getLock(lockKey);
try {
// 第一次获取锁(加锁,过期时间30秒,Redisson默认开启Watch Dog续期)
lock.lock();
System.out.println("第一次获取锁成功,线程ID:" + Thread.currentThread().getId());
// 同一个线程再次获取同一把锁(可重入)
boolean reentrantLock = lock.tryLock();
System.out.println("第二次尝试获取锁结果:" + reentrantLock); // 输出true
// 执行业务逻辑(比如订单处理的核心方法+子方法)
handleOrderMain(lock);
} finally {
// 注意:加锁几次就要解锁几次,或直接用unlock()(Redisson内部会计数)
lock.unlock();
lock.unlock();
System.out.println("锁已释放");
}
}
// 订单处理主方法调用的子方法(需要再次获取锁)
private static void handleOrderMain(RLock lock) {
lock.lock();
System.out.println("子方法获取锁成功,线程ID:" + Thread.currentThread().getId());
// 模拟子方法业务逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
lock.unlock();
}
}
适用场景:电商订单处理、金融交易处理等包含 “主方法 + 子方法” 的业务流程。
- 举例:电商平台处理订单时,主线程调用
createOrder()(加锁),该方法内部又调用deductStock()、deductBalance()、generateLogistics()三个子方法,每个子方法都需要验证 “同一订单的锁” 以防止并发修改。(减库存 减余额 ) - 价值:如果锁不可重入,子方法尝试获取已持有的锁会导致死锁,可重入锁避免了这种问题,同时简化了代码逻辑(无需在子方法中判断 “是否已持有锁”)。
锁的自动续期(Watch Dog) + 场景使用
核心逻辑
Redisson 的lock()方法(无过期时间参数)会默认开启Watch Dog(看门狗)机制:锁的初始过期时间为 30 秒,每隔 10 秒自动续期(续到 30 秒),直到线程释放锁或线程崩溃。若手动指定lock(10, TimeUnit.SECONDS),则关闭 Watch Dog,不会自动续期。
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class AutoRenewLockDemo {
public static void main(String[] args) throws InterruptedException {
RedissonClient redissonClient = RedissonConfig.getRedissonClient();
String lockKey = "transfer:lock:9527"; // 转账业务锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 不指定过期时间,开启Watch Dog自动续期
lock.lock();
System.out.println("获取锁成功,开始执行大额转账业务");
// 模拟业务执行时间超过30秒(Redisson默认锁过期时间)
// 若没有自动续期,锁会在30秒后过期,其他线程会获取到锁,导致并发问题
Thread.sleep(40 * 1000);
System.out.println("大额转账业务执行完成,耗时40秒");
} finally {
lock.unlock();
System.out.println("锁已释放");
}
}
}
业务适用场景
执行时间不确定的长耗时业务,如金融大额转账、大数据批处理、物流轨迹同步。
- 举例:银行大额转账业务,需要调用央行清算接口、风控审核接口,这些接口响应时间不确定(可能 30 秒甚至更久)。
- 价值:如果锁设置固定过期时间(比如 30 秒),业务没执行完锁就过期,其他线程会获取到锁并重复处理转账,导致 “重复扣款”;Watch Dog 自动续期保证 “业务没执行完,锁就不会过期”,避免资金安全问题。
公平锁 vs 非公平锁 + 业务适用场景
核心逻辑
- 公平锁:按线程请求锁的顺序获取锁,先到先得,避免 “饥饿”(某个线程一直拿不到锁)。
- 非公平锁:不保证获取顺序,线程随机抢锁,性能更高(Redisson 默认是非公平锁)。
import org.redisson.api.RFairLock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
public class FairVsUnfairLockDemo {
public static void main(String[] args) {
RedissonClient redissonClient = RedissonConfig.getRedissonClient();
String lockKey = "queue:lock:1001"; // 排队叫号锁
// 1. 公平锁实现(医院挂号/银行叫号场景)
RFairLock fairLock = redissonClient.getFairLock(lockKey);
new Thread(() -> useFairLock(fairLock, "用户A")).start();
new Thread(() -> useFairLock(fairLock, "用户B")).start();
new Thread(() -> useFairLock(fairLock, "用户C")).start();
// 2. 非公平锁实现(电商秒杀场景,Redisson默认)
RLock unfairLock = redissonClient.getLock("seckill:lock:666");
new Thread(() -> useUnfairLock(unfairLock, "秒杀用户1")).start();
new Thread(() -> useUnfairLock(unfairLock, "秒杀用户2")).start();
new Thread(() -> useUnfairLock(unfairLock, "秒杀用户3")).start();
}
// 公平锁使用方法
private static void useFairLock(RFairLock fairLock, String userName) {
try {
fairLock.lock();
System.out.println(userName + " 公平锁获取成功,开始办理业务");
Thread.sleep(1000); // 模拟业务耗时
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
fairLock.unlock();
System.out.println(userName + " 公平锁释放");
}
}
// 非公平锁使用方法
private static void useUnfairLock(RLock unfairLock, String userName) {
try {
unfairLock.lock();
System.out.println(userName + " 非公平锁获取成功,开始秒杀");
Thread.sleep(500); // 秒杀业务耗时短
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unfairLock.unlock();
System.out.println(userName + " 非公平锁释放");
}
}
}
业务适用场景
| 锁类型 | 适用场景 | 业务价值 |
|---|---|---|
| 公平锁 | 医院挂号、银行叫号、排队购票 | 保证用户 “先到先得”,符合大众认知的公平性,提升用户体验,避免投诉。 |
| 非公平锁 | 电商秒杀、商品库存扣减、接口限流 | 放弃少量公平性换取更高的并发性能(无需维护等待队列),提升系统吞吐量。 |
锁的超时获取和尝试获取 + 业务适用场景
核心逻辑
tryLock(long waitTime, long leaseTime, TimeUnit unit):在waitTime时间内尝试获取锁,超时则返回 false;拿到锁后,leaseTime后自动释放(关闭 Watch Dog)。tryLock():立即尝试获取锁,拿到返回 true,拿不到返回 false(不阻塞)。
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class TimeoutTryLockDemo {
public static void main(String[] args) throws InterruptedException {
RedissonClient redissonClient = RedissonConfig.getRedissonClient();
String lockKey = "stock:lock:777"; // 商品库存锁
RLock lock = redissonClient.getLock(lockKey);
// 场景1:超时获取锁(用户下单扣库存)
boolean timeoutLock = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (timeoutLock) {
try {
System.out.println("5秒内获取到库存锁,开始扣减库存");
Thread.sleep(8000); // 模拟扣库存业务
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
System.out.println("库存锁释放");
}
} else {
System.out.println("等待5秒仍未获取到库存锁,提示用户:系统繁忙,请稍后再试");
}
// 场景2:立即尝试获取锁(非核心流程:缓存更新)
boolean immediateLock = lock.tryLock();
if (immediateLock) {
try {
System.out.println("立即获取到锁,开始更新商品缓存");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
System.out.println("缓存更新完成,释放锁");
}
} else {
System.out.println("未获取到锁,放弃缓存更新(不阻塞主流程)");
}
}
}
业务场景
| 获取方式 | 适用场景 | 业务价值 |
|---|---|---|
| 超时获取锁 | 用户下单、支付、预约服务 | 避免线程无限阻塞(比如库存锁被占用时,等待 5 秒后提示用户,而非一直卡着),提升系统响应性。 |
| 立即尝试获取锁 | 非核心流程(缓存更新、日志记录) | 不阻塞主业务流程(比如商品详情页的缓存更新,能更就更,不能更就放弃),保证核心功能的流畅性。 |
总结
- 可重入锁:解决 “同一线程多方法加锁” 的死锁问题,适用于包含主 / 子方法的业务(订单处理、转账)。
- 自动续期:保证长耗时业务(大额转账、批处理)的锁不会提前过期,避免并发安全问题。
- 公平 / 非公平锁:公平锁保证排队公平性(叫号、挂号),非公平锁追求高并发性能(秒杀、库存扣减)。
- 超时 / 尝试获取锁:避免线程无限阻塞,区分核心 / 非核心流程,提升系统响应性和用户体验。
参考资料
[1]Redisson: https://github.com/redisson/redisson
[2]redisson-3.17.6: https://github.com/redisson/redisson/releases/tag/redisson-3.17.6
[3]Redlock 算法: https://redis.io/topics/distlock
[4]How to do distributed locking - Martin Kleppmann - 2016: https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
[5]JavaGuide:原文链接:支付宝一面:如何基于Redis实现分布式锁?
更多推荐


所有评论(0)