当前位置:   article > 正文

分布式系统技术——分布式锁原理与实战_分布式高并发抢锁原理

分布式高并发抢锁原理

摘要

分布式应用进行逻辑处理时经常会遇到并发问题。比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题, 因为读取和保存状态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始, 就一直运行到结束,中间不会有任何 context switch 线程切换。)这个时候就要使用到分布式锁来限制程序并发的执行。本博文主要是的介绍分布式锁的原理和应用场景。

一、分布式锁实现方式

锁是一种常用的并发控制机制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。

锁指的是程序级别的锁,例如 Java 语言中的 synchronized 和 ReentrantLock 在单应用中使用不会有任何问题, 但如果放到分布式环境下就不适用了,这个时候我们就要使用分布式锁。分布式锁比较好理解就是用于分布式环境下并发控制的一种机制, 用于控制某个资源在同一时刻只能被一个应用所使用。分布式锁比较常见的实现方式有三种:

  1. 基于数据库分布式锁实现。
  2. 基于Redis实现的分布式锁。
  3. 基于ZooKeeper实现的分布式锁:使用ZooKeeper顺序临时节点来实现分布式锁。

二、MySQL分布式锁

三、Redis分布式锁

3.1 单机系统的数据一致性问题

场景描述:客户端模拟购买商品过程,在Redis中设定库存总数剩100个,多个客户端同时并发购买。

  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. /**
  7. * @description
  8. * 最简单的情况,没有加任何的考虑,
  9. * 即使是单体应用,并发情况下数据一致性都有问题
  10. * @param: null
  11. * @date: 2022/4/9 21:25
  12. * @return:
  13. * @author: xjl
  14. */
  15. @RestController
  16. public class NoneController {
  17. @Autowired
  18. StringRedisTemplate template;
  19. @RequestMapping("/buy")
  20. public String index() {
  21. // Redis中存有goods:001号商品,数量为100 相当于是的redis中的get("goods")的操作。
  22. String result = template.opsForValue().get("goods");
  23. // 获取到剩余商品数
  24. int total = result == null ? 0 : Integer.parseInt(result);
  25. if (total > 0) {
  26. // 剩余商品数大于0 ,则进行扣减
  27. int realTotal = total - 1;
  28. // 将商品数回写数据库 相当于设置新的值的结果
  29. template.opsForValue().set("goods", String.valueOf(realTotal));
  30. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  31. return "购买商品成功,库存还剩:" + realTotal + "件";
  32. } else {
  33. System.out.println("购买商品失败");
  34. }
  35. return "购买商品失败";
  36. }
  37. }

使用Jmeter模拟高并发场景,测试结果如下:

测试结果出现多个用户购买同一商品,发生了数据不一致问题!解决办法:单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性:

  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.concurrent.locks.Lock;
  7. import java.util.concurrent.locks.ReentrantLock;
  8. /**
  9. * @description 单体应用的情况下,对并发的操作进行加锁操作,保证对数据的操作具有原子性
  10. * 1. synchronized
  11. * 2. ReentrantLock
  12. * 这种情况下,不会产生并发问题
  13. * @param: null
  14. * @date: 2022/4/9 21:25
  15. * @return:
  16. * @author: xjl
  17. */
  18. @RestController
  19. public class ReentrantLockController {
  20. // 引入的ReentrantLock 锁机制
  21. Lock lock = new ReentrantLock();
  22. @Autowired
  23. StringRedisTemplate template;
  24. @RequestMapping("/buy")
  25. public String index() {
  26. // 加锁
  27. lock.lock();
  28. try {
  29. // Redis中存有goods:001号商品,数量为100 相当于是的redis中的get("goods")的操作。
  30. String result = template.opsForValue().get("goods");
  31. // 获取到剩余商品数
  32. int total = result == null ? 0 : Integer.parseInt(result);
  33. if (total > 0) {
  34. int realTotal = total - 1;
  35. // 将商品数回写数据库 相当于设置新的值的结果
  36. template.opsForValue().set("goods", String.valueOf(realTotal));
  37. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  38. return "购买商品成功,库存还剩:" + realTotal + "件";
  39. } else {
  40. System.out.println("购买商品失败");
  41. }
  42. } catch (Exception e) {
  43. //解锁
  44. lock.unlock();
  45. } finally {
  46. //解锁
  47. lock.unlock();
  48. }
  49. return "购买商品失败";
  50. }
  51. }

3.2 分布式数据一致性问题

上面解决了单体应用的数据一致性问题,但如果是分布式架构部署呢,架构如下:提供两个服务,端口分别为8001、8002,连接同一个Redis服务,在服务前面有一台Nginx作为负载均衡。两台服务代码相同,只是端口不同。

将8001、8002两个服务启动,每个服务依然用ReentrantLock加锁,用Jmeter做并发测试,发现会出现数据一致性问题!

4.3 Redis来实现分布式锁

  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.UUID;
  7. /**
  8. * @description 面使用redis的set命令来实现加锁
  9. * 1.SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
  10. * EX seconds − 设置指定的到期时间(以秒为单位)。
  11. * PX milliseconds - 设置指定的到期时间(以毫秒为单位)。
  12. * NX - 仅在键不存在时设置键。
  13. * XX - 只有在键已存在时才设置。
  14. * @param: null
  15. * @date: 2022/4/9 21:25
  16. * @return:
  17. * @author: xjl
  18. */
  19. @RestController
  20. public class RedisLockControllerV1 {
  21. public static final String REDIS_LOCK = "good_lock";
  22. @Autowired
  23. StringRedisTemplate template;
  24. @RequestMapping("/buy")
  25. public String index() {
  26. // 每个人进来先要进行加锁,key值为"good_lock"
  27. String value = UUID.randomUUID().toString().replace("-", "");
  28. try {
  29. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
  30. // 加锁失败
  31. if (!flag) {
  32. return "抢锁失败!";
  33. }
  34. System.out.println(value + " 抢锁成功");
  35. String result = template.opsForValue().get("goods");
  36. int total = result == null ? 0 : Integer.parseInt(result);
  37. if (total > 0) {
  38. int realTotal = total - 1;
  39. template.opsForValue().set("goods", String.valueOf(realTotal));
  40. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  41. return "购买商品成功,库存还剩:" + realTotal + "件";
  42. } else {
  43. System.out.println("购买商品失败");
  44. }
  45. return "购买商品失败";
  46. } finally {
  47. // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理 template.delete(REDIS_LOCK);
  48. template.delete(REDIS_LOCK);
  49. }
  50. }
  51. }

4.4 Redis设置过期时间

如果程序在运行期间,部署了微服务jar包的机器突然挂了,代码层面根本就没有走到finally代码块,也就是说在宕机前,锁并没有被删除掉,这样的话,就没办法保证解锁,所以,这里需要对这个key加一个过期时间,Redis中设置过期时间有两种方法:

  • template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
  • template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)

第一种方法需要单独的一行代码,且并没有与加锁放在同一步操作,所以不具备原子性,也会出问题, 第二种方法在加锁的同时就进行了设置过期时间,所有没有问题,这里采用这种方式。

  1. // 为key加一个过期时间,其余代码不变
  2. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);
  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.UUID;
  7. import java.util.concurrent.TimeUnit;
  8. /**
  9. * @description 在第四种情况下,如果在程序运行期间,部署了微服务的jar包的机器突然挂了,代码层面根本就没有走到finally代码块
  10. * 没办法保证解锁,所以这个key就没有被删除
  11. * 这里需要对这个key加一个过期时间,设置过期时间有两种方法
  12. * 1. template.expire(REDIS_LOCK,10, TimeUnit.SECONDS);第一种方法需要单独的一行代码,并没有与加锁放在同一步操作,所以不具备原子性,也会出问题
  13. * 2. template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);第二种方法在加锁的同时就进行了设置过期时间,所有没有问题
  14. * @date: 2022/4/9 21:25
  15. * @return:
  16. * @author: xjl
  17. */
  18. @RestController
  19. public class RedisLockControllerV2 {
  20. public static final String REDIS_LOCK = "good_lock";
  21. @Autowired
  22. StringRedisTemplate template;
  23. @RequestMapping("/buy")
  24. public String index() {
  25. // 每个人进来先要进行加锁,key值为"good_lock"
  26. String value = UUID.randomUUID().toString().replace("-", "");
  27. try {
  28. // 为key加一个过期时间 10s
  29. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
  30. // 加锁失败
  31. if (!flag) {
  32. return "抢锁失败!";
  33. }
  34. System.out.println(value + " 抢锁成功");
  35. String result = template.opsForValue().get("goods");
  36. int total = result == null ? 0 : Integer.parseInt(result);
  37. if (total > 0) {
  38. int realTotal = total - 1;
  39. template.opsForValue().set("goods", String.valueOf(realTotal));
  40. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  41. return "购买商品成功,库存还剩:" + realTotal + "件";
  42. } else {
  43. System.out.println("购买商品失败");
  44. }
  45. return "购买商品失败";
  46. } finally {
  47. // 如果在抢到所之后,删除锁之前,发生了异常,锁就无法被释放,所以要在finally处理 template.delete(REDIS_LOCK);
  48. template.delete(REDIS_LOCK);
  49. }
  50. }
  51. }

4.5 Redis设置锁的删除

设置了key的过期时间,解决了key无法删除的问题,但问题又来了,上面设置了key的过期时间为10秒,如果业务逻辑比较复杂,需要调用其他微服务, 处理时间需要15秒(模拟场景,别较真),而当10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key, 此时如果耗时15秒的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题!所以,谁上的锁,谁才能删除。

  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.data.redis.core.StringRedisTemplate;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import java.util.UUID;
  7. import java.util.concurrent.TimeUnit;
  8. /**
  9. * @description
  10. * 在第五种情况下,设置了key的过期时间,解决了key无法删除的问题,但问题又来了
  11. * 我们设置了key的过期时间为10秒,如果我们的业务逻辑比较复杂,需要调用其他微服务,需要15秒
  12. * 10秒钟过去之后,这个key就过期了,其他请求就又可以设置这个key了
  13. * 但是如果耗时的请求处理完了,回来继续执行程序,就会把别人设置的key给删除了,这是个很严重的问题
  14. * 所以,谁上的锁,谁才能删除
  15. * @date: 2022/4/9 21:25
  16. * @return:
  17. * @author: xjl
  18. */
  19. @RestController
  20. public class RedislockControllerV3 {
  21. public static final String REDIS_LOCK = "good_lock";
  22. @Autowired
  23. StringRedisTemplate template;
  24. @RequestMapping("/buy")
  25. public String index() {
  26. // 每个人进来先要进行加锁,key值为"good_lock"
  27. String value = UUID.randomUUID().toString().replace("-", "");
  28. try {
  29. // 为key加一个过期时间10s
  30. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
  31. // 加锁失败
  32. if (!flag) {
  33. return "抢锁失败!";
  34. }
  35. System.out.println(value + " 抢锁成功");
  36. String result = template.opsForValue().get("goods");
  37. int total = result == null ? 0 : Integer.parseInt(result);
  38. if (total > 0) {
  39. // 如果在此处需要调用其他微服务,处理时间较长。。。
  40. int realTotal = total - 1;
  41. template.opsForValue().set("goods", String.valueOf(realTotal));
  42. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  43. return "购买商品成功,库存还剩:" + realTotal + "件";
  44. } else {
  45. System.out.println("购买商品失败");
  46. }
  47. return "购买商品失败";
  48. } finally {
  49. // 谁加的锁,谁才能删除
  50. if (template.opsForValue().get(REDIS_LOCK).equals(value)) {
  51. template.delete(REDIS_LOCK);
  52. }
  53. }
  54. }
  55. }

4.6 Redis中Lua原子操作

规定了谁上的锁,谁才能删除,但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题,并发嘛,就是要保证数据的一致性,保证数据的一致性, 最好要保证对数据的操作具有原子性。在redis中的保证原子操作的是

  1. 使用Lua脚本,进行锁的删除
  2. 使用Redis事务来实现原子操作
  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import com.zhuangxiaoyan.springbootredis.utils.RedisUtils;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.data.redis.core.StringRedisTemplate;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import redis.clients.jedis.Jedis;
  8. import java.util.Collections;
  9. import java.util.List;
  10. import java.util.UUID;
  11. import java.util.concurrent.TimeUnit;
  12. /**
  13. * @description 在第六种情况下,规定了谁上的锁,谁才能删除
  14. * 但finally快的判断和del删除操作不是原子操作,并发的时候也会出问题
  15. * 并发就是要保证数据的一致性,保证数据的一致性,最好要保证对数据的操作具有原子性
  16. * @param: null
  17. * @date: 2022/4/9 21:25
  18. * @return:
  19. * @author: xjl
  20. */
  21. @RestController
  22. public class RedisLockControllerV4 {
  23. public static final String REDIS_LOCK = "good_lock";
  24. @Autowired
  25. StringRedisTemplate template;
  26. /**
  27. * @description 使用Lua脚本,进行锁的删除
  28. * @param:
  29. * @date: 2022/4/9 21:56
  30. * @return: java.lang.String
  31. * @author: xjl
  32. */
  33. @RequestMapping("/buy")
  34. public String index() {
  35. // 每个人进来先要进行加锁,key值为"good_lock"
  36. String value = UUID.randomUUID().toString().replace("-", "");
  37. try {
  38. // 为key加一个过期时间
  39. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
  40. // 加锁失败
  41. if (!flag) {
  42. return "抢锁失败!";
  43. }
  44. System.out.println(value + " 抢锁成功");
  45. String result = template.opsForValue().get("goods");
  46. int total = result == null ? 0 : Integer.parseInt(result);
  47. if (total > 0) {
  48. // 如果在此处需要调用其他微服务,处理时间较长。。。
  49. int realTotal = total - 1;
  50. template.opsForValue().set("goods", String.valueOf(realTotal));
  51. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  52. return "购买商品成功,库存还剩:" + realTotal + "件";
  53. } else {
  54. System.out.println("购买商品失败");
  55. }
  56. return "购买商品失败,服务端口为8001";
  57. } finally {
  58. // 谁加的锁,谁才能删除 使用Lua脚本,进行锁的删除
  59. Jedis jedis = null;
  60. try {
  61. jedis = RedisUtils.getJedis();
  62. String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
  63. "then " +
  64. "return redis.call('del',KEYS[1]) " +
  65. "else " +
  66. " return 0 " +
  67. "end";
  68. Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
  69. if ("1".equals(eval.toString())) {
  70. System.out.println("-----del redis lock ok....");
  71. } else {
  72. System.out.println("-----del redis lock error ....");
  73. }
  74. } catch (Exception e) {
  75. System.out.println(e.getMessage());
  76. } finally {
  77. if (null != jedis) {
  78. jedis.close();
  79. }
  80. }
  81. }
  82. }
  83. /**
  84. * @description 使用redis事务
  85. * @param:
  86. * @date: 2022/4/9 21:56
  87. * @return: java.lang.String
  88. * @author: xjl
  89. */
  90. @RequestMapping("/buy2")
  91. public String index2() {
  92. // 每个人进来先要进行加锁,key值为"good_lock"
  93. String value = UUID.randomUUID().toString().replace("-", "");
  94. try {
  95. // 为key加一个过期时间
  96. Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value, 10L, TimeUnit.SECONDS);
  97. // 加锁失败
  98. if (!flag) {
  99. return "抢锁失败!";
  100. }
  101. System.out.println(value + " 抢锁成功");
  102. String result = template.opsForValue().get("goods");
  103. int total = result == null ? 0 : Integer.parseInt(result);
  104. if (total > 0) {
  105. // 如果在此处需要调用其他微服务,处理时间较长。。。
  106. int realTotal = total - 1;
  107. template.opsForValue().set("goods", String.valueOf(realTotal));
  108. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  109. return "购买商品成功,库存还剩:" + realTotal + "件";
  110. } else {
  111. System.out.println("购买商品失败");
  112. }
  113. return "购买商品失败,服务端口为8001";
  114. } finally {
  115. // 谁加的锁,谁才能删除 ,使用redis事务
  116. while (true) {
  117. template.watch(REDIS_LOCK);
  118. if (template.opsForValue().get(REDIS_LOCK).equalsIgnoreCase(value)) {
  119. template.setEnableTransactionSupport(true);
  120. template.multi();
  121. template.delete(REDIS_LOCK);
  122. List<Object> list = template.exec();
  123. if (list == null) {
  124. continue;
  125. }
  126. }
  127. template.unwatch();
  128. break;
  129. }
  130. }
  131. }
  132. }

4.7 Redis集群实现分布式锁

规定了谁上的锁,谁才能删除,并且解决了删除操作没有原子性问题。但还没有考虑缓存续命,以及Redis集群部署下,异步复制造成的锁丢失: 主节点没来得及把刚刚set进来这条数据给从节点,就挂了。所以直接上RedLock的Redisson落地实现。

  1. package com.zhuangxiaoyan.springbootredis.controller;
  2. import org.redisson.Redisson;
  3. import org.redisson.api.RLock;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.data.redis.core.StringRedisTemplate;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. import java.util.UUID;
  9. /**
  10. * @description
  11. * 在第六种情况下,规定了谁上的锁,谁才能删除
  12. * 1. 缓存续命
  13. * 2. redis异步复制造成的锁丢失:主节点没来得及把刚刚set进来这条数据给从节点,就挂了
  14. * @param: null
  15. * @date: 2022/4/9 21:25
  16. * @return:
  17. * @author: xjl
  18. */
  19. @RestController
  20. public class RedisLockControllerV5 {
  21. public static final String REDIS_LOCK = "good_lock";
  22. @Autowired
  23. StringRedisTemplate template;
  24. @Autowired
  25. Redisson redisson;
  26. @RequestMapping("/buy")
  27. public String index() {
  28. RLock lock = redisson.getLock(REDIS_LOCK);
  29. lock.lock();
  30. // 每个人进来先要进行加锁,key值为"good_lock"
  31. String value = UUID.randomUUID().toString().replace("-", "");
  32. try {
  33. String result = template.opsForValue().get("goods");
  34. int total = result == null ? 0 : Integer.parseInt(result);
  35. if (total > 0) {
  36. // 如果在此处需要调用其他微服务,处理时间较长。。。
  37. int realTotal = total - 1;
  38. template.opsForValue().set("goods", String.valueOf(realTotal));
  39. System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
  40. return "购买商品成功,库存还剩:" + realTotal + "件";
  41. } else {
  42. System.out.println("购买商品失败");
  43. }
  44. return "购买商品失败";
  45. } finally {
  46. // 如果锁依旧在同时还是在被当前线程持有,那就解锁。 如果是其他的线程持有 那就不能释放锁资源
  47. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  48. lock.unlock();
  49. }
  50. }
  51. }
  52. }

四、Zookeeper分布式锁

五、分布式锁总结

参考博文

系统设计解决方案/1-分布式锁方案/分布式锁解决方案.md · 庄小焱/SeniorArchitect - Gitee.com

如何用Redis实现分布式锁? - 掘金

面试官:怎么用Zk(Zookeeper)实现实现分布式锁呀? - 掘金

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号