You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 7.8 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. # spring-boot-demo-distributed-lock-redis
  2. > 此 demo 主要演示了 Spring Boot 如何基于 RedisTemplate 实现一个分布式锁,支持集群部署、支持可重入、支持自动续期等功能
  3. ## 1.开发步骤
  4. 在 `demo-distributed-lock-api` 模块中,已经实现了基于 AOP 的分布式锁注解拦截、简单的扣减库存案例,因此本模块只需要实现以下两个接口即可。
  5. - `com.xkcoding.distributed.lock.api.DistributedLock`
  6. - `com.xkcoding.distributed.lock.api.DistributedLockClient`
  7. ### 1.1.添加依赖
  8. ```xml
  9. <dependencies>
  10. <dependency>
  11. <groupId>com.xkcoding</groupId>
  12. <artifactId>demo-distributed-lock-api</artifactId>
  13. <version>1.0.0-SNAPSHOT</version>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-data-redis</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.projectlombok</groupId>
  21. <artifactId>lombok</artifactId>
  22. <optional>true</optional>
  23. </dependency>
  24. </dependencies>
  25. ```
  26. ### 1.2.代码实现
  27. #### 1.2.1.RedisDistributedLock
  28. > 基于 RedisTemplate 实现分布式锁,主要是通过 Lua 脚本保证原子性操作、通过 hash 数据结构实现可重入性
  29. ```java
  30. public class RedisDistributedLock extends DistributedLock {
  31. private final StringRedisTemplate redisTemplate;
  32. /**
  33. * 锁的唯一标识,格式:主机标识(UUID)+线程编号
  34. * 防误删
  35. */
  36. private final String uniqueId;
  37. protected RedisDistributedLock(StringRedisTemplate redisTemplate, String uniqueIdPrefix, String lockKey, long lockTime,
  38. TimeUnit timeUnit) {
  39. super(lockKey, lockTime, timeUnit);
  40. this.redisTemplate = redisTemplate;
  41. this.uniqueId = uniqueIdPrefix + ":" + Thread.currentThread().getId();
  42. }
  43. @Override
  44. public void lock() {
  45. // 加锁失败,自旋阻塞
  46. while (!tryLock()) {
  47. try {
  48. TimeUnit.MILLISECONDS.sleep(50);
  49. } catch (InterruptedException e) {
  50. throw new RuntimeException(e);
  51. }
  52. }
  53. }
  54. @Override
  55. public boolean tryLock() {
  56. try {
  57. return tryLock(lockTime, timeUnit);
  58. } catch (InterruptedException e) {
  59. return false;
  60. }
  61. }
  62. @Override
  63. public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
  64. String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
  65. "then " +
  66. " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
  67. " redis.call('expire', KEYS[1], ARGV[2]) " +
  68. " return 1 " +
  69. "else " +
  70. " return 0 " +
  71. "end";
  72. long expire = unit.toSeconds(time);
  73. Boolean getLock = Optional.ofNullable(
  74. redisTemplate.execute(
  75. new DefaultRedisScript<>(script, Boolean.class),
  76. Collections.singletonList(lockKey),
  77. uniqueId, String.valueOf(expire)))
  78. .orElse(Boolean.FALSE);
  79. // 如果获得锁,开启自动续期
  80. if (getLock) {
  81. renewLockTime(time, unit);
  82. }
  83. return getLock;
  84. }
  85. private void renewLockTime(long time, TimeUnit unit) {
  86. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
  87. "then " +
  88. " return redis.call('expire', KEYS[1], ARGV[2]) " +
  89. "else " +
  90. " return 0 " +
  91. "end";
  92. long expire = unit.toSeconds(time);
  93. new Timer().schedule(new TimerTask() {
  94. @Override
  95. public void run() {
  96. Boolean renewed = Optional.ofNullable(
  97. redisTemplate.execute(
  98. new DefaultRedisScript<>(script, Boolean.class),
  99. Collections.singletonList(lockKey),
  100. uniqueId, String.valueOf(expire))
  101. ).orElse(Boolean.FALSE);
  102. // 续期成功,代表未被解锁,则需要进行下一次续期
  103. if (renewed) {
  104. renewLockTime(time, unit);
  105. }
  106. }
  107. }, expire * 1000 / 3);
  108. }
  109. @Override
  110. public void unlock() {
  111. String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
  112. "then " +
  113. " return nil " +
  114. "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
  115. "then " +
  116. " return redis.call('del', KEYS[1]) " +
  117. "else " +
  118. " redis.call('expire', KEYS[1], ARGV[2]) " +
  119. " return 0 " +
  120. "end";
  121. // 如果解锁,发现是重入的,需要重新续期
  122. long expire = timeUnit.toSeconds(lockTime);
  123. Long flag = this.redisTemplate.execute(
  124. new DefaultRedisScript<>(script, Long.class),
  125. Collections.singletonList(lockKey),
  126. uniqueId, String.valueOf(expire)
  127. );
  128. if (flag == null) {
  129. throw new IllegalMonitorStateException("this lock doesn't belong to you!");
  130. }
  131. }
  132. }
  133. ```
  134. #### 1.2.2.RedisDistributedLockClient
  135. > 获取一把 RedisTemplate 分布式锁,这里需要注意集群部署下,通过 uuid 标识当前应用唯一 id
  136. ```java
  137. public class RedisDistributedLockClient implements DistributedLockClient {
  138. private final StringRedisTemplate redisTemplate;
  139. /**
  140. * 唯一标识前缀,用于集群环境下的主机标识,会在 Bean 初始化到 Spring 容器的时候设置
  141. */
  142. private final String uniqueIdPrefix;
  143. public RedisDistributedLockClient(StringRedisTemplate redisTemplate) {
  144. this.redisTemplate = redisTemplate;
  145. uniqueIdPrefix = UUID.randomUUID().toString();
  146. }
  147. /**
  148. * 获取一把锁
  149. *
  150. * @param lockKey 锁的标识
  151. * @param lockTime 锁的时间
  152. * @param timeUnit 锁的时间单位
  153. * @return 锁
  154. */
  155. @Override
  156. public DistributedLock getLock(String lockKey, long lockTime, TimeUnit timeUnit) {
  157. return new RedisDistributedLock(redisTemplate, uniqueIdPrefix, lockKey, lockTime, timeUnit);
  158. }
  159. }
  160. ```
  161. #### 1.2.3.自动装配
  162. > 替换 `demo-distributed-lock-api` 中的默认实现
  163. ```java
  164. @Configuration(proxyBeanMethods = false)
  165. public class RedisDistributedLockAutoConfiguration {
  166. @Bean
  167. public RedisDistributedLockClient distributedLockClient(StringRedisTemplate redisTemplate) {
  168. return new RedisDistributedLockClient(redisTemplate);
  169. }
  170. }
  171. ```
  172. ## 2.测试
  173. ### 2.1.环境搭建
  174. 主要是 mysql 及 redis 环境的搭建,这里我提供了 docker-compose 文件,方便同学们一键启动测试环境
  175. ```bash
  176. $ cd demo-distributed-lock/demo-distributed-lock-redis
  177. $ docker compose -f docker-compose.env.yml up
  178. ```
  179. ### 2.2.测试流程
  180. 这里我通过 Apache Bench 进行模拟并发场景,我也构建了一个压测镜像 `xkcoding/ab:alpine-3.16.2` ,方便同学们进行快速测试
  181. > 注意:每次启动项目,都会在重置库存,你也可以手动调用 `/demo/stock/reset` 接口重置
  182. #### 2.2.1.测试无分布式锁下高并发请求是否会发生超卖
  183. 1. 把 `RedisDistributedLockAutoConfiguration` 类全部注释掉,这将不会装配 RedisTemplate 分布式锁
  184. 2. 启动 `RedisDistributedLockApplication`
  185. 3. 模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
  186. 4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
  187. #### 2.2.2.测试 RedisTemplate 分布式锁
  188. 1. 把 `RedisDistributedLockAutoConfiguration` 类的注释解开,此时 Spring Boot 会自动装配我们的 RedisTemplate 分布式锁
  189. 2. 再次启动 `RedisDistributedLockApplication`
  190. 3. 再次模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
  191. 4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
  192. ## 3.参考
  193. - [Spring Data Redis 官方文档之调用 lua 脚本](https://docs.spring.io/spring-data/redis/docs/3.0.0-M5/reference/html/#scripting)
  194. - [lua 脚本语法](https://www.runoob.com/lua/lua-tutorial.html)