前述
本篇先讲述Redis锁、信号量实现,最后讲解事务及Lua脚本。对于锁和信号量的具体实现,这里不讲,可以查看网上关于Redis分布式锁具体实现的博文。这里只谈谈相关实现思路,并指出相应实现会遇到的问题及解决思路。
分布式锁
Redis没有原生支持锁,需要自己实现。基本思路就是利用string。下面是一个简要步骤。
1,获取锁
执行下面命令获取锁
set lockname value EX 30 NX
上面的命令设置一个含过期时间字符串,过期时间自己定,它的作用是避免某个获取锁的客户端由于某些原因长期阻塞。 NX选项指定在字符串不存在的时候才能设置成功,用来判断是否获取到锁。
如果成功设置,则获取到锁,否则继续等待重新获取。
PS:value最好是个随机字符串,比如uuid,避免并发状态下误删其它客户端创建的锁。
2,释放锁
释放锁就是删除指定的key。由于del删除的时候可能是其他事务设置的锁,因此一定要检查value是否相等。并利用WATCH
监听锁key,避免被其他事务修改。
3,改进
上面的删除不是原子性的,因此可以利用lua脚本来删除。lua脚本能很好的支持事务。
3,其他方式
Redis官方推荐RedLock算法实现,它可以避免上面的锁单节点的问题。很多主要语言都已经有开源的实现了,自己项目中直接用就行。参考官网 和RedLock锁
RedLock缺点就是只适用于N个独立的Redis节点,主从模式和集群模式并不适用。而且至少得三个节点。参考Redis RedLock 完美的分布式锁么?
其他参考资料:Redis分布式锁
!思考,如果主从复制模式,A进程在master节点获取到锁,在锁信息还没同步到slave节点时,master节点挂掉,此时slave被提升为主节点,B进程从slave节点获取锁,就造成重复获取锁,这种问题怎么解决?
对于上面问题,RedLock是无法解决的。因为RedLock锁不支持主从复制模式,而且依赖系统时间。思考了很久,我觉得Redis由客户端控制锁的方式是很难解决的,因为A进程和B进程之间并不知道彼此获取锁的情况。
如果master节点和slave节点数据强一致,只有数据同步完成才给客户端返回获取锁成功,那上面的问题就解决了。但是这种方式影响了Redis服务对命令的响应速度,和Redis的设计思想不匹配。而且,到目前为止官方也不支持数据强一致的主从复制和集群模式。
有人建议zookeeper实现分布式锁。我查阅了zookeeper相关资料,它对数据一致性的却支持的比较好,支持不同维度的数据一致性。关于zookeeper分布式锁,参考zookeeper分布式锁 与zookeeper功能
后面也会抽空研究下zookeeper,然后再针对zookeeper写相关系列的文章。
信号量
信号量是一种锁,用于限制资源访问的进程数。
1,基本构建
利用Redis zset数据结构存储持有信号量的进程,score为获取时间。假设我们允许5个进程获取信号量。获取信号量时,进程先把自己的标识和当前系统时间加入zset,检查自己的排序位置是不是小于最多允许的进程数(这里为5)。如果小于,则获取信号量成功,否则失败,删除插入的标识。
这里获取信号量时需要清除过期时间。
这种方式缺点很明显,每个进程指定的超时时间必须一致,否则无法清除超时的锁。 还有一个进程的信号量超时被其他进程释放,但是它自己并不知道,如果他的执行不是事务性的,中间可能被其他进程插入影响结果。
结论:客户端控制锁的问题,彼此之间交流是个问题。如果是Redis自己实现,它完全可以将锁和持有锁的进程映射存储,超时的时候强制回滚。
2,改进,提升公平
当获取信号量的进程位于不同网络主机上时,系统时间可能不一致。如A主机进程和B主机进程,加入A主机系统时间比B主机快,那么即使A首先插入自己的标识,B在没有操过这个时间插入也会偷走A成功获取信号量的机会。
为了提升公平,避免系统时间不一致的影响。可以为Redis实现一个计数器和一个拥有者zset,进程插入自己标识到拥有者zset,先获得计数器,再用计数器值作为score插入。(32位主机可能溢出,64位够用)。
3,刷新和消除竞争
刷新信号量的超时时间,利用上一节提到的分布式锁,消除资源计数器的竞争。
异步队列
1,先进先出队列
使用列表模仿,如果需要实现优先级,可以多个列表表示不同优先级。
2,延时队列
基于有序集合实现(sorted set)。基本思路是将任务作为zset的成员,任务执行时间点作为score。任务执行worker轮询zset,取出第一条数据并检测是否可以执行。
事务
multi、exec、watch、unwatch、discard。
Redis的事务没有回滚机制,某条语句执行错误,multi打包的事务就结束了。因此Redis事务原子性,一致性,持久性都不满足。由于Redis是单线程运行,事务可以保证隔离性。watch、unwatch命令实现类似乐观锁的机制。
Redis支持非事务流水线(pipeline),会将多个命令一次性发送给Redis,然后等待所以命令结果再返回。pipeline降低了网络延迟消耗。默认pipeline对多个命令不开启事务,不过可以通过参数调整。
Lua脚本
由于Redis事务的缺陷,Redis提供了Lua脚本来保证原子性,但是脚本会阻塞其他客户端进程执行。
通过EVAL
命令执行脚本。脚本中可以通过Redis.call
和Redis.pcall
调用Redis命令
一条简单的Redis脚本示例,传递参数应该由KEYS和ARGV指定。
eval "return redis.call('set',KEYS[1], ARGV[1])" 1 foo bar
后述
我们讲解了Redis分布式锁的实现思路和一些问题,并引出了zookeeper的替代方案。关于zookeeper,后期会深入研究并针对它写一系列文章。