首页天道酬勤redis分布式锁原理面试题,java面试老是面试不上

redis分布式锁原理面试题,java面试老是面试不上

张世龙 05-03 21:13 120次浏览

前言

随着互联网的发展,各种高同时、大容量处理的场景在增加。 为了提供高可用性和可扩展性的系统,经常使用分布式系统,避免了单点故障和普通计算机的cpu、内存等瓶颈。

但是,分布式系统带来了数据一致性问题,如用户抢购秒杀商品的多台机器联合运行,出现超卖。 有些同学容易混淆分布式锁定和线程安全。 线程安全是指线程之间的协作。 当多个进程之间的协作需要分布式锁定时,本文总结了几种常见的分布式锁定。

数据库

悲观锁定-事务

例如,在用户抢购商品的情况下,多个设备收到抢购的请求,可以将多个数据库操作合并为一个事务处理,如获取库存、判断有无商品、用户支付、扣除库存等。 以这种方式,如果一台设备链接到数据库并请求进行商品抢购事务处理,则其他设备在该机器完成请求之前无法操作数据库。 在实际的APP应用方案中,库存和交易往往是两个独立的系统,这种情况下的事务是分布式事务,需要两个阶段、三个阶段的提交。

优点:是一种比较安全的实现方法。

缺点:高合并的场景下无法忍受费用。 容易发生数据库死锁等。

乐观锁定-基于版本号

乐观锁通常用于分布式系统对数据库中的特定表执行update操作。 考虑到在线选择座位的情况,用户a和b同时选择某部电影的座位,然后去将座位状态设定为已售。

假设这样的执行序列。

1、用户a判断该座位处于未销售状态;

2、用户b判断该座位处于未销售状态;

3、用户a执行更新的座位已经销售完毕

4、用户b已售出更新座位。

这可能会导致同一座位销售两次。 解决方案是将版本号字段添加到此数据库表中。 在执行操作之前读取当前数据库表的版本号,并在执行update语句时将版本号放入where语句中。 如果记录已更新,则表示成功;如果记录未更新,则表示此次更新失败。

带有乐观锁定的执行序列:

1、用户a查询该座位,得到该座位为未销售状态,版本号为5;

2、用户b查询该座位,得到该座位为未销售状态,版本号为5;

3、用户a执行update语句将座位状态更新为已售,版本号更新为6;

4、用户b执行update语句时,该座位记录版本号为6,没有版本号为5的该座位记录,执行失败。

优点:乐观锁定比悲观锁定性能更高,不易发生死锁。

缺点:乐观锁定只能锁定到一个表中的数据。 如果需要对多个表的数据操作进行分布式锁定,则不能基于版本号进行乐观锁定。

基于memcached

可以根据add命令锁定memcached。 memcached的添加命令意味着如果有此密钥,添加命令将失败,如果没有此密钥,添加命令将成功。 此外,memcached还支持用于设置过期日期的添加原子操作。 同时add相同的key也只有一个成功。

基于memcached的add命令和分布式锁定的思想是,key定义为分布式锁定,如果add过期的key成功,则执行相应的业务操作,执行完成后判断锁定是否过期,如果锁定过期,则不删除锁定带有效期限是防止机器停机,不能解除锁定。

很多人并未根据memcached实现的分布式锁定判断锁定是否过期,在执行相应的业务操作后直接删除锁定会出现以下问题。

假设这样的执行序列。

1、机器a成功添加了带有效期限的密钥

2、机器a在执行业务操作时出现长时间休眠,如长时间GC pause;

3、机器a在长时间休眠中尚未恢复,且已解锁,机器b成功add了带有效期的锁;

4、此时机器a已从长时间休眠中恢复,并执行相应的业务操作,解除了机器Badd的锁定;

5、此时,机器b的业务操作在无锁定保护的情况下执行。

但是,memcached没有提供判断有无key的操作,需要将锁定时的时钟和执行业务操作的时钟减去求出执行时间,将执行时间和锁定的有效期限进行比较。 或者,将与锁定key对应的value设置为当前时间加上过期时间的时钟,然后执行相应的业务操作将锁定key的值与当前时钟进行比较。

注:有效期必须长于执行业务操作的时间。

优点:性能高于数据库实现方法。

基于redis

redis提供了setNx原子操作。 基于redis的分布式锁定也是基于此操作实现的。 setNx是指,有此key则set失败,没有此key则set成功,但setNx无法设置超时时间。

基于redis的分布式锁定解决方案包括:

1、setNx为锁键,对应的value为当前时间加上有效期的时钟;

2、setNx成功或当前时钟大于此时key对应的时钟则结束锁定成功,否则以锁定失败结束;

3、加锁正常执行相应业务操作(处理共享数据源);

4、解锁时,判断当前时钟是否小于锁定键的值,当前时钟小于对应于锁定键的值,执行删除锁定键的操作。

注:这可以很好地为单个redis实现分布式锁定。 对于redis群集,将发生ma

ster宕机的情况。如果master宕机,此时锁key还没有同步到slave节点上,会出现机器B从新的master上获取到了一个重复的锁。

设想以下执行序列:

1、机器AsetNx了一个锁key,value为当前时间加上过期时间,master更新了锁key的值;

2、此时master宕机,选举出新的master,新的master正同步数据;

3、新的master不含锁key,机器BsetNx了一个锁key,value为当前时间加上过期时间;

这样机器A和机器B都获得了一个相同的锁;解决这个问题的办法可以在第3步进行优化,内存中存储了锁key的value,在执行访问共享数据源前再判断内存存储的锁key的value与此时redis中锁key的value是否相等如果相等则说明获得了锁,如果不相等则说明在之前有其他的机器修改了锁key,加锁失败。同时在第4步不仅仅判断当前时钟是否小于锁key的value,也可以进一步判断存储的value值与此时的value值是否相等,如果相等再进行删除。

此时的执行序列:

1、机器AsetNx了一个锁key,value为当前时间加上过期时间,master更新了锁key的值;

2、此时,master宕机,选举出新的master,新的master正同步数据;

3、机器BsetNx了一个锁key,value为此时的时间加上过期时间;

4、当机器A再次判断内存存储的锁与此时的锁key的值不一样时,机器A加锁失败;

5、当机器B再次判断内存存储的锁与此时的锁key的值一样,机器B加锁成功。

注:如果是为了效率而使用分布式锁,例如:部署多台定时作业的机器,在同一时间只希望一台机器执行一个定时作业,在这种场景下是允许偶尔的失败的,可以使用单点的redis分布式锁;如果是为了正确性而使用分布式锁,最好使用再次检查的redis分布式锁,再次检查的redis分布式锁虽然性能下降了,但是正确率更高。

基于zookeeper

基于zookeeper的分布式锁大致思路为:

1、客户端尝试创建ephemeral类型的znode节点/lock;

2、如果客户端创建成功则加锁成功,可以执行访问共享数据源的操作,如果客户端创建失败,则证明有别的客户端加锁成功,此次加锁失败;

3、如果加锁成功当客户端执行完访问共享数据源的操作,则删除znode节点/lock。

基于zookeeper实现分布式锁不需要设置过期时间,因为ephemeral类型的节点,当客户端与zookeeper创建的session在一定时间(session的过期时间内)没有收到心跳,则认为session过期,会删除客户端创建的所有ephemeral节点。

但是这样会出现两个机器共同持有锁的情况。设想以下执行序列。

1、机器A创建了znode节点/lock;

2、机器A执行相应操作,进入了较长时间的GC pause;

3、机器A与zookeeper的session过期,相应的/lock节点被删除;

4、机器B创建了znode节点/lock;

5、机器A从较长的停顿中恢复;

6、此时机器A与机器B都认为自己获得了锁。

与基于redis的分布式锁,基于zookeeper的锁可以增加watch机制,当机器创建节点/lock失败的时候可以进入等待,当/lock节点被删除的时候zookeeper利用watch机制通知机器。但是这种增加watch机制的方式只能针对较小客户端集群,如果较多客户端集群都在等待/lock节点被删除,当/lock节点被删除时,zookeeper要通知较多机器,对zookeeper造成较大的性能影响。这就是所谓的羊群效应。

优化的大致思路为:

1、客户端调用创建名为“lock/number_lock_”类型为EPHEMERAL_SEQUENTIAL的节点;

2、客户端获取lock节点下所有的子节点;

3、判断自己是否是序号最小的节点的,如果是最小的节点则加锁成功,如果不是序号最小的节点,则在比自己小的并且最接近的节点注册监听;

4、当被关注的节点删除后,再次获取lock节点下的所有子节点,判断是否是最小序号,如果是最小序号则加锁成功;

优化后的思路,虽然能一定程度避免羊群效应,但是也不能避免两个机器共同持有锁的情况。

工作一到五年的程序员朋友面对目前的技术无从下手,感到很迷茫可以加群744677563,里面有阿里Java高级dfdlc直播讲解知识点,分享知识,课程内容都是各位老师多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!

实现分布式锁,分布式锁框架