分布式锁的实现—Redis(条理清晰)

实现分布式锁的必要条件:互斥性和不会发生死锁

互斥性的保证:就是同时只能有一个线程注册成功获取到锁 比如 jedis.setNX(key,value):方法含义:如果key不存在就设置
避免发生死锁:就是获得锁以后 无论这个加锁的客户端怎么样,都要最终能释放出来锁;

redis的分布式锁的实现机制就是:

  1. 获得锁:多线程竞争注册相同的key并存储value,因为Jedis有排他性的方法比如setNX(key,value),如果不存在对应的key,就注册key和value,所以同时只会有一个线程抢注成功(排他性)。
  2. 然后注册成功的线程就去执行自己要执行的业务代码。
  3. 释放锁:就是删除key,删除key的时候,通过key先获得value,然后线程根据value的值来判断这个key是不是自己注册的,如果是就可以删除key。所以value的值的识别性很重要。
  4. 释放锁以后(key被删除),其余的线程就可以开始抢注key了,就重复上演1,2,3流程了。

综上:我们可以理解为 谁能注册key 就相当于谁获得了锁,至于是谁注册的, 就要根据注册key的时候的value的值来判断。所以不同线程在竞争锁的时候key值应该一样,value值应该能识别线程身份,比如value是线程的名字。

获取锁的过程中存在的问题

避免死锁

如果某个线程,获得了锁(注册key和value)之后,突然死掉了,怎么办?这把锁就永远没办法得到主动释放,所以我们要设置锁的过期时间,
让锁被动释放,我们可以给key设置过期时间,过期了,key就会被redis删除掉,从而被动释放了锁。
if(jedis.setNX(key, value) ==1{
	jedis.setExpire(key,expireTime)
}
上面代码这样写符合我们的要求吗?抢占了,也设置了过期时间,是不是就OK了,
答案是不行的,因为万一执行了jedis.setNX(key, value) ==1之后,线程挂了,怎么办?是不是key就没有过期时间了,就死锁了,
所以我们需要将设置key和过期时间放在一个原子操作里,而上面分两步是两个原子操作。所以我们可以选择。
jedis.set(key,value,"NX", "PX",expireTime);这里就是将设置key和过期时间放在一个原子操作里了。

综上获取锁的代码可以如下:

    public boolean lock1(KeyPrefix prefix, String key, String value, Long lockExpireTimeOut,
                         Long lockWaitTimeOut) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String realKey = prefix.getPrefix() + key;
            Long deadTimeLine = System.currentTimeMillis() + lockWaitTimeOut;

            for (;;) {
                String result = jedis.set(realKey, value, "NX", "PX", lockExpireTimeOut);

                if ("OK".equals(result)) {
                    return true;
                }

                lockWaitTimeOut = deadTimeLine - System.currentTimeMillis();

                if (lockWaitTimeOut <= 0L) {
                    return false;
                }
            }
        } catch (Exception ex) {
            log.info("lock error");
        } finally {
            returnToPool(jedis);
        }

        return false;
    }

释放锁存在的问题

错误释放锁

仅仅 jedis.del(key) 肯定是不行的,比如此时Key是线程A注册的,线程B调用这个方法会直接删除这个key,就相当于B线程释放了A线程的锁。
那加个判断呢?判断Value是否是自己放进去的,如果不是就不能删除。
        String currentValue = jedis.get(realKey);
            if (!StringUtils.isEmpty(currentValue) && value.equals(currentValue)) {
                jedis.del(realKey);
            }
 看上去上面的代码好像没什么问题,但是仔细一看,这里的判断和删除是两步操作,也就是说可能存在一种情况,A线程在释放自己的锁的时候,刚执行完
 !StringUtils.isEmpty(currentValue) && value.equals(currentValue),准备删除自己注册的key的时候,这个时候刚好Key过期了,
 B线程抢注了自己的key和value,此时按理说锁是B的,但是A线程继续执行jedis.del(realKey)就会删除B的key,从而释放了B的锁这样也是不行的。
 所以我们就需要 将判断和删除 这两步操作合并成一个原子操作,那怎么办呢?通过Lua脚本来做如下:
   public boolean unlock1(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) {
            log.info("unlock error");
        } finally {
            returnToPool(jedis);
        }
        return false;

    }

你可能感兴趣的:(中间件,分布式)