redis 学习 - 分布式锁

本篇已收录至redis in action 学习笔记系列

分布式锁作用于不同的 redis 客户端之间. 何时使用, 使用 watch 还是锁, 则取决于当使用 watch 命令来监视一个会被频繁访问的 key 时会带来系统性能的损失时, 则需要考虑是否用锁.

设计锁之前先想想可能会有哪些问题

首先需要了解使用锁可能出现的问题:

  • 持有锁的进程, 因为操作时间过长, 而导致锁被自动释放, 但是进程本身并不知道锁被释放, 甚至可能会进一步错误的释放掉其他进程持有的锁.

  • 一个持有锁并打算执行长时间操作的进程已经崩溃, 但是其他进程不知道哪个进程持有锁, 也无法检测出持有锁的进程已经崩溃.

  • 在一个进程持有锁过期了以后, 多个进程同时去获取锁, 并且都获得了锁.

  • 多个进程获取到了锁, 并且都以为只有自己获取到了锁.

redis 在最新的硬件上每秒可以执行 100,000 次操作, 在高端的硬件上每秒可以执行 225,000 次操作, 所以上述问题在高负载高并发的情况下是完全可能发生的.

设计一个简单锁 v1.0

正确的加锁

使用 setnx 命令是实现这个锁的前提. 下图是实现了 获取锁 的函数:

原理: 当 key 不存在时, 为 key 设置一个值, 否则在一定时间内进行尝试.

谨慎的释放锁

加锁的时候, 使用 setnx 对 key 设置了一个 uuid, 这个id 可以在对锁释放时, 使用 watch 进行监控, 如果 id 没有变化, 才会去释放锁, 同样这个 id 可以确保释放锁的操作不会重复多次

下面是释放锁的函数:

使用锁代替 watch 并对比两者的性能

下面的代码是redis in action书中的一个案例, 在市场中购买商品时的一个一致性实现, 此处使用刚刚设计的锁去实现分布式一致性问题:

在这段代码里面锁的名字是 lock:market, 所以锁并不是只有一把, 可以根据业务需要自定义N多个锁. 然后作者给出了性能对比:

由于锁的引入, 每次操作实际上市场这个对象实际上是被锁上的, 所以可以看出商品的上架数量上会受到一定程度的影响而变少, 但是其他的方面的提升还是很明显的. 出现这种情况的原因是加锁的粒度过大导致的. 整个市场都被锁住了, 影响会比较大. 正常来说, 上架的时候只要是与你购买的物品无关, 那不应该被锁住才对.

设计一个细粒度的锁 v2.0

对锁1.0进行改进, 将锁的粒度降低, 只锁住准备购买的商品上, 这样对锁的竞争就下降了, 进而商品的上架率就提升了, 整个系统的性能进而会提升.

当使用了细粒度的锁后, 对比1.0的图如下:

书中并没有给出2.0的锁的代码, 只是展示了N多张性能表现的图表. 对于细粒度和粗粒度的选择, 往往要根据实际情况来定. 并不一定是细粒度就好.

设计一个带有超时功能的锁 v3.0

上面的锁在持有者崩溃的时候并不会释放. 这将会导致锁一直处于被获取的状态. 为了解决这个问题, 需要对锁添加一个超时的功能.

这个超时设计基于以下核心原理:

  • setnx 命令对锁赋值以后, 要使用 EXPIRE 对锁添加过期时间.

  • 如果程序在 setnx 和 expire 之间崩溃(极限情况), 其他的进程在尝试获取锁时, 需要检查锁的剩余过期时间, 如果发现锁没有过期时间, 则需要对锁重新添加过期时间. 这里虽然有可能会有多个客户端同时为锁添加过期时间, 但是这之间的差别不会太大, 所以可以不做考虑的.

下面的代码为优化后的获取锁的函数:

注: 这个代码片段比较老了, 新的 redis 是支持在 set 的时候设置 nx 和 ex 选项, 所以代码量上会少很多.

设计一个锁, 可以限制访问资源的客户端个数 v4.0

前面几代锁的特点是, 当对资源加锁以后, 只能一个持有了锁客户端进程访问该资源, 能否设计这样一个锁, 可以让 n 个进程对其访问. 这种锁有个名字叫做计数信号量.

技术信号量与普通的锁进行一些对比的话, 正常客户端在申请锁时如果失败, 会等待一段时间继续申请. 而如果客户端申请信号量失败的时候, 会直接返回该资源正在繁忙的信息.

书中同样设计了一个场景:

访问 market 市场不再限于游戏内部, 而是在外部可以使用进程访问市场, 同一个账号只能最多有 5 个进程访问.

不公平信号量与公平信号量

这里涉及到了一个概念是公平问题. 书中给出信号量的设计方案是用一个有序集合, 将进程 id 作为有序集合的成员, 将 id 添加进有序集合的时间戳作为分值, 每个进程加入集合后, 检查自己的排名, 如果超过了 5, 就证明自己无权获取信号量, 则需要从该集合移除自己. 但是使用时间戳作为分值会发生一个问题, 不同的主机的时间可能是不同的.

为了能够避免不公平现象出现, 通常会结合一个计数器使用, 并将计数器经过一个算法计算后的值作为有序集合的分值. 但是对于 32 位平台的 redis server, 计数器容易出现溢出. 所以对于此情况, 计数器的作用并不大.

posted @ 2020-03-28 20:44  YanyuWu  阅读(229)  评论(0编辑  收藏  举报