关于分布式锁的概念,具体实现方式,直接参阅下面两个帖子,这里就很少介绍了。redis
分布式锁的多种实现方式数据库
分布式锁总结缓存
对于分布式锁的几种实现方式的优劣,这里再列举下服务器
1. 数据库实现方式异步
优势:易理解async
缺点:操做数据库消耗较大,性能较低。为了处理一些异常,会使得整个方案愈来愈复杂分布式
2. 缓存实现方式函数
优势:性能好,实现起来较为方便。性能
缺点:经过超时时间来控制锁的失效时间并非十分的靠谱。.net
3 zookeeper实现
优势:有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。
缺点:性能上不如使用缓存实现分布式锁
第二篇帖子中,谈到redis实现分布式锁时,提了一些建议
"redis若是能像ZooKeeper同样,实现了和客户端绑定的临时key,一旦redis客户端挂了,临时key删除,通知watch该key的其余客户端(感受这个是一个不错的需求,不知redis将来是否要实现),就能够消除锁超时,再使用Redlock实现的分布式锁,这时候可靠性就更高了。"
就性能而言,redis比zookeeper具备自然优点,而它的缺点也能够经过一些机制来另外改进。因此就尝试着修改了redis的源码,看可否解决上述问题。
修改点一:增长一条命令settp
settp(tp 能够理解为temporary的缩写),故名思议,就是一个临时的key。
命令格式:settp key value
首先使用这条命令,必须保证key是不存在的,即这个命令具备setnx命令的属性,而后在添加完key以后,将这个key加入到执行这条命令client的一个list里面。这个list专门用来保存临时键。那么在redis客户端挂了,或者意外断开链接时,在调用freeclient()函数时,即可以将临时键清理掉。就不会影响其余client再次获取锁
修改点二:增长命令watchex
命令格式:watchex key
返回:redisReply是一个字符串类型
若是key存在,则str内容为"EXIST"
若是key不存在,则str内容为"NOEXIST"
若是key被添加,返回"ADD";key被删除时,返回"DEL"
watchex,ex能够认为是exist的缩写,也是为了区别redis自己带有的watch命令。自带的watch命令,是为了在执行事务时,保证事务执行过程当中键不被修改的一种乐观锁机制。而咱们要实现的watchex命令,是为了监视某个键是否存在。在执行命令时,当即会返回一个结果,表示这个键是否存在。而后在运行过程当中,若是这个键被建立,或者被删除,也会通知到watchex该key的全部客户端。
示例以下:
首先运行hiredis-example-ae,对应的源文件是example-ae.c
在另外一个窗口中执行以下命令
能够看到在删除或者添加某个key时,在第一个窗口中都会收到通知
若是不想再watchex某个key,执行unwatchex key命令便可。
这个命令的实现原理其实有点相似redis 自身的pubsub机制,可是pubsub有一个局限就是,执行了该命令以后,就不能执行其余命令,只能等待channel上的信息。这种方式显然不适用于咱们的场景。
咱们的实现方式是,首先须要在client中保存一个全部watchex的list,而后在系统增长一个dict,用于保存每一个被watchex的key。这个dict的键就是被watchex的key,值就是全部watchex这个key的client组成的一个链表。
不管在添加或者是删除某个key时,都去检查一下这个dict里面,有没有这个key。若是有,取出全部的client,发一份通知消息。
因为这个watchex这个命令,是一个典型的异步通知。因此在客户端调用这个命令时,要使用redis的异步执行命令接口redisAsyncCommand。具体调用方式,能够参考example-ae.c文件。
固然在客户端解析请求时,也要作一些变化。在async.c这个文件中,redisProcessCallbacks()这个函数专门解析服务器发回来的相应。每次从读缓冲区组装出一个redisreply结构,而后从redisCallbackList 里面取出头结点,其实就是一个回调函数,将redisreply传入到这个回调函数。这就是一次正常的调用过程。可是对于watchex命令,它是一个永久命令,故而不能回调函数不能插到redisCallbackList里面,因此另外建了一个dict用于保存watchex命令的回调函数,键是watchex命令的key,值便是回调函数。这样每次客户端解析出一个redisreply,首先判断这个reply是否是一个watchex命令的返回,若是是就从dict里面获取相应的回调函数,不然执行原有的解析流程。
整个过程便是如此,那么下面咱们说一下在此基础上实现分布式锁的过程
首先,调用settp key "value"命令,若是返回成功,则说明获取锁成功;不然调用watchex key命令。因为这两步操做不是原子的,因此有可能调用watchex命令以后,返回noexist ,那么这时能够再尝试调用settp命令。若是还返回失败,说明锁已经被其余人占有,调用者能够等待或者干别的事。 当占有锁的人,用完释放以后,全部watchex这个key的client都会收到通知,这时全部client都会调用settp命令去抢锁,只会有一我的成功,其他的则继续等待,直到能抢占到锁为止。
从这个过程当中,能够看出,这种实现方式会有“惊群”的问题,即通知了全部人,只有一我的能抢到锁,就会致使不少的无效操做。固然,也能够选择在key被释放时,只通知某一个client。可是因为redis的回复消息是没有确认机制的,若是这个通知消息丢失了,就可能致使其余全部的client一直等待下去。目前,尚未更好的解决方法,暂时先选择通知全部的client,若是你们有更好的方案,欢迎留言讨论。