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, 计数器容易出现溢出. 所以对于此情况, 计数器的作用并不大.