前述

本篇先讲述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.callRedis.pcall调用Redis命令

一条简单的Redis脚本示例,传递参数应该由KEYS和ARGV指定。

eval "return redis.call('set',KEYS[1], ARGV[1])" 1 foo bar

后述

我们讲解了Redis分布式锁的实现思路和一些问题,并引出了zookeeper的替代方案。关于zookeeper,后期会深入研究并针对它写一系列文章。


Published

Category

Redis

Tags

Stay in Touch

Friendship Links