redis有一个命令: set key value nx;
该命令的用处是,只有当redis中不含key时,才能set成功。
基于以上原理,可以设计分布式锁。
分布式锁可用于防止redis缓存击穿,也可解决幂等性问题。
分布式锁设计思路:
- 为防止在解锁前服务器突然宕机,导致死锁,redis分布式锁会设置一个过期时间。
// (1)相当于redis中的set lock value nx
redisTemplate.opsForValue().setIfAbsent("lock", "value");
// (2)设置过期时间
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
- 上述写法仍存在问题,即在(1)和(2)两个操作之间,redis宕机了,仍会造成死锁问题。因此加锁和设置过期时间得设计成原子性操作。
// 设置锁和过期时间操作是原子的
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "val", 300, TimeUnit.SECONDS);
- 我们的业务完成之后,应立马释放锁以防止阻塞其它进程。
// 设置锁和过期时间操作是原子的
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "val", 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
try {
// 执行业务代码
// ......
} finally {
//删除锁
redisTemplate.delete("lock");
}
}
- 上述流程又有一个问题,假如请求A业务超时,超过了设置的30s,这个时候锁过期了便会自动删除。这个时候请求B进来了,并加了分布式锁。然后请求A手动delete的操作就会把请求B加的锁给删了,导致误删。为了解决这个问题,可将分布式锁的
value
设置一个指定值,删的时候看看是不是自己的锁。
// 设置锁和过期时间操作是原子的,并指定uuid,以便删锁的时候判断是不是自己的。
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
try {
// 执行业务代码
// ......
} finally {
String lockValue = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
//删除我自己的锁
redisTemplate.delete("lock");//删除锁
}
}
- 上述操作就完美了吗?其实还是有问题。如果在
get("lock")
和delete("lock")
之间锁过期了,仍然会导致误删。因此get("lock")
和delete("lock")
也要一气呵成。可用lua
脚本实现。
// 设置锁和过期时间操作是原子的,并指定uuid,以便删锁的时候判断是不是自己的。
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
try {
// 执行业务代码
// ......
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class)
, Arrays.asList("lock"), uuid);
}