0%

# Redis_分布式锁

Posted on 2019-12-10 Edited on 2020-01-12 In Redis , Distributed Lock , C , Redis Views: Views: Disqus:

今天看了一下 `redis` 分布式锁 `redlock` 的实现，简单记录下，

## 加锁

原先我对 `redis` 锁的概念就是加锁使用 `setnx`，解锁使用 `lua` 脚本，但是 `setnx` 具体是啥，`lua` 脚本是啥不是很清楚
首先简单思考下这个问题，首先为啥不是先 `get` 一下 `key` 存不存在，然后再 `set` 一个 `key value`，因为加锁这个操作我们是要保证两点，一个是不能中途被打断，也就是说要原子性，如果是先 `get` 一下 `key`，如果不存在再 `set` 值的话，那就不是原子操作了；第二个是可不可以直接 `set` 值呢，显然不行，锁要保证唯一性，有且只能有一个线程或者其他应用单位获得该锁，正好 `setnx` 给了我们这种原子命令

然后是 `setnx` 的键和值分别是啥，键比较容易想到是要锁住的资源，比如 `user_id`, 这里有个我自己之前比较容易陷进去的误区，但是这个误区后
面再说，这里其实是把`user_id` 作为要锁住的资源，在我获得锁的时候别的线程不允许操作，以此保证业务的正确性，不会被多个线程同时修改，确定了键，再来看看值是啥，其实原先我认为值是啥都没关系，我只要锁住了，光键就够我用了，但是考虑下多个线程的问题，如果我这个线程加了锁，然后我因为 `gc` 停顿等原因卡死了，这个时候`redis` 的锁或者说就是 `redis` 的缓存已经过期了，这时候另一个线程获得锁成功，然后我这个线程又活过来了，然后我就仍然认为我拿着锁，我去对数据进行修改或者释放锁，是不是就出现问题了，所以是不是我们还需要一个东西来区分这个锁是哪个线程加的，所以我们可以将值设置成为一个线程独有识别的值，至少在相对长的一段时间内不会重复。

上面其实还有两个问题，一个是当 `gc` 超时时，我这个线程如何知道我手里的锁已经过期了，一种方法是我在加好锁之后就维护一个超时时间，这里其实还有个问题，不过跟第二个问题相关，就一起说了，就是设置超时时间，有些对于不是锁的 `redis` 缓存操作可以是先设置好值，然后在设置过期时间，那么这就又有上面说到的不是原子性的问题，那么就需要在同一条指令里把超时时间也设置了，幸好 `redis` 提供了这种支持

    1
    SET resource_name my_random_value NX PX 30000

这里借鉴一下解释下，`resource_name`就是 key，代表要锁住的东西，`my_random_value`就是识别我这个线程的，`NX`代表只有在不存在的时候才设置，然后`PX 30000`表示超时时间是 30秒自动过期

PS：记录下我原先有的一个误区，是不是要用 key 来区分加锁的线程，这样只有一个用处，就是自身线程可以识别是否是自己加的锁，但是最大的问题是别的线程不知道，其实这个用户的出发点是我在担心前面提过的一个问题，就是当 gc 停顿后，我要去判断当前的这个锁是否是我加的，还有就是当释放锁的时候，如果保证不会错误释放了其他线程加的锁，但是这样附带很多其他问题，最大的就是其他线程怎么知道能不能加这个锁。

## 解锁

当线程在锁过期之前就处理完了业务逻辑，那就可以提前释放这个锁，那么提前释放要怎么操作，直接`del key`显然是不行的，因为这样就是我前面想用线程随机值加资源名作为锁的初衷，我不能去释放别的线程加的锁，那么我要怎么办呢，先 `get` 一下看是不是我的？那又变成非原子的操作了，幸好redis 也考虑到了这个问题，给了`lua` 脚本来操作这种

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

这里的`KEYS[1]`就是前面加锁的`resource_name`,`ARGV[1]`就是线程的随机值`my_random_value`

## 多节点

前面说的其实是单节点 `redis` 作为分布式锁的情况，那么当我们的 `redis` 有多节点的情况呢，如果多节点下处于加锁或者解锁或者锁有效情况下
`redis` 的某个节点宕掉了怎么办，这里就有一些需要思考的地方，是否单独搞一个单节点的 `redis`作为分布式锁专用的，但是如果这个单节点的挂了呢，还有就是成本问题，所以我们需要一个多节点的分布式锁方案
这里就引出了开头说到的`redlock`，这个可是 `redis`的作者写的, 他的加锁过程是分以下几步去做这个事情

- 获取当前时间（毫秒数）。
- 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同，包含随机字符串my_random_value，也包含过期时间(比如PX 30000，即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行，这个获取锁的操作还有一个超时时间(time out)，它要远小于锁的有效时间（几十毫秒量级）。客户端在向某个Redis节点获取锁失败以后，应该立即尝试下一个Redis节点。这里的失败，应该包含任何类型的失败，比如该Redis节点不可用，或者该Redis节点上的锁已经被其它客户端持有（注：Redlock原文中这里只提到了Redis节点不可用的情况，但也应该包含其它的失败情况）。
- 计算整个获取锁的过程总共消耗了多长时间，计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点（>= N/2+1）成功获取到了锁，并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time)，那么这时客户端才认为最终获取锁成功；否则，认为最终获取锁失败。
- 如果最终获取锁成功了，那么这个锁的有效时间应该重新计算，它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
- 如果最终获取锁失败了（可能由于获取到锁的Redis节点个数少于N/2+1，或者整个获取锁的过程消耗的时间超过了锁的最初有效时间），那么客户端应该立即向所有Redis节点发起释放锁的操作（即前面介绍的Redis Lua脚本）。
  释放锁的过程比较简单：客户端向所有Redis节点发起释放锁的操作，不管这些节点当时在获取锁的时候成功与否。这里为什么要向所有的节点发送释放锁的操作呢，这里是因为有部分的节点的失败原因可能是加锁时阻塞，加锁成功的结果没有及时返回，所以为了防止这种情况还是需要向所有发起这个释放锁的操作。
  初步记录就先到这。

请我喝杯咖啡

WeChat Pay

Alipay

- Post author: Nicksxs
- Post link: https://nicksxs.me/2019/12/10/Redis-Part-1/
- Copyright Notice: All articles in this blog are licensed under BY-NC-SA unless stating additionally.