在完成JedisCluster的所需配置之后,可以看看分布式锁的如何实现的
所有代码如下所示。
* JedisCluster + lua脚本实现分布式锁
* @author zhoujy
* @date 2018年12月19日
public class RedisDistributeLock {
private Logger logger = LoggerFactory.getLogger(RedisDistributeLock.class);
private JedisCluster jedisCluster;
* lua脚本:判断锁住值是否为当前线程持有,是的话解锁,不是的话解锁失败
private static final String DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL = "if" +
" redis.call('get', KEYS[1]) == ARGV[1]" +
" then" +
" return redis.call('del', KEYS[1])" +
" else" +
" return 0" +
" end";
private volatile String unlockSha1 = "";
private static final Long UNLOCK_SUCCESS_CODE = 1L;
private static final String LOCK_SUCCESS_CODE = "ok";
public RedisDistributeLock(JedisCluster jedisCluster) {
this.jedisCluster = jedisCluster;
* 根据loopTryTime循环重试
* @param lockKey 锁key
* @param lockVal 锁值,用于解锁校验
* @param expiryTime 锁过期时间
* @param loopTryTime 获取失败时,循环重试获取锁的时长
* @return 是否获得锁
public boolean tryLock(String lockKey, String lockVal, long expiryTime, long loopTryTime){
Long endTime = System.currentTimeMillis() + loopTryTime;
while (System.currentTimeMillis() < endTime){
if (tryLock(lockKey, lockVal, expiryTime)){
return true;
return false;
* 根据loopTryTime循环重试
* @param lockKey 锁key
* @param lockVal 锁值,用于解锁校验
* @param expiryTime 锁过期时间
* @param retryTimes 重试次数
* @param setpTime 每次重试间隔 mills
* @return 是否获得锁
public boolean tryLock(String lockKey, String lockVal, long expiryTime, int retryTimes, long setpTime){
while (retryTimes > 0){
if (tryLock(lockKey, lockVal, expiryTime)){
return true;
retryTimes--;
try {
Thread.sleep(setpTime);
} catch (InterruptedException e) {
logger.error("get distribute lock error" +e.getLocalizedMessage());
return false;
* 一次尝试,快速失败。不支持重入
* @param lockKey 锁key
* @param lockVal 锁值,用于解锁校验
* @param expiryTime 锁过期时间 MILLS
* @return 是否获得锁
public boolean tryLock(String lockKey, String lockVal, long expiryTime){
String result = jedisCluster.set(lockKey, lockVal, "NX", "PX", expiryTime);
return LOCK_SUCCESS_CODE.equalsIgnoreCase(result);
* 释放分布式锁,释放失败最可能是业务执行时间长于lockKey过期时间,应当结合业务场景调整过期时间
* @param lockKey 锁key
* @param lockVal 锁值
* @return 是否释放成功
public boolean tryUnLock(String lockKey, String lockVal){
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> argv = new ArrayList<>();
argv.add(lockVal);
try {
Object result = jedisCluster.evalsha(unlockSha1, keys, argv);
return UNLOCK_SUCCESS_CODE.equals(result);
}catch (JedisNoScriptException e){
logger.info("try to store script......");
storeScript(lockKey);
Object result = jedisCluster.evalsha(unlockSha1, keys, argv);
return UNLOCK_SUCCESS_CODE.equals(result);
}catch (Exception e){
e.printStackTrace();
return false;
* 由于使用redis集群,因此每个节点都需要各自缓存一份脚本数据
* @param slotKey 用来定位对应的slot的slotKey
public void storeScript(String slotKey){
if (StringUtils.isEmpty(unlockSha1) || !jedisCluster.scriptExists(unlockSha1, slotKey)){
unlockSha1 = jedisCluster.scriptLoad(DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL, slotKey);
针对上面的代码,逐步分析。
相比一般的redis分布式锁,这里操作jedis的操作方式进行加锁,好处就是Jedis保证set与设置有效期两个操作之间的原子性,避免在set值之后,程序宕机,导致没有设置过期时间,锁就一直被锁住。
这一步操作我们单独使用lua脚本实现也可以,但是幸好jedis已经帮我们进行实现。
* 一次尝试,快速失败。不支持重入
* @param lockKey 锁key
* @param lockVal 锁值,用于解锁校验
* @param expiryTime 锁过期时间 MILLS
* @return 是否获得锁
public boolean tryLock(String lockKey, String lockVal, long expiryTime){
String result = jedisCluster.set(lockKey, lockVal, "NX", "PX", expiryTime);
return LOCK_SUCCESS_CODE.equalsIgnoreCase(result);
同时加锁操作也有几个简单的重载实现,分别是重试获取和循环获取锁的重载,根据业务场景适当调整使用。
这里的分布式锁的解锁操作使用lua脚本帮助实现。
我们都知道,分布式锁在解锁时一定需要验证是不是锁的持有者,这种情况下,我们需要进行的操作就有获取key的对应value,然后验证value的值,这个过程,存在一种情况,导致误删别的持有者的锁。分析如下的操作顺序图
上面的操作顺序可能出错的情况就是当lock1尝试释放时,先获取值,判断是否是锁的持有者,如果是,就再发指令删除锁。这个过程可能存在问题就是,lock1在获取值之后,刚好到了有效期了,那么锁可能会在此时被锁竞争者2获得,并且设置锁lock2,然而这时锁竞争者1删除锁的指令刚好重新发送到redis-server,就会误删lock2,导致后续会被其他锁竞争者3获取,发送不可知业务错误。
使用lua脚本的好处就是保证redis指令之间执行的原子性,把get和del执行放在脚本中,保证不会误删别的锁竞争者的锁,假如刚好出现get之后锁值过期,最多就是del操作结果为0,不会出现误删结果。
* 释放分布式锁,释放失败最可能是业务执行时间长于lockKey过期时间,应当结合业务场景调整过期时间
* @param lockKey 锁key
* @param lockVal 锁值
* @return 是否释放成功
public boolean tryUnLock(String lockKey, String lockVal){
List<String> keys = new ArrayList<>();
keys.add(lockKey);
List<String> argv = new ArrayList<>();
argv.add(lockVal);
try {
Object result = jedisCluster.evalsha(unlockSha1, keys, argv);
return UNLOCK_SUCCESS_CODE.equals(result);
}catch (JedisNoScriptException e){
logger.info("try to store script......");
storeScript(lockKey);
Object result = jedisCluster.evalsha(unlockSha1, keys, argv);
return UNLOCK_SUCCESS_CODE.equals(result);
}catch (Exception e){
e.printStackTrace();
return false;
* lua脚本:判断锁住值是否为当前线程持有,是的话解锁,不是的话解锁失败
private static final String DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL = "if" +
" redis.call('get', KEYS[1]) == ARGV[1]" +
" then" +
" return redis.call('del', KEYS[1])" +
" else" +
" return 0" +
" end";