九)redis 实现分布式锁

主要官方翻译文档:解释了redis实现分布式锁的问题
http://ifeve.com/redis-lock/

一个可靠的分布式锁的要求

  1. 安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
  2. 效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
  3. 效率属性B:容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

1)redis单实例
上锁的过程是基于 SET resource_name my_random_value NX PX 30000,即只有这个key不存在的时候才会设置key的值(NX代表必须为null此时返回1;如果返回0代表执行失败,该key已经存在了),同时设置了超时时间为30000ms(通过PX选项设置的)。注意,这个key的值设置为一个随机值my_random_value,它应当在所有获取锁的请求的客户端里保持唯一,这个用来后续安全的释放锁。

释放锁的过程如下:是基于一段lua脚本,这样才能保证类似CAS操作:删除这个key当且仅当这个key存在而且值是我期望的那个值。

if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end

这个很重要,是用来防止误删除其他客户端得到的锁。举例来说,客户端A拿到了锁,然后它被某个操作阻塞了很久,过了超时时间后redis会自动释放这个锁,客户端B获得了锁,然后客户端A又会尝试去删除这个锁。如果简单的用DEL指令可能会导致客户端A误删除其他客户端的锁,用这个方式可以保证该锁只能被lock的客户端进行unlock操作。

核心是:不管是加锁还是解锁,都要保持操作的原子性,否则就有可能产生死锁。
一种基于上述思路的实现方式
https://blog.csdn.net/qq_28397259/article/details/80839072

加锁

public boolean lock(String key, Long expire){
    try{
        final String lockResult = redisTemplate.execute((RedisCallback<String>) connection -> {
            JedisCommands commands = (JedisCommands) connection.getNativeConnection();
            String randomValue = UUID.randomUUID().toString();
            return commands.set(key, randomValue, "NX", "PX", expire);
        });
        return !StringUtils.isEmpty(lockResult);
    }catch (Exception e){
        log.error("set redis occured an exception", e);
    }
    return false;
}

备注直接用redisTemplate的set方法是没有设置过期时间的,故选择了上述的方式。

**上述的分布式锁是存在问题的,它只能在一个非分布式、单节点、永不宕机的环境下使用。**原因是如果有主从同步Master、Slave节点的话,那么会存在下列问题:
A)客户端A在master节点拿到了锁;
B)然后master节点在把A创建的key写入到slave之前宕机了;
C)slave节点被切换成了master节点
D)客户端B向新master节点申请并获得了另一把锁
此时客户端A和客户端B都持有一个锁,显然是互斥的。
这个方案可以用在对宕机情况下,允许若干客户端都持有锁的情况。他们并不是严格限制一致性的。

2)通过redLock方式
Redis团队开发了另一套方案。
它是基于N个(奇数)个redis master节点实现的。这些节点都是相互独立的,不用任何复制或分布式协调算法。一般N是5或者7,最好是不在不同节点上运行master节点,以保证他们不会同时宕机。
它的基础是客户端向所有的N个redis节点发送setNXPX命令,这里和上面单个节点的步骤是一致的,如果它能在一定时间范围内获取到超出半数的redis节点上锁成功,那就意味着它获得了分布式锁;否则需要到所有master节点上释放锁,不管这个锁有没有释放成功。

具体操作步骤如下,每个客户端执行如下步骤
1.获取当前时间(单位是毫秒)。
2.轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3.客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4.如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间
5.如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
6.当一个客户端需要重试获取锁时,这个客户端会等待一段时间,等待的时间相对来说会比我们重新获取大多数锁的时间要长一些,这样可以降低不同客户端竞争锁资源时发生死锁的概率。

REDLock的方案有几个需要注意的点:
1)锁的过期时间和每个redis锁请求的时间设置必须合理,必须要给业务执行留下足够的时间;
2)参与redLock的redis实例必须设置延时重启(也就是断电后其他原因时,断电后需要延期一段时间等待该实例上所有的锁都过期,然后再重启)
3)不同redis实例的系统时间需要一致,这个是有相应手段保证的。
原因如下:
假设有5个redis实例,某个客户端获取到了其中3个实例的锁(A\B\C),但是C随后断电重启了而且上面的锁没有被持久化(比如采用了默认的每秒一次fsync,有可能丢掉这1s的数据)。那么C、D、E就可以支持新获取一把锁了。
理论上是可以通过设置永远执行fsync来解决此问题,但是这样的性能会大受影响,比传统的要分布式锁系统要差了。
所以,在采用默认的每秒一次fsync的前提下,我们可以设置redis实例延时重启,从而让该redis实例上所有持有的锁都已经过期了(A、B上的锁都过期),然后重启C,这样在断电期间C不会影响到已有锁的争用,可以保证在断电重启情况下不会有分布式锁的安全性。

猜你喜欢

转载自blog.csdn.net/xiaohesdu/article/details/87906423