Redis_分布式唯一锁

说到分布式,就必然涉及到分布式锁的概念,如何保证不同机器不同线程的分布式锁同步呢。

实现分布式锁的要点:

  1. 互斥性:同一时刻,只能有一个客户端持有锁。
  2. 防止死锁发生:如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。
  3. 加锁和释放锁必须是同一个客户端。
  4. 容错性:只有redis还有节点存活,就可以进行正常的加锁解锁操作。

Redis是线程安全的,单线程操作的。

1、保持互斥性:

jedis.setnx(); 将 key 的值设为 value ,当且仅当 key 不存在。

                       若给定的 key 已经存在,则 SETNX 不做任何动作。

  public Long setnx(final String key, final String value) {
    checkIsInMultiOrPipeline();
    client.setnx(key, value);
    return client.getIntegerReply();
  }

2、防止死锁,则为资源设置一个失效时间;

jedis.expire();用来设置超时时间;

  public Long expire(final String key, final int seconds) {
    checkIsInMultiOrPipeline();
    client.expire(key, seconds);
    return client.getIntegerReply();
  }

错误的加锁方式一:

在多线程并发环境下,任何非原子性的操作,都可能导致问题。
这段代码中,如果设置过期时间前,redis实例崩溃,就无法设置过期时间。如果客户端没有正确的释放锁,那么该锁(永远不会过期),就永远不会被释放。

public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
        Long result = jedis.setnx(key, uniqueId);
        if (1 == result) {
            //如果该redis实例崩溃,那就无法设置过期时间了
            jedis.expire(key, expireTime);
        }
    }

错误加锁方式二:

实现方式,在value中传入失效时间。存在如下问题:

  1. value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。
  2. 锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功。但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。
    jedis.getSet 方法 用于设置指定 key 的值,并返回 key 的旧值。
 public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
        long expireTs = System.currentTimeMillis() + expireTime;
        // 锁不存在,当前线程加锁成果
        if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
            return true;
        }

// 如果所已经存在了
// 获取当前所得value
// 如果value存在,且实现时间已经小于了当前时间
// 通过getSet方法,获取设置key值,并且为key设置新的失效时间
// 返回是否设置成功

        String value = jedis.get(key);
        //如果当前锁存在,且锁已过期
        if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
            //锁过期,设置新的过期时间
            String oldValue = jedis.getSet(key, String.valueOf(expireTs));
            if (oldValue != null && oldValue.equals(value)) {
                // 多线程并发下,只有一个线程会设置成功
                // 设置成功的这个线程,key的旧值一定和设置之前的key的值一致
                return true;
            }
        }
        // 其他情况,加锁失败
        return true;
    }

错误的释放锁的方式:

释放锁:jedis.del(key); 调用该方法直接解锁,但不是自己加锁的,也会被删除。


正确的加解锁,应该避免以上问题的出现。

加锁 直接使用set命令同时设置唯一id和过期时间;

解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。

@Component
public class RedisUtils {
    /**
	 * Only set the key if it does not already exist
	 */
	private static final String NXXX_NX = "NX";
    
    /**
	 * 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 expireTime 超时时间,单位毫秒
	 * @return true 保存成功 false 保存失败
	 */
	public static boolean setNX(String key, String value, long expireTime){
		if(StringUtils.isEmpty(key) || expireTime <= 0){
			return false;
		}

		try (Jedis jedis = jedisPool.getResource()){
			String result = jedis.set(key, value, NXXX_NX, EXPIRE_TIME_UNITS_PX, expireTime);
			return STATUS_CODE_REPLY_SUCCESS.equalsIgnoreCase(result);
		}catch (Throwable t){
			logger.error("[E1][redis setNX Throwable][key:{},value:{},time:{}]", key, value, expireTime, t);
			return false;
		}
	}
}


// 源码
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();
  }

其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。

    /**
     * 释放锁
     * @param key 锁的信息
     * @return 是否释放锁成功
     */
    public static boolean release(String key){
        return RedisUtils.del(key, ThreadIdUtils.get());
    }

    /**
	 * 删除key的值为value的key
	 * @param key 保存的key值
	 * @param value 保存的value值
	 * @return true 删除成功 false 删除失败
	 */
	public static boolean del(String key, String value){
		if(StringUtils.isEmpty(key)){
			return false;
		}
		try (Jedis jedis = jedisPool.getResource()){
			Long result = (Long)jedis.eval(LUA_DEL_KEY_VALUE, 1, key, value);
			return result == 1;
		}catch (Throwable t){
			logger.error("[E1][redis del Throwable][key:{},value:{}]", key, value, t);
			return false;
		}
	}

	private final static String LUA_DEL_KEY_VALUE =
			"if redis.call(\"get\",KEYS[1]) == ARGV[1]" // 如果key的value等于传输的value
			+ " then"
				+ " return redis.call(\"del\",KEYS[1])" // 删除key
			+ " else"
				+ " return -1" // 返回-1
			+ " end";

参考文章

你可能感兴趣的:(Redis,redis,分布式锁)