基于Redis缓存层实现分布式锁

常见分布式锁实现方案

分布式锁原则

1、相互排斥,即任一时刻,只能有一个客户端持有锁;

2、无死锁,持有锁的客户端宕机或网络延迟下仍可获取锁;

3、有始有终,一个客户端加了锁,只能自己释放锁,当然也不能被其他客户端解锁;

4、容错性,只要大部分redis节点还存活,那么客户端就应该可以正常加锁和释放锁;

一、加锁操作

<!--jedis 2.9.0-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
private static final String LOCKED_SUCCESS = "OK";
private static final String NX = "NX";
private static final String EXPIRE_TIME = "PX";

/**
* 获取锁
*
* @param jedis   redis客户端
* @param lockKey    锁的key
* @param uniqueId   请求标识
* @param expireTime 过期时间
* @return 是否获取锁
*/
public static boolean tryDistributedLock(Jedis jedis, String lockKey, String uniqueId, long expireTime) {
    String result = jedis.set(lockKey, uniqueId, NX, EXPIRE_TIME, expireTime);
    return LOCKED_SUCCESS.equals(result);
}

在低版本的redis中是没有这个set方法的,至于为什么这个简单的set方法能够保证前面提到的分布式锁原则呢?看下这个set的源码参数

/**
* Set the string value as value of the key. The string can't be longer than 1073741824 bytes (1
* GB).
* @param key  唯一标识key
* @param value   存储的值value
* @param nxxx  可选项:NX、XX  其中NX表示当key不存在时才set值,XX表示当key存在时才set值
* @param expx  过期时间单位,可选项:EX|PX 其中EX为seconds,PX为milliseconds
* @param time  过期时间,单位取上一个参数
* @return Status code reply
*/
public String set(final String key, final String value, final String nxxx, final String expx,
    final long time) {
  checkIsInMultiOrPipeline();
  client.set(key, value, nxxx, expx, time);
  return client.getStatusCodeReply();
}

如上的tryDistributedLock就可以实现简单的redis分布式锁了(此set方法的原子性)

1、set方法中nxx参数为NX,表示当key不存在时才会set值,保证了互斥性;

2、set值的同时设置过期时间(过期后del此key),客户端宕机或网络延迟时不会一直持有锁,避免了死锁发生;

3、set方法中的value,比如UUID之类的,用来表示当前请求客户端的唯一性标识;

4、因为是redis单例,暂时没有考虑容错性;

常见的错误分布式加锁实现

public static void getLock(Jedis jedis, String lockKey, String uniqueId, int expireTime) {
    //setnx方法表示当key不存在时才set值,存在不做任何操作 
    Long result = jedis.setnx(lockKey, uniqueId);
    //设置过期时间,但是如果此时服务器宕机,将无法释放锁(这两个setnx和expire并不具备原子性)
    if (result == 1) jedis.expire(lockKey, expireTime);
}

二、解锁操作

private static final Long RELEASE_SUCCESS = 1L;

/**
* 释放锁
*
* @param jedis     redis客户端
* @param lockKey   锁的key
* @param uniqueId 请求标识
* @return 是否释放
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String uniqueId) {
    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(lockKey), Collections.singletonList(uniqueId));
    return RELEASE_SUCCESS.equals(result);
}

不难看出使用Lua脚本告诉redis,如果这个key存在,且其存储的值和指定的value一致才可以删除这个key,从而释放锁,这样也保证了分布式锁的几个原则. 常见的错误释放锁会直接del这个key,没有考虑当前锁的拥有者,不符合分布式锁原则的有始有终原则;

如果不想用上面的Lua脚本,也可以用如下代码:

public static void releaseLock(Jedis jedis,String lockKey,String uniqueId){
      if(uniqueId.equals(jedis.get(lockKey))){
          jedis.del(lockKey);
      }
}

如上代码存在这样的场景: Client A去加锁lockKey,然后释放锁,在执行del(lockKey)之前,这时lockKey锁expire到期失效了,此时Client B尝试加锁lockKey成功,Client A接着执行释放锁操作(del),便释放了Client B的锁.

补充:

之前存在一点误解,就是认为当redis也高可用时,这个锁还能不能用,答案是可以的,因为一般而言,你的应用服务和redis服务是解耦的,如果你的redis采用了高可用,比如一主多从甚至多主多从,应用服务只会和一个主redis服务交互,这样基于Redis缓存层实现分布式锁到就比较完美了!

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页