分布式锁看了又看,优秀方案我来告诉你

2025-05-29 0 69

分布式锁看了又看,优秀方案我来告诉你

分布式锁的场景

秒杀场景案例

对于商品秒杀的场景,我们需要防止库存超卖或者重复扣款等并发问题,我们通常需要使用分布式锁,来解决共享资源竞争导致数据不一致的问题。

以手机秒杀的场景为例子,在抢购的过程中通常我们有三个步骤:

扣掉对应商品的库存;2. 创建商品的订单;3. 用户支付。

对于这样的场景我们就可以采用分布式锁的来解决,比如我们在用户进入秒杀 “下单“ 链接的过程中,我们可以对商品库存进行加锁,然后完成扣库存和其他操作,操作完成后。释放锁,让下一个用户继续进入保证库存的安全性;也可以减少因为秒杀失败,导致 DB 回滚的次数。整个流程如下图所示:

分布式锁看了又看,优秀方案我来告诉你

注:对于锁的粒度要根据具体的场景和需求来权衡。

三种分布式锁

对于 Zookeeper分布式锁实现,主要是利用 Zookeeper 的两个特征来实现:

  1. Zookeeper 的一个节点不能被重复创建
  2. Zookeeper 的 Watcher 监听机制

非公平锁

对于非公平锁,我们在加锁的过程如下图所示。

分布式锁看了又看,优秀方案我来告诉你

优点和缺点

其实上面的实现有优点也有缺点:

优点:

实现比较简单,有通知机制,能提供较快的响应,有点类似 ReentrantLock 的思想,对于节点删除失败的场景由 Session 超时保证节点能够删除掉。

缺点:

重量级,同时在大量锁的情况下会有 “惊群” 的问题。

“惊群” 就是在一个节点删除的时候,大量对这个节点的删除动作有订阅 Watcher 的线程会进行回调,这对Zk集群是十分不利的。所以需要避免这种现象的发生。

解决“惊群”:

为了解决“惊群“问题,我们需要放弃订阅一个节点的策略,那么怎么做呢?

  1. 我们将锁抽象成目录,多个线程在此目录下创建瞬时的顺序节点,因为 Zookeeper 会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
  2. 首先创建顺序节点,然后获取当前目录下最小的节点,判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
  3. 获取锁失败的节点获取当前节点上一个顺序节点,对此节点注册监听,当节点删除的时候通知当前节点。
  4. 当unlock的时候删除节点之后会通知下一个节点。

公平锁

基于非公平锁的缺点,我们可以通过一下的方案来规避。

分布式锁看了又看,优秀方案我来告诉你

优点和缺点

优点: 如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。

缺点: 对于读写场景来说,无法解决一致性的问题,如果读的时候也去获取锁的话,这样会导致性能下降,对于这样的问题,我们可以通过读写锁来实现如类似 jdk 中的 ReadWriteLock

读写锁实现

对于读写锁的特点:读写锁在如果多个线程都是在读的时候,是可以并发读的,就是一个无锁的状态,如果有写锁正在操作的时候,那么读锁需要等待写锁。在加写锁的时候,由于前面的读锁都是并发,所以需要监听最后一个读锁完成后执行写锁。步骤如下:

  1. read 请求, 如果前面是读锁,可以直接读取,不需要监听。如果前面是一个或者多个写锁那么只需要监听最后一个写锁。
  2. write 请求,只需要对前面的节点监听。Watcher 机制和互斥锁一样。

分布式锁看了又看,优秀方案我来告诉你

分布式锁实战

本文源码中使用环境:JDK 1.8 、Zookeeper 3.6.x

Curator 组件实现

POM 依赖

  1. <dependency>
  2. <groupId>org.apache.curator</groupId>
  3. <artifactId>curator-framework</artifactId>
  4. <version>2.13.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.curator</groupId>
  8. <artifactId>curator-recipes</artifactId>
  9. <version>2.13.0</version>
  10. </dependency>

互斥锁运用

由于 Zookeeper 非公平锁的 “惊群” 效应,非公平锁在 Zookeeper 中其实并不是最好的选择。下面是一个模拟秒杀的例子来使用 Zookeeper 分布式锁

  1. publicclassMutexTest{
  2. staticExecutorServiceexecutor=Executors.newFixedThreadPool(8);
  3. staticAtomicIntegerstock=newAtomicInteger(3);
  4. publicstaticvoidmain(String[]args)throwsInterruptedException{
  5. CuratorFrameworkclient=getZkClient();
  6. Stringkey="/lock/lockId_111/111";
  7. finalInterProcessMutexmutex=newInterProcessMutex(client,key);
  8. for(inti=0;i<99;i++){
  9. executor.submit(()->{
  10. if(stock.get()<0){
  11. System.err.println("库存不足,直接返回");
  12. return;
  13. }
  14. try{
  15. booleanacquire=mutex.acquire(200,TimeUnit.MILLISECONDS);
  16. if(acquire){
  17. ints=stock.decrementAndGet();
  18. if(s<0){
  19. System.err.println("进入秒杀,库存不足");
  20. }else{
  21. System.out.println("购买成功,剩余库存:"+s);
  22. }
  23. }
  24. }catch(Exceptione){
  25. e.printStackTrace();
  26. }finally{
  27. try{
  28. if(mutex.isAcquiredInThisProcess())
  29. mutex.release();
  30. }catch(Exceptione){
  31. e.printStackTrace();
  32. }
  33. }
  34. });
  35. }
  36. while(true){
  37. if(executor.isTerminated()){
  38. executor.shutdown();
  39. System.out.println("秒杀完毕剩余库存为:"+stock.get());
  40. }
  41. TimeUnit.MILLISECONDS.sleep(100);
  42. }
  43. }
  44. privatestaticCuratorFrameworkgetZkClient(){
  45. StringzkServerAddress="127.0.0.1:2181";
  46. ExponentialBackoffRetryretryPolicy=newExponentialBackoffRetry(1000,3,5000);
  47. CuratorFrameworkzkClient=CuratorFrameworkFactory.builder()
  48. .connectString(zkServerAddress)
  49. .sessionTimeoutMs(5000)
  50. .connectionTimeoutMs(5000)
  51. .retryPolicy(retryPolicy)
  52. .build();
  53. zkClient.start();
  54. returnzkClient;
  55. }
  56. }

读写锁运用

读写锁可以用来保证缓存双写的强一致性的,因为读写锁在多线程读的时候是无锁的, 只有在前面有写锁的时候才会等待写锁完成后访问数据。

  1. publicclassReadWriteLockTest{
  2. staticExecutorServiceexecutor=Executors.newFixedThreadPool(8);
  3. staticAtomicIntegerstock=newAtomicInteger(3);
  4. staticInterProcessMutexreadLock;
  5. staticInterProcessMutexwriteLock;
  6. publicstaticvoidmain(String[]args)throwsInterruptedException{
  7. CuratorFrameworkclient=getZkClient();
  8. Stringkey="/lock/lockId_111/1111";
  9. InterProcessReadWriteLockreadWriteLock=newInterProcessReadWriteLock(client,key);
  10. readLock=readWriteLock.readLock();
  11. writeLock=readWriteLock.writeLock();
  12. for(inti=0;i<16;i++){
  13. executor.submit(()->{
  14. try{
  15. booleanread=readLock.acquire(2000,TimeUnit.MILLISECONDS);
  16. if(read){
  17. intnum=stock.get();
  18. System.out.println("读取库存,当前库存为:"+num);
  19. if(num<0){
  20. System.err.println("库存不足,直接返回");
  21. return;
  22. }
  23. }
  24. }catch(Exceptione){
  25. e.printStackTrace();
  26. }finally{
  27. if(readLock.isAcquiredInThisProcess()){
  28. try{
  29. readLock.release();
  30. }catch(Exceptione){
  31. e.printStackTrace();
  32. }
  33. }
  34. }
  35. try{
  36. booleanacquire=writeLock.acquire(2000,TimeUnit.MILLISECONDS);
  37. if(acquire){
  38. ints=stock.get();
  39. if(s<=0){
  40. System.err.println("进入秒杀,库存不足");
  41. }else{
  42. s=stock.decrementAndGet();
  43. System.out.println("购买成功,剩余库存:"+s);
  44. }
  45. }
  46. }catch(Exceptione){
  47. e.printStackTrace();
  48. }finally{
  49. try{
  50. if(writeLock.isAcquiredInThisProcess())
  51. writeLock.release();
  52. }catch(Exceptione){
  53. e.printStackTrace();
  54. }
  55. }
  56. });
  57. }
  58. while(true){
  59. if(executor.isTerminated()){
  60. executor.shutdown();
  61. System.out.println("秒杀完毕剩余库存为:"+stock.get());
  62. }
  63. TimeUnit.MILLISECONDS.sleep(100);
  64. }
  65. }
  66. privatestaticCuratorFrameworkgetZkClient(){
  67. StringzkServerAddress="127.0.0.1:2181";
  68. ExponentialBackoffRetryretryPolicy=newExponentialBackoffRetry(1000,3,5000);
  69. CuratorFrameworkzkClient=CuratorFrameworkFactory.builder()
  70. .connectString(zkServerAddress)
  71. .sessionTimeoutMs(5000)
  72. .connectionTimeoutMs(5000)
  73. .retryPolicy(retryPolicy)
  74. .build();
  75. zkClient.start();
  76. returnzkClient;
  77. }
  78. }

打印结果如下,一开始会有 8 个输出结果为 读取库存,当前库存为: 3 然后在写锁中回去顺序的扣减少库存。

  1. 读取库存,当前库存为:3
  2. 读取库存,当前库存为:3
  3. 读取库存,当前库存为:3
  4. 读取库存,当前库存为:3
  5. 读取库存,当前库存为:3
  6. 读取库存,当前库存为:3
  7. 读取库存,当前库存为:3
  8. 读取库存,当前库存为:3
  9. 购买成功,剩余库存:2
  10. 购买成功,剩余库存:1
  11. 购买成功,剩余库存:0
  12. 进入秒杀,库存不足
  13. 进入秒杀,库存不足
  14. 进入秒杀,库存不足
  15. 进入秒杀,库存不足
  16. 进入秒杀,库存不足
  17. 读取库存,当前库存为:0
  18. 读取库存,当前库存为:0
  19. 读取库存,当前库存为:0
  20. 读取库存,当前库存为:0
  21. 读取库存,当前库存为:0
  22. 读取库存,当前库存为:0
  23. 读取库存,当前库存为:0
  24. 读取库存,当前库存为:0
  25. 进入秒杀,库存不足
  26. 进入秒杀,库存不足
  27. 进入秒杀,库存不足
  28. 进入秒杀,库存不足
  29. 进入秒杀,库存不足
  30. 进入秒杀,库存不足
  31. 进入秒杀,库存不足
  32. 进入秒杀,库存不足

分布式锁的选择

咱们最常用的就是 Redis 的分布式锁Zookeeper分布式锁,在性能方面 Redis 的每秒钟 TPS 可以上轻松上万。在大规模的高并发场景我推荐使用 Redis 分布式锁来作为推荐的技术方案。如果对并发要求不是特别高的场景可以使用 Zookeeper 分布式来处理。

参考资料

https://www.cnblogs.com/leeego-123/p/12162220.html

http://curator.apache.org/

https://blog.csdn.net/hosaos/article/details/89521537

原文地址:https://mp.weixin.qq.com/s/k1HR_2rUWUe-ARN7KzhlYg

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 分布式锁看了又看,优秀方案我来告诉你 https://www.kuaiidc.com/94089.html

相关文章

发表评论
暂无评论