关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
在高并发分布式场景中,分布式锁是我们经常使用的工具。你的Redis
分布式锁是在使用set
还是setnx
实现的呢?是否还在裸奔?
今天我们一起看看Redis
分布式锁是如何裸奔的?可能会出现什么样的问题,以及如何处理。
02 裸奔的分布式锁
在高并发或者幂等性设计的时候,我们都需要使用到分布式锁。最常见的就是防止表单重复提交的场景,我们来看看裸奔的实现。
java
try {
if(redis.setnx(key, objFormat) == 0){
return new JsonResult(false, "不可重复提交!"));
}
// 设置过期时间为2s
redis.setExpireSec(key, objFormat, EXPIRE_TIME_2S);
// 其他业务逻辑......
}finally {
redis.delete(key);
}
乍一看好像也没有什么问题,不瞒你说,我们项目里面也是这么用的!还没有出现过问题呢,哈哈哈...
2.1 裸奔式分析
setnx()
实在key
不存在时可以设置进去并返回1,key
存在时,则表示没有设置进去,返回0。当key
设置成功之后,通过setExpireSec()
设置过期时间。随后继续执行其他业务,最后删除Redis
中的key
。
细扣里面的细节,包含这几个问题:
- 过期时间:过期时间的设置应该是多长
- 原子操作:设置值和过期时间是非原子操作
- 暴力删除:删除key过于暴力
过期时间
在高并发或者幂等性操作时,Redis
的key
一般都是临时的,请求完成之后基本就没有用了。为了防止Redis
中存放大量无效的Key
,我们需要对无效的key
进行清理。而有效时间,会被Redis
记录,到期自动清理。
但是过期时间的时长无法精确估计,设置短无法拦住多余的请求,设置的长有可能拦截住正常的请求。再加上网络抖动等硬件影响,时间更家无法确定。
原子操作
原子操作可以保证要么都成功要么都失败,设置值和时间非原子操作,可能会造成设置值成功了,但是设置过期时间的时候出现了异常,导致设置的Key
没有过期时间。
try...finally
可以弥补这一缺陷,但是如果出现系统的异常或者服务器宕机了,这种兜底就无法生效了。所以说还是存在瑕疵的。
暴力删除
暴力删除指的是,没有判断当前的可以是不是你自己的key
,就直接删除了Key
。我们假设这样一个场景:
- 第一个请求进来执行了自己的业务后卡住了,导致自己的
key
已经过期了,但是还没有执行finally
方法。 - 第二个请求进来拥有相同的
key
,在设置到Redis
之后业务还没有执行完,第一个请求突然执行了finally
中删除了key
的方法,这个时候就会删除了第二个请求的锁,导致第二个请求失去了锁。
所以我们在删除key
之前,增加value
的判断,如果相同就表示是自己的key,可以放心删除,如果不是就不能删除。
2.2 改进版
过期时长我们依然无法解决,暂时先不解决,先填平其他两个坑点。
java
try {
if(!"OK".equals(redis.set(key, objFormat, "NX", "EX", EXPIRE_TIME_2S))){
return new JsonResult(false, "不可重复提交!"));
}
// 其他业务逻辑......
}finally {
if (objFormat.equals(redis.get(key)) {
redis.delete(key);
}
// Lua脚本保证原子性删除
/*
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(luaScript, 1, key, objFormat);
*/
}
Redis
的set
命令自带的 nx
(如果不存在就创建)和 ex
(设置超时时间),将原来的setnx
和expire
命令合成一个了,并且是原子操作,极大的方面了我们对分布式锁的使用。当然我们还可以使用Redis
支持的Lua
脚本来实现原子操作。
03 看门狗
第二节介绍的方式无论初始版还是改进版,通过设置较长的过期时间,已经可以应对分布式下的并发问题、幂等性问题。用在生产环境毫无毛病,至少现在我们还没有遇到问题。
如何精确的解决过期时间的问题,看门狗机制正事通过监控机制,为较短的过期时间续约,已达到精确控制过期时间的目的。
实现思路:通过守护线程每隔一段时间就会去检查Redis
的key
是否快要过期了,如果是就会延长过期时间。
3.1 自定义实现
启用看门狗模式
java
try {
if(!"OK".equals(redis.set(key, objFormat, "NX", "EX", EXPIRE_TIME_2S))){
return new JsonResult(false, "不可重复提交!"));
}
/** 启动看门狗模式 */
startWatchDog(key, objFormat);
// 其他业务逻辑......
}finally {
// 停止看门狗
stopWatchDog(key, objFormat);
// Lua脚本保证原子性删除
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(luaScript, 1, key, objFormat);
}
看门狗实现与停止
java
public void startWatchDog(String lockKey, String lockValue) {
// 创建调度线程池
scheduler = Executors.newSingleThreadScheduledExecutor();
// 以固定的频率调度任务
watchdogTask = scheduler.scheduleAtFixedRate(() -> {
// 使用Lua脚本确保原子性校验和续期
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('pexpire', KEYS[1], ARGV[2]) " +
"else return 0 end";
if (jedis.eval(luaScript, 1, key, objFormat) == 1) {
System.out.println("锁续期成功");
}else {
// 锁续约失败,关闭看门狗
stopWatchDog(lockKey, lockValue)
}
}, WATCHDOG_INTERVAL, WATCHDOG_INTERVAL, TimeUnit.MILLISECONDS);
}
public void stopWatchDog(String lockKey, String lockValue) {
if (watchdogTask != null) {
watchdogTask.cancel(true);
}
if (scheduler != null){
scheduler.shutdown();
}
}
3.2 基于Redisson的看门狗
看门狗是有重复的轮子可用的,Redisson
作为Redis
的客户端已经实现了这一功能,可以放心使用在生产环境上。我们无需关心续约问题,Redisson
已经帮我们实现了,我们只要像用锁一样使用就可以了。
Maven依赖
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.0</version>
</dependency>
具体实现
java
@Test
void testWatchDog() {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("resource_lock");
try {
// 获取锁并自动启动看门狗
lock.lock(); // 默认30秒过期,看门狗每10秒续期
// 执行长时业务逻辑
Thread.sleep(60000); // 模拟60秒操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁并停止看门狗
redisson.shutdown();
}
}
04 关键的实现细节
- 续期条件验证:每次续期前校验锁的value是否匹配,防止续期他人持有的锁
- 优雅停止机制:业务完成时立即取消定时任务,避免空转
- 原子性操作:使用Lua脚本保证「校验+删除」的原子性
- 容错处理:网络异常时自动重试(
Redisson
内置重试机制)
05 小结
在分布式锁场景中,当业务执行时间超过锁的过期时间时,Redis
看门狗通过自动续期机制防止锁被意外释放,避免多个客户端同时持有锁导致数据不一致。
生产环境优先选用Redisson
框架,其经过大规模验证,提供完善的分布式锁实现和看门狗机制,同时支持Redis
集群模式下的高可用场景。