目录
一 为什么需要分布式锁
在一些高并发的场景中,需要对共享变量进行互斥访问。也就是说在同一时间点有且只能有一个客户端能对共享资源进行访问。如电商系统中的秒杀系统等。
二 分布式锁实现的条件
为了保证分布式锁的可用性,我们要保证分布式锁的实现过程中必须满足以下四个条件:
1 互斥性:在任意时刻,有且只有一个客户端能持有锁。
2 死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生。
3 容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和释放锁。
4 安全性:加锁和释放锁必须是同一个客户端,客户端自己不能将别的客户端加的锁给释放了。
三 Redis 实现分布式锁
3.1 加锁
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second :设置键的过期时间为 second 秒。
- PX millisecond :设置键的过期时间为 millisecond 毫秒。
- NX :只在键不存在时,才对键进行设置操作。
- XX:只在键已经存在时,才对键进行设置操作。
- SET 操作成功完成时,返回 OK ,否则返回 nil。
伪代码实现:
public class DistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
@Resource
RedisCluster redisCluster;
public static boolean lock(String key, String requestId, int expireTime) {
String result = redisCluster.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
代码参数解释:
- key:与业务相关,具有唯一性。
- requestId:客户端通过 requestId 加锁,同时该客户端必须通过 requestId 释放锁。
- SET_IF_NOT_EXIST:即当 key 不存在时,我们进行 set 操作;若 key 已经存在,则不做任何操作。
- SET_WITH_EXPIRE_TIME:给这个 key 加一个过期时间的设置。
- expireTime:key 的过期时间。
3.2 释放锁
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
伪代码实现:
public class DistributedLock {
@Resource
RedisCluster redisCluster;
public static boolean unLock(String key, String requestId) {
String script = "if redisCluster.call('get', KEYS[1]) == ARGV[1] then return redis.redisCluster('del', KEYS[1]) else return 0 end";
Object result = redisCluster.eval(script, Collections.singletonList(key), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
那么为什么要使用 Lua 语言来实现呢?
因为要确保上述 get 和 del 操作是原子性的。如果调用 redisCluster.del()
方法的时候,如果这把锁已经不属于当前客户端的话,del 操作会释放其他客户端加的锁。
比如客户端 A 加锁,一段时间之后客户端A 释放锁,在执行 redisCluster.del()
之前,锁突然过期了,此时客户端 B 尝试加锁成功,然后客户端 A 再执行 del() 方法,则将客户端 B 的锁给释放了。
四 分布式锁续期实现原理
这块有一个异常情况,假设我们给锁设置的过期时间太短,业务还没执行完成,锁就过期了,这块应该如何处理呢?
如何给分布式锁续期?
redisson 是 Redis 的一个客户端,它能给 Redis 分布式锁实现过期时间自动续期。
redisson 如何实现过期时间自动续期?
redisson 加锁成功后,会单独创建一个线程来监视这个锁。假设我们锁的过期时间是3s,这个线程在1s的时候就会查看当前的锁是否释放,如果没有释放,将过期时间再次设置成3s。
这个相当于是一个定时任务,每隔过期时间的1/3就会来确认一次锁是否释放,没有释放就续期。这个是从源码查看的,这块就暂时不深究了,源码看这(拜托,面试请不要再问我Redis分布式锁的实现原理【石杉的架构笔记】)。
五 redisson 实现分布式锁
5.1 引入 pom 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
5.2 代码实现
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // cluster state scan interval in milliseconds
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();
/docs/reference/patterns/distributed-locks/
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers