直接说重点:
setnx命令,key自然是锁名。
- value是
requestId
,即请求标识,能够区分不同的客户端,保证锁只能由加锁的客户端释放 - expireTime 直接作为setnx的参数,而不是单独的设置过期时间,保证原子性
- 释放锁时,判断value是否相等,然后再释放锁。但**关键是用lua脚本,保证原子性**
因为文章里都讲得很好很清楚,我就不赘述了。
感叹一下在实际做项目前,准备面试点,只理解到setnx一同设置过期时间,还觉得这个问题很简单…实践才知道这么多坑。
拓展:集群时分布式锁的问题
客户A在主节点上拿到了锁,然后主节点挂了(此时锁的数据还没同步到从节点),而从节点成为新的主节点,随后客户B获取到了锁。
这样就造成了两个客户同时拥有了锁,而且客户A无法感知主节点挂了。
解决方法:
Redlock
算法,具体自行百度
附源码:
/**
*
* @param prefix
* @param key
* @param value 客户端的唯一标识,保证:只有加锁的客户端可以解锁
* @param expireTime 过期时间,单位:毫秒 (与PX、EX有关)
* @param lockWaitTime 等待锁的时间。单位:毫秒
* @return
*/
public boolean lock(KeyPrefix prefix, String key, String value,
Long expireTime, Long lockWaitTime) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String realKey = prefix.getPrefix() + key;
Long deadTimeLine = System.currentTimeMillis() + lockWaitTime;
// 关键是自旋,用的就是setnx而已。
for (; ; ) {
// PX:key过期时间单位,PX:毫秒,EX:秒
String result = jedis.set(realKey, value, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
lockWaitTime = deadTimeLine - System.currentTimeMillis();
// 等待锁的限定时间已过,仍未获取锁,返回false
if (lockWaitTime <= 0L) {
// 这里break也可以
return false;
}
}
} catch (Exception ex) {
System.out.println("lock error");
} finally {
returnToPool(jedis);
}
return false;
}
// 关键: 用Lua脚本保证 : 判断value相等 + 删除key ,这两步的原子性
// 防止出现: A判断相等 -> 而删除前,锁过期。B检测到锁过期,获取了锁 -> A删除锁
// 这种情况下,A删除的就是B的锁,而不是自己的锁(已过期)
public boolean unlock(KeyPrefix prefix, String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String realKey = prefix.getPrefix() + key;
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(luaScript, Collections.singletonList(realKey),
Collections.singletonList(value));
if ("1".equals(result)) {
return true;
}
} catch (Exception ex) {
System.out.println("unlock error");
} finally {
returnToPool(jedis);
}
return false;
}
// 释放资源
private void returnToPool(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
KeyPrefix 是项目中的接口,因为直接用锁名(string) 很容易就会重复。因此抽象出前缀,配合类名,可以保证一致性。不需要的话,去掉直接用key即可,无区别。
附KeyPrefix的相关代码:
public interface KeyPrefix {
public int expireSeconds();
public String getPrefix();
}
public abstract class BasePrefix implements KeyPrefix{
/***
* @Description: 单位:秒s ,0代表永不过期
* @Author: hermanCho
* @Date: 2020-08-15
* @Param null:
* @return: null
**/
private int expireSeconds;
private String prefix;
public BasePrefix(String prefix) {//0代表永不过期
this(0, prefix);
}
public BasePrefix( int expireSeconds, String prefix) {
this.expireSeconds = expireSeconds;
this.prefix = prefix;
}
public int expireSeconds() {
return expireSeconds;
}
// 通过 className + prefix ,作为key的前缀,可以保证不重复
public String getPrefix() {
String className = getClass().getSimpleName();
return className+":" + prefix;
}
public String getSimPrefix(){
return prefix;
}
}