背景
分布式锁在业务场景中的应用十分广泛,当你的服务部署在分布式的多节点集群中,在执行某一个方法时,需要控制并发的情况,保证同一时间只有一个请求可以执行,你一定需要分布式锁来实现。
最常见的分布式锁,一般就是Redis和ZK(亦或者MySQL的for update也可以充当),当然也有很多其他的实现,本篇的主角,就是Redis,我们来逐步使用实现一个简单易用的Redis分布式锁。
RedisLock
首先,我们先来回忆一下Java的Lock是怎么玩的:
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// do something...
} finally {
lock.unlock();
}
很简单,很优雅,那么我们就仿照Java的API来实现我们的Redis Lock。
最直观的实现方式,就是使用Redis String的setnx方法,如果key存在就放弃,如果key不存在就set一个值,并指定过期时间,释放锁时,就把key删除,非常easy。
Version1:
@Component
public class RedisLockUtil {
@Autowired
private JedisClient jedisClient;
public boolean lock(String key, long lockTimeout) {
key = key + ".lock";
UUID uuid = UUID.randomUUID();
String res = jedisClient.set(key, uuid.toString(), "nx", "px", lockTimeout);
if (Objects.equals("OK", res)) {
return true;
}
return false;
}
public boolean unlock(String key) {
key = key + ".lock";
Long res = jedisClient.del(key);
return Objects.equals(res, 1L);
}
}
Version1的实现非常简单,也可以满足基本的需求,但是有一个问题,任何线程,只要知道lockkey的名称,都可以调用unlock方法,这显然不是我们所希望的,所以我们在此基础上,进行一点优化,只有加锁的人,才可以释放锁,因此,我们需要保存一些额外的信息。
Version2:
我们新建一个Bean,存储RedisLock的相关信息,改造后我们的实现如下:
public class RedisLock {
private String key;
private final UUID uuid;
private long lockTimeout;
private long startLockTimeMillis;
private long getLockTimeMillis;
public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis) {
this.key = key;
this.uuid = uuid;
this.lockTimeout = lockTimeout;
this.startLockTimeMillis = startLockTimeMillis;
this.getLockTimeMillis = getLockTimeMillis;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public UUID getUuid() {
return uuid;
}
public long getLockTimeout() {
return lockTimeout;
}
public void setLockTimeout(long lockTimeout) {
this.lockTimeout = lockTimeout;
}
public long getGetLockTimeMillis() {
return getLockTimeMillis;
}
public void setGetLockTimeMillis(long getLockTimeMillis) {
this.getLockTimeMillis = getLockTimeMillis;
}
public long getStartLockTimeMillis() {
return startLockTimeMillis;
}
public void setStartLockTimeMillis(long startLockTimeMillis) {
this.startLockTimeMillis = startLockTimeMillis;
}
}
@Component
public class RedisLockUtil {
@Autowired
private JedisClient jedisClient;
// 释放锁的Lua脚本
private static final String LUA_SCRIPT_UNLOCK =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" redis.call('del', KEYS[1]); " +
" return 'suc' " +
"else " +
" return 'fail' " +
"end";
public RedisLock lock(String key, long lockTimeout) {
key = key + ".lock";
UUID uuid = UUID.randomUUID();
String res = jedisClient.set(key, uuid.toString(), "nx", "px", lockTimeout);
if (Objects.equals("OK", res)) {
return new RedisLock(key, uuid, lockTimeout, System.currentTimeMillis());
}
return null;
}
public boolean unlock(RedisLock lock) {
if (lock == null) {
return false;
}
try {
String lockValue = lock.getUuid().toString();
JedisClusterPipeline pipeline = jedisClient.getPipelined();
Response<String> response = pipeline.eval(LUA_SCRIPT_UNLOCK,
Collections.singletonList(lock.getKey()),
Collections.singletonList(lockValue)
);
pipeline.sync();
return response != null && Objects.equals(response.get(), "suc");
} catch (Exception e) {
return false;
}
}
}
Well,改造后的方法,好像比Version1复杂了一些,别着急,其实并不复杂,为了保证原子性,删除lockkey部分我们采用了Lua脚本的方式实现,只有当lockkey的value校验一致时,才可以对key进行删除,这样就解决了Version1中出现的问题。
Version2基本可以运行的很好了,但是还有一个问题,我们知道Java的synchronized和ReentrantLock的实现,都是带有自旋抢锁功能的,当第一次没有获取到锁后,会尝试自旋等待,再次尝试抢锁,而我们的实现,好像并不具备这个功能,这怎么可以呢?那我们也要加入自旋。
Version3:
在RedisLock Bean中加入一些额外的信息:
public class RedisLock {
private String key;
private final UUID uuid;
private long lockTimeout;
private long startLockTimeMillis;
private long getLockTimeMillis;
private int tryCount;
public RedisLock(String key, UUID uuid, long lockTimeout, long startLockTimeMillis, long getLockTimeMillis, int tryCount) {
this.key = key;
this.uuid = uuid;
this.lockTimeout = lockTimeout;
this.startLockTimeMillis = startLockTimeMillis;
this.getLockTimeMillis = getLockTimeMillis;
this.tryCount = tryCount;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public UUID getUuid() {
return uuid;
}
public long getLockTimeout() {
return lockTimeout;
}
public void setLockTimeout(long lockTimeout) {
this.lockTimeout = lockTimeout;
}
public long getGetLockTimeMillis() {
return getLockTimeMillis;
}
public void setGetLockTimeMillis(long getLockTimeMillis) {
this.getLockTimeMillis = getLockTimeMillis;
}
public long getStartLockTimeMillis() {
return startLockTimeMillis;
}
public void setStartLockTimeMillis(long startLockTimeMillis) {
this.startLockTimeMillis = startLockTimeMillis;
}
public int getTryCount() {
return tryCount;
}
public void setTryCount(int tryCount) {
this.tryCount = tryCount;
}
}
@Component
public class RedisLockUtil {
private static final Logger logger = LoggerFactory.getLogger(RedisDemo.class)<