今天同事发现自己写的一个redis分布式锁未生效,一个task在3台机上跑了(总共部署了4台机器),这个分布式锁的伪代码大致如下:
try {
if (!redisClient.setnxpx("lock", "1", expireTime)) {
return;
}
//下面是业务代码
} catch (Exception e) {
log.error("xxxx", e);
} finally {
redisClient.del("lock");
}
review了代码后,发现这个锁的实现存在漏洞。当一台机器执行完task,删除redis里的key值后,其他机器才开始尝试setNxPx,便会出现重复执行的情况。
了解原因后,我便向他推荐改用组内封装的redis锁,组内封装redis锁的实现的伪代码如下:
String key = "lock" + Date.format("yyyy-MM-dd HH:mm:ss");
try {
if (!redisClient.setnxpx(key, "1", expireTime)) {
return;
}
//下面是业务代
} catch (Exception e) {
log.error("xxxx", e);
}
与上面那个实现相比,这个锁的实现主要有两点区别:
- redis中的key值不是一个常量,而是根据task执行的时间来生成(这里通常用到精细到秒的task,所以格式化为yyyy-MM-dd HH:mm:ss的格式增加可读性)
- 任务执行完不删除锁,由redis的过期机制淘汰无用的锁
即使不同机器间时间误差较大,存在一台机执行完了task,其他机器才开始尝试获取锁的场景,由于任务计划执行的时间一致,生成的redis key值也会一致,其他机器同样无法获得锁,从而保证task只会在一台机器执行。
这个实现还有一个需要注意的点,就是锁过期时间需要设置合理(比一次任务周期长)。并且有一个缺点,如果一台机器的task执行超时(超过了一次task执行的周期),其他机器还是有机会抢到锁执行task,这样就有可能存在多台机器(也可能是同一台机器)同时在执行属于不同周期的task,这种情况需要在业务逻辑上进行容错。
以上说的是分布式任务执行的锁实现,更多场景下的分布式锁参考文章:
importNew-《Redis 分布式锁的正确实现方式( Java 版 )》
RedLock算法-使用redis实现分布式锁服务