分布式锁一般有三种实现方式:
数据库乐观锁
基于ZooKeeper的分布式锁
基于Redis的分布式锁
这里主要记录下基于Redis的分布式锁
Redis加锁
springboot2.1以后的版本可以直接使用redisTemplate提供的setIfAbsent方法进行加锁 相当于使用redis命令:SET key value [EX seconds] [PX millisecounds] [NX|XX]
redisTemplate.opsForValue().setIfAbsent(key,value,time,TimeUnit)
为什么是set命令而不是setNx命令?
因为setNx 无法设置key过期时间 需要通过expire来为key设置过期时间,意味着加锁是两条命令,不满足原子性。
锁的过期时间设置多少合适,是否可以不设置?
锁的过期时间一定是要有的,不然留着过年么?过期时间根据具体的业务逻辑来设置,但是一定要大于代码执行的时间。例如:
//加锁 锁的过期时间为5秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent("111", "11", 5, TimeUnit.SECONDS);
if(lock){
System.out.println("业务逻辑执行在0-8秒范围内");
}
这个时候肯定是不合适的。
Redis解锁
加锁之后,一定要保证锁的释放,所以通常是在finally代码块里面释放锁。
获取到锁才释放锁,没有获取到,不要去释放锁,避免释放其他客户端加的锁。
释放锁的时候可以判断锁的持有者是否是自己,是自己的才进行释放。(2和3至少要遵循一个,这样才能避免误释放锁)
解锁方式一:
redisTemplate.delete(key)
解锁方式二:使用lua脚本
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
完整的加锁解锁代码如下:
@Autowired
RedisTemplate redisTemplate;
@Test
public void testLock() throws UnknownHostException {
String key = "lockTest";
InetAddress ia = InetAddress.getLocalHost();
String value = ia.toString();
//加锁 锁的过期时间为20秒
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, value, 20, TimeUnit.SECONDS);
if (!lock) {
//未获取到锁,直接返回
return;
}
try {
System.out.println("业务逻辑执行在小于20秒范围内");
} catch (Exception e) {
System.out.println("业务错误信息");
} finally {
// 方式一 直接使用del
redisTemplate.delete(key);
// 方式二 使用lua脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript redisScript = new DefaultRedisScript(script);
List keys = new ArrayList<>();
keys.add(key);
redisTemplate.execute(redisScript, keys, value);
}
}