分布式锁的实现之Redis
前面我们有聊过乐观锁和悲观锁的实现,均是对于单体架构的场景下的实现。那么现在我们来总结看下分布式情况下如何实现锁机制。
常见场景
我们来看下一个场景,假设我现在在分布式系统下要做一个业务逻辑的消费动作,我如何保证我的消费动作只被消费一次不重复消费?有的同学第一时间就想到了MQ,诸如Zookeeper。我们今天暂不谈MQ,那其实核心还是代码执行的锁机制问题。
我们再来看一个场景,我们有个接口需要经常查数据库DB数据,如果场景允许我们经常会对其加一层缓存,并设定过期时间。假设在某一瞬间,缓存过期,但此时并发量又很大,会有大量的请求穿透去数据库请求数据,造成缓存雪崩效应。于是,我们就可以考虑加锁机制,只让一个请求去执行查询DB更新缓存的操作。
基本原理
回顾下我们之前聊到锁的原理,分布式锁也是一样的,要实现它必须满足:
1.互斥:任何时刻只能有一个客户端对其加锁;
2.避免死锁:要充分考虑某客户端在持有锁的期间崩溃,也不能导致后续其他客户端不能加锁;
3.谁加锁谁解锁:加锁和解锁必须是同一个客户端,否则容易出现A客户端把B客户端的锁给解了,导致锁机制失效。
示例实践
我们仅以Redis实现分布式锁为例来说明分布式锁的实现。以单机单机部署Redis的情况为例,如果有分布式Redis集群部署的情况,可以参考Redlock算法的实现。下面我们进入Redis+Lua实现分布式锁的实践。
我们来看示例代码,php为例。
加锁

注意到代码的每个细节了么?都是至关重要的。上面的set是封装过的,那我们来简单说明一下这个方法吧,该方法分别对应了上面的锁需要满足的条件。比如,NX操作保证了锁的互斥,设置过期时间避免了死锁,唯一请求ID用来标注客户端,在解锁的时候可以用来校验是不是同一个客户端自己的锁。
解锁
解锁这个动作就有趣了,看似简单却暗藏玄机,也是很重要的环节。因为解锁存在一个判断是都本客户端的锁的操作,之后才执行解锁。而这个if判断在高并发的情况下我们不得不考虑操作的原子性,这其实和PHP等其他语言代码考虑高并发的原理是大相径庭(有兴趣的看官也可以思考下,为什么有判断就要保证原子性呢,有哪些可能出现问题的场景)。那我们如果保证操作的原子性呢?第一反应是想到事务?我们这里借助Lua脚本来保证原子性,Redis的eval命令执行Lua脚本保证原子性。注意这里指的是redis在执行整个lua脚本时是排他的,同一时间只能一个线程在执行这个脚本,这点很关键,可以用于一切高并发场景下多线程需要排他的原子性保证,如秒杀的库存判断和扣除,避免库存为负,超卖。redis本身是单线程的(与每个PHP文件的执行是单线程的不同的是,但是PHP服务器(apache/nigix/php-fpm)是多线程的;redis是排队处理每一个进来的请求,后面的处理才是多线程的和多路io复用。所以PHP是有并发问题的,redis是线程安全的),具有原子性,lua本身也具有原子性。

我们同样来说明下面的解锁代码。其实很简单,就是执行了一个Lua脚本,这个脚本实现了获取当前锁的值,即唯一请求ID值,判断是否同一个客户端的请求ID,如果是,则执行Redis的del操作。

好了,关于Redis实现分布式的锁例子就到这里了,这里只是简单的示例便于理解,实际生产将需要考虑更多的场景和因素,比如集群,Zookeeper方式实现,时间和能力有限,这里就不展开赘述。
参考资料:
两个nginx嵌入lua的例子(nginx-lua模块。根据请求参数不同返回指定5xx错误快照,避免直接在nginx里写逻辑难以维护;直接把PHP请求分发成异步,类似ajax但比其请求减少,启动lua协程异步请求各部分页面内容异步输出。python本身就没有协程。)
Lua在游戏中作为被C调用的逻辑处理脚本例子(协程,与C/C++好集成,热更新。从语言的的抽象层面来说C/C++的抽象低更加适合于底层逻辑的支持,而Lua脚本抽象层次高,更加适合游戏逻辑的实现。脚本语言运行在虚拟机之上,而虚拟机运行在游戏逻辑之上,作为一种解释型语言,我们可以随时修改并及时体现在游戏之中,快速完成开发,当然游戏核心还是C++来。C/C++却做不到,对一个巨大的游戏工程,每次修改都需要重新编译,成本很高。设想一下,如果所有的功能都是使用C/C++实现的话,那么对开发人员来说简直是一场灾难。)
相关传送门:乐观锁与悲观锁的实现