1.前言
目前系统架构是分布式,因为场景要保证数据一致性,且项目采用的是redis作为缓存,故采用reids实现分布式锁
2.实现方案
采用 redis中的 setNx + expire 实现分布式锁
setNx(key, value): 如果key存在,则返回0. 否则返回1
expire: 过期时间
3.代码实现(Demo)
/**
* @Description 是否获取到锁
* @param:
* @param jedis
* @param lockKey 键
* @param value 值
* @param expireTime 过期时间
* @return boolean
* @author Cc
* @date 2020/2/6 11:26
*/
public static boolean lock(Jedis jedis, String lockKey, String value, int expireTime) {
//key存在的情况下,不操作redis内存,也就是返回值是0。否则返回1
Long result = jedis.setnx(lockKey, value);
//设置锁
if (result == 1) {
//获取锁成功
//若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
//通过过期时间删除锁
jedis.expire(lockKey, expireTime);
return true;
}
return false;
}
分析
- 因为 setnx + expire 不是原子操作,线程不安全
- 如果程序设置锁之后,因为程序突然崩溃,则无法设置过期时间,将发送死锁。故有以下2种解决方案
4.解决死锁
4.1 采用 lua 脚本
// 加锁脚本,KEYS[1] 要加锁的key,ARGV[1]是UUID随机值,ARGV[2]是过期时间
private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
// 解锁脚本,KEYS[1]要解锁的key,ARGV[1]是UUID随机值
private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
4.2 采用set命令
要求redis版本 2.6 版本之后才可以用这个命令
public static boolean lock(Jedis jedis, String lockKey, String value, int expireTime) {
String result = jedis.set(lockKey, value, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
}
产生的问题:
虽然 SETNX()方式能够保证设置锁和过期时间的原子性,但是如果设置的过期时间比较短,而执行业务时间比较长,就会存在锁代码块失效的问题,导致其他客户端也能获取到同样的锁,执行同样的业务,此时可能会出现一些问题。
解决方案建议:
可以将key的过期时间设置长一点,上诉问题就不出出现,但是设置多长时间,具体还需要根据业务场景权衡
书里面学到的,总结以下,旨在提供思路。