做者:踩刀诗人html
www.cnblogs.com/chopper-poet/p/10802242.html面试
前言redis
提到数据一致性、操做原子性,诸如此类的一些与并发有关的词汇时不知道你第一时间会联想到什么呢?我相信大多数人可能会想到“锁”,为何是锁呢,这个我很少说,你们内心应该都明白。在单体应用时代,咱们使用jvm提供的锁就能够很好的工做,可是到了分布式应用时代,jvm提供的锁就行不通了,那么势必要借助一些跨jvm的临界资源来支持锁的相关语义,好比redis,zookeeper等。数据库
步入正题安全
我今天就来分享下我司基于redis来实现的分布式锁,2013年投入使用,也算是久经沙场。可是也存在一些设计上的缺陷,这个我后面也会提到,但愿你们秉着互相学习的态度文明交流,别一上来就说这不行那不行,仍是那句话“适合本身的才是最好的”。网络
加锁过程分析并发
我第一次读代码的时候,有这么几个疑惑:框架
Q1:为何不使用 SET key value [expiration EX seconds|PX milliseconds] [NX|XX] 这个指令来实现key的自动过时呢,反而放到应用代码判断key是否过时?jvm
A1:咱们的分布式锁开发的时候SET命令还不支持NX、PX,因此才想出这种办法来实现key过时,NX、PX在2.6.12之后开始支持;分布式
Q2:已经判断了当前key对应的时间戳已通过期了,为何还要使用getset再获取一次呢,直接使用set指令覆盖不能够吗?
A2:这里其实牵扯到并发的一些事情,若是直接使用set,那有可能多个客户端会同时获取到锁,若是使用getset而后判断旧值是否过时就不会有这个问题,设想一下以下场景:
C1加锁成功,不巧的是,这时C1意外的奔溃了,天然就不会释放锁;
C2,C3尝试加锁,这时key已存在,因此C2,C3去判断key是否已过时,这里假设key已通过期了,因此C2,C3使用set指令去设置值,那两个都会加锁成功,这就闯大祸了;若是使用getset指令,而后判断下返回值是否过时就能够避免这种问题,假如C2跑的快,那C3判断返回的时间戳已通过期,天然就加锁失败;
释放锁过程分析
Q1:为何释放锁时还须要判断key是否过时呢,直接del不是性能更高吗?
A1:考虑这样一种场景:
C1获取锁成功,开始执行本身的操做,不幸的是C1这时被阻塞了;
C2这时来获取锁,因为C1被阻塞了很长时间,因此key对应的value已通过期了,这时C2经过getset加锁成功;
C1尘封了过久终于被再次唤醒,对于释放锁这件事它但是认真的,伴随着一波del操做,悲剧即将发生;
C3来获取锁,好家伙,竟然一下就成功了,接着就是一波操做猛如虎,接着就是一堆的客诉过来了;
为何会这样呢?回想C1被唤醒之后的事情,竟然敢直接del,C2活都没干完呢,锁就被C1给释放了,这时C3来直接就加锁成功,因此为了安全起见C3释放锁时得分红两步:1.判断value是否已通过期 2.若是已过时直接忽略,若是没过时就执行del。这样就真的安全了吗?安全了吗?安全了吗?假如第一步和第二步之间相隔了好久是否是也会出现锁被其余人释放的问题呢?是吧?是的!有没有别的解决办法呢?据说借助lua就能够解决这个问题了。
正视本身的缺点
Q1:Redis锁的过时时间小于业务的执行时间该如何续期?
A1:这个暂时没有实现,听说有一个叫Redisson的家伙解决了这个问题,咱们也有部分业务在使用,将来有可能会切换到Redisson。
Q2:怎么实现的高可用?
A2:咱们采用Failover机制,初始化redis锁的时候会维护一个redis链接池,加锁或者释放锁的时候采用多写的方式来保障一致性,若是某个节点不可用的时候会自动切换到其余节点,可是这种机制可能会致使多个客户端同时获取到锁的状况,考虑这种状况:
C1去redis1加锁,加锁成功后会写到redis2,redis3;
C2也去redis1加锁,可是此时C2到redis1的网络出现问题,这时C2切换到redis2去加锁,因为第一步中的redis多写并非原子的,全部就有可能致使C2也获取锁成功;
针对这种状况,目前有些业务方是经过数据库惟一索引的方式来规避的,将来会修复这个bug,具体方案目前尚未。
【推荐阅读】
[技术]:面试官问你MySQL的优化,看这篇文章就够了
[技术]:IntelliJ IDEA快捷键终极大全,速度收藏!