Redis共有五种基础数据结构,字符串(String),集合(Set),列表(List),散列(Hash)和有序集合(ZSet)。
分布式锁相较于锁来说,有几个问题需要解决:
1、 持有锁的进程因操作过长,导致自身的锁被释放,但是自身并不知晓这一点;
2、 持有锁的进程因操作过长不能及时释放锁,导致其他进程无法获取锁;
3、 在一个集成持有的锁过期后,其他多个进程都可能尝试去获取锁,并且都获取到了锁。
解决分布式锁的关键还是提供:超时机制,关键节点加上本地锁。
可以简单的通过使用Redis提供的SETEX命令实现基本的锁功能,但是无分布式锁常见的一些高级特性,比如超时时限等。
相对于锁,如果使用Redis提供的Watch,Multi,exec组成的事务,不存在良好的扩展性,在线程竞争激烈的情况下,线程重试概率较大,存在较为严重的资源浪费。
简易锁:通过setex方法设置值来设计锁,并且监控锁是否会发生变化(redis中客户端可以访问锁),一旦发生变化进行锁的消除。
在给锁增加超时时限设置时,会在程序取得锁之后调用EXPIRE命令设置过期时间,但如果在获取锁与调用Expire之间线程崩溃则该锁就无法释放了。因此,每当客户端在获取线程失败后会对请求的锁设置超时时限(由请求的客户端设置,条件是没有超时时限),最终锁就会带有超时时间,并且最终会因为时间超时而释放,其它的客户端就可以继续使用已经被释放的锁了。可能会存在多个客户端同时设置超时时限,但是锁的超时时间也不会产生太多变化。
信号量相较于锁来说,锁允许一个客户端访问,而信号量是允许多个客户端同时访问,两者的区别在于同时访问的客户端数量,那么如何选择哪些客户端可以先访问,在客户端使用完毕后,如何归还信号量;和锁相同,也是需要考虑信号量的超时时限问题,线程奔溃信号量如何回收问题。
用Redis构建信号量,主要是采用计数器+有序集合来构建,可以构建简易信号量,公平信号量和刷新信号量。考虑的因素是客户端首先是通过计算器的自增操作获取客户端在有序集合的排名,之后进入有序集合的排名,但是获取计数器的计算值与进入有序集合并不是一个原子操作,可能存在相互窃取信号量的问题。
具体的信号量获取操作是:
1、 去除超时信号量,有两个有序集合(1、超时有序集合,存储的是客户端的超时时限;2信号量拥有者有序集合,是客户端的排名,用以判断信号量又拥有者客户端【排名必须要低】),首先移除超时有序集合中的超时客户端,之后两个集合做交集并将结果保存在信号量拥有者有序集合中。【流水线事务操作的】
2、 调用计数器进行自增操作,并将计算器生成的值添加到信号量拥有者有序集合中,并添加客户端(当前的系统时间)到超时有序集合,如果排名足够低能够获取信号量,则成功获取信号量。【流水线操作】
3、 如果没有获取信号量,那么则需要从信号量拥有者有序集合和超时有序集合中移除和客户端相关的元素。【流水线事务操作】
【备注,redis的事务处理操作特性:1、要不全部执行成功,要不全部不执行;2、redis的事务处理时不受其他redis的执行命令影响】
当持有信号量的客户端需要延长信号量持有时间时,需要刷新信号量,显然从前面的阐述中得知,只需要将超时有序集合中系统时间进行刷新即可。
信号量之间也是存在一定的竞争条件,从上面可以得知,redis的信号量获取主要是分为三个步骤:
1、清除超时;2、获取计数器;3、获取信号量。在第2步和第3步存在线程竞争条件。 为了消除这种情况,在第2步与第3步期间增加获取锁的操作,或者对整个信号量获取的操作用前面提到的分布式锁进行同步处理。
参考资料:Redis实战