【Java面试】几种常见的分布式锁

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 前言随着互联网的发展,各种高并发、海量处理的场景越来越多。为了实现高可用、可扩展的系统,常常使用分布式,这样避免了单点故障和普通计算机cpu、内存等瓶颈。

前言

随着互联网的发展,各种高并发、海量处理的场景越来越多。为了实现高可用、可扩展的系统,常常使用分布式,这样避免了单点故障和普通计算机cpu、内存等瓶颈。

但是分布式系统也带来了数据一致性的问题,比如用户抢购秒杀商品多台机器共同执行出现超卖等。有些同学容易将分布式锁与线程安全混淆,线程安全是指的线程间的协同。如果是多个进程间的协同需要用到分布式锁,本文总结了几种常见的分布式锁。

基于数据库

悲观锁—事务

比如用户抢购秒杀商品的场景,多台机器都接收到了抢购的请求,可以将获取库存、判断有货、用户付款、扣减库存等多个数据库操作放到一个事务,这样当一台机器与数据库建立链接请求了抢购商品这个事务,另外的机器只能等这个机器将请求完成才能操作数据库。在实际应用场景中,常常库存与交易是两个独立的系统,这时的事务是一个分布式事务,需要用到两段式、三段式提交。

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

缺点:在高并发的场景下开销是不能容忍的。容易出现数据库死锁等情况。

乐观锁—基于版本号

乐观锁常常用于分布式系统对数据库某张特定表执行update操作。考虑线上选座的场景,用户A和B同时选择了某场次电影的一个座位,都去将座位的状态设置为已售。

设想这样的执行序列:

1、用户A判断该座位为未售状态;

2、用户B判断该座位为未售状态;

3、用户A执行update座位为已售;

4、用户B执行update座位为已售。

这样会出现同一个座位售出两次的情况,解决方案是在这张数据库表中增加一个版本号的字段。执行操作前读取当前数据库表中的版本号,在执行update语句时将版本号放在where语句中,如果更新了记录则说明成功,如果没有更新记录,则说明此次update失败。

加了乐观锁的执行序列:

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

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

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

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

优点:乐观锁的性能高于悲观锁,并不容易出现死锁。

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

基于memcached

memcached可以基于add命令加锁。memcached的add指令是指如果有这个key,add命令则失败,如果没有这个key,则add命令成功。并且memcached支持设置过期时间的add原子操作。并发add同一个key也只有一个会成功。

基于memcached的add指令加分布式锁的思路为:定义一个key为分布式锁的key,如果add一个带过期时间的key成功则执行相应的业务操作,执行完判断锁是否过期,如果锁过期则不删除锁,如果锁没过期则删除锁。带过期时间是防止出现机器宕机,一直不能释放锁。

很多人基于memcached实现的分布式锁没有判断锁是否过期,执行完相应的业务操作直接删除锁会出现以下问题。

设想这样的执行序列:

1、机器A成功add一个带过期时间的key;

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一个锁key,相应的value为当前时间加上过期时间的时钟;

2、如果setNx成功,或者当前时钟大于此时key对应的时钟则加锁成功,否则加锁失败退出;

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

4、释放锁时判断当前时钟是否小于锁key的value,如果当前时钟小于锁key对应的value则执行删除锁key的操作。

注:这对于单点的redis能很好地实现分布式锁,如果redis集群,会出现master宕机的情况。如果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高级大牛直播讲解知识点,分享知识,课程内容都是各位老师多年工作经验的梳理和总结,带着大家全面、科学地建立自己的技术体系和技术认知!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
11天前
|
监控 负载均衡 Cloud Native
ZooKeeper分布式协调服务详解:面试经验与必备知识点解析
【4月更文挑战第9天】本文深入剖析ZooKeeper分布式协调服务原理,涵盖核心概念如Server、Client、ZNode、ACL、Watcher,以及ZAB协议在一致性、会话管理、Leader选举中的作用。讨论ZooKeeper数据模型、操作、会话管理、集群部署与管理、性能调优和监控。同时,文章探讨了ZooKeeper在分布式锁、队列、服务注册与发现等场景的应用,并在面试方面分析了与其它服务的区别、实战挑战及解决方案。附带Java客户端实现分布式锁的代码示例,助力提升面试表现。
30 2
|
25天前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
48 1
|
4天前
|
XML 缓存 Java
Java大厂面试题
Java大厂面试题
16 0
|
4天前
|
存储 安全 Java
Java大厂面试题
Java大厂面试题
10 0
|
4天前
|
存储 安全 Java
Java大厂面试题
Java大厂面试题
13 0
|
5天前
|
安全 Java
就只说 3 个 Java 面试题 —— 02
就只说 3 个 Java 面试题 —— 02
18 0
|
5天前
|
存储 安全 Java
就只说 3 个 Java 面试题
就只说 3 个 Java 面试题
10 0
|
15天前
|
Java 关系型数据库 MySQL
大厂面试题详解:Java抽象类与接口的概念及区别
字节跳动大厂面试题详解:Java抽象类与接口的概念及区别
39 0
|
24天前
|
存储 缓存 算法
Java入门高频考查基础知识4(字节跳动面试题18题2.5万字参考答案)
最重要的是保持自信和冷静。提前准备,并对自己的知识和经验有自信,这样您就能在面试中展现出最佳的表现。祝您面试顺利!Java 是一种广泛使用的面向对象编程语言,在软件开发领域有着重要的地位。Java 提供了丰富的库和强大的特性,适用于多种应用场景,包括企业应用、移动应用、嵌入式系统等。下是几个面试技巧:复习核心概念、熟悉常见问题、编码实践、项目经验准备、注意优缺点、积极参与互动、准备好问题问对方和知其所以然等,多准备最好轻松能举一反三。
49 0
Java入门高频考查基础知识4(字节跳动面试题18题2.5万字参考答案)
|
28天前
|
Java 程序员 API
java1.8常考面试题
在Java 1.8版本中,引入了很多重要的新特性,这些特性常常成为面试的焦点
42 8