diff --git a/demo-distributed-lock/demo-distributed-lock-redis/README.md b/demo-distributed-lock/demo-distributed-lock-redis/README.md
new file mode 100644
index 0000000..f522819
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/README.md
@@ -0,0 +1,240 @@
+## spring-boot-demo-distributed-lock-redis
+
+> 此 demo 主要演示了 Spring Boot 如何基于 RedisTemplate 实现一个分布式锁
+
+## 1.开发步骤
+
+在 `demo-distributed-lock-api` 模块中,已经实现了基于 AOP 的分布式锁注解拦截、简单的扣减库存案例,因此本模块只需要实现以下两个接口即可。
+- `com.xkcoding.distributed.lock.api.DistributedLock`
+- `com.xkcoding.distributed.lock.api.DistributedLockClient`
+
+### 1.1.添加依赖
+
+```xml
+
+
+ com.xkcoding
+ demo-distributed-lock-api
+ 1.0.0-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+```
+
+### 1.2.代码实现
+
+#### 1.2.1.RedisDistributedLock
+
+> 基于 RedisTemplate 实现分布式锁,主要是通过 Lua 脚本保证原子性操作、通过 hash 数据结构实现可重入性
+
+```java
+public class RedisDistributedLock extends DistributedLock {
+ private final StringRedisTemplate redisTemplate;
+ /**
+ * 锁的唯一标识,格式:主机标识(UUID)+线程编号
+ * 防误删
+ */
+ private final String uniqueId;
+
+ protected RedisDistributedLock(StringRedisTemplate redisTemplate, String uniqueIdPrefix, String lockKey, long lockTime,
+ TimeUnit timeUnit) {
+ super(lockKey, lockTime, timeUnit);
+ this.redisTemplate = redisTemplate;
+ this.uniqueId = uniqueIdPrefix + ":" + Thread.currentThread().getId();
+ }
+
+ @Override
+ public void lock() {
+ // 加锁失败,自旋阻塞
+ while (!tryLock()) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(50);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean tryLock() {
+ try {
+ return tryLock(lockTime, timeUnit);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
+ String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
+ "then " +
+ " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
+ " redis.call('expire', KEYS[1], ARGV[2]) " +
+ " return 1 " +
+ "else " +
+ " return 0 " +
+ "end";
+
+ long expire = unit.toSeconds(time);
+ Boolean getLock = Optional.ofNullable(
+ redisTemplate.execute(
+ new DefaultRedisScript<>(script, Boolean.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire)))
+ .orElse(Boolean.FALSE);
+ // 如果获得锁,开启自动续期
+ if (getLock) {
+ renewLockTime(time, unit);
+ }
+ return getLock;
+ }
+
+ private void renewLockTime(long time, TimeUnit unit) {
+ String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
+ "then " +
+ " return redis.call('expire', KEYS[1], ARGV[2]) " +
+ "else " +
+ " return 0 " +
+ "end";
+
+ long expire = unit.toSeconds(time);
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Boolean renewed = Optional.ofNullable(
+ redisTemplate.execute(
+ new DefaultRedisScript<>(script, Boolean.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire))
+ ).orElse(Boolean.FALSE);
+ // 续期成功,代表未被解锁,则需要进行下一次续期
+ if (renewed) {
+ renewLockTime(time, unit);
+ }
+ }
+ }, expire * 1000 / 3);
+ }
+
+ @Override
+ public void unlock() {
+ String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
+ "then " +
+ " return nil " +
+ "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
+ "then " +
+ " return redis.call('del', KEYS[1]) " +
+ "else " +
+ " redis.call('expire', KEYS[1], ARGV[2]) " +
+ " return 0 " +
+ "end";
+
+ // 如果解锁,发现是重入的,需要重新续期
+ long expire = timeUnit.toSeconds(lockTime);
+ Long flag = this.redisTemplate.execute(
+ new DefaultRedisScript<>(script, Long.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire)
+ );
+ if (flag == null) {
+ throw new IllegalMonitorStateException("this lock doesn't belong to you!");
+ }
+ }
+
+}
+```
+
+#### 1.2.2.RedisDistributedLockClient
+
+> 获取一把 RedisTemplate 分布式锁,这里需要注意集群部署下,通过 uuid 标识当前应用唯一 id
+
+```java
+public class RedisDistributedLockClient implements DistributedLockClient {
+ private final StringRedisTemplate redisTemplate;
+
+ /**
+ * 唯一标识前缀,用于集群环境下的主机标识,会在 Bean 初始化到 Spring 容器的时候设置
+ */
+ private final String uniqueIdPrefix;
+
+ public RedisDistributedLockClient(StringRedisTemplate redisTemplate) {
+ this.redisTemplate = redisTemplate;
+ uniqueIdPrefix = UUID.randomUUID().toString();
+ }
+
+
+ /**
+ * 获取一把锁
+ *
+ * @param lockKey 锁的标识
+ * @param lockTime 锁的时间
+ * @param timeUnit 锁的时间单位
+ * @return 锁
+ */
+ @Override
+ public DistributedLock getLock(String lockKey, long lockTime, TimeUnit timeUnit) {
+ return new RedisDistributedLock(redisTemplate, uniqueIdPrefix, lockKey, lockTime, timeUnit);
+ }
+}
+```
+
+#### 1.2.3.自动装配
+
+> 替换 `demo-distributed-lock-api` 中的默认实现
+
+```java
+@Configuration(proxyBeanMethods = false)
+public class RedisDistributedLockAutoConfiguration {
+
+ @Bean
+ public RedisDistributedLockClient distributedLockClient(StringRedisTemplate redisTemplate) {
+ return new RedisDistributedLockClient(redisTemplate);
+ }
+
+}
+```
+
+## 2.测试
+
+### 2.1.环境搭建
+
+主要是 mysql 及 redis 环境的搭建,这里我提供了 docker-compose 文件,方便同学们一键启动测试环境
+
+```bash
+$ cd demo-distributed-lock/demo-distributed-lock-redis
+$ docker compose -f docker-compose.env.yml up
+```
+
+### 2.2.测试流程
+
+这里我通过 Apache Bench 进行模拟并发场景,我也构建了一个压测镜像 `xkcoding/ab:alpine-3.16.2` ,方便同学们进行快速测试
+
+> 注意:每次启动项目,都会在重置库存,你也可以手动调用 `/demo/stock/reset` 接口重置
+
+#### 2.2.1.测试无分布式锁下高并发请求是否会发生超卖
+
+1. 把 `RedisDistributedLockAutoConfiguration` 类全部注释掉,这将不会装配 Redisson 分布式锁
+2. 启动 `RedisDistributedLockApplication`
+3. 模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
+4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
+
+#### 2.2.2.测试 Redisson 分布式锁
+
+1. 把 `RedisDistributedLockAutoConfiguration` 类的注释解开,此时 Spring Boot 会自动装配我们的 Redisson 分布式锁
+2. 再次启动 `RedisDistributedLockApplication`
+3. 再次模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
+4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
+
+## 3.参考
+
+- [Spring Data Redis 官方文档之调用 lua 脚本](https://docs.spring.io/spring-data/redis/docs/3.0.0-M5/reference/html/#scripting)
+- [lua 脚本语法](https://www.runoob.com/lua/lua-tutorial.html)
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/docker-compose.env.yml b/demo-distributed-lock/demo-distributed-lock-redis/docker-compose.env.yml
new file mode 100644
index 0000000..cac8254
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/docker-compose.env.yml
@@ -0,0 +1,14 @@
+version: "3.8"
+
+services:
+ mysql:
+ image: mysql:8.0.30
+ ports:
+ - "3306:3306"
+ environment:
+ - MYSQL_ROOT_PASSWORD=root
+ - MYSQL_DATABASE=spring-boot-demo
+ redis:
+ image: redis:7.0.4-alpine
+ ports:
+ - "6379:6379"
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/pom.xml b/demo-distributed-lock/demo-distributed-lock-redis/pom.xml
index 35f7e5c..8952b03 100644
--- a/demo-distributed-lock/demo-distributed-lock-redis/pom.xml
+++ b/demo-distributed-lock/demo-distributed-lock-redis/pom.xml
@@ -16,4 +16,23 @@
17
+
+
+ com.xkcoding
+ demo-distributed-lock-api
+ 1.0.0-SNAPSHOT
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
diff --git a/demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/RedisDistributedLockApplication.java
similarity index 56%
rename from demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java
rename to demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/RedisDistributedLockApplication.java
index 766fbcf..af177ec 100644
--- a/demo-zookeeper/src/main/java/com/xkcoding/zookeeper/SpringBootDemoZookeeperApplication.java
+++ b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/RedisDistributedLockApplication.java
@@ -1,4 +1,4 @@
-package com.xkcoding.zookeeper;
+package com.xkcoding.distributed.lock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -9,14 +9,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
*
*
* @author yangkai.shen
- * @date Created in 2018-12-27 14:51
+ * @date 2022-09-03 12:23
*/
@SpringBootApplication
-public class SpringBootDemoZookeeperApplication {
+public class RedisDistributedLockApplication {
public static void main(String[] args) {
- SpringApplication.run(SpringBootDemoZookeeperApplication.class, args);
+ SpringApplication.run(RedisDistributedLockApplication.class, args);
}
}
-
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLock.java b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLock.java
new file mode 100644
index 0000000..d6d2c34
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLock.java
@@ -0,0 +1,134 @@
+package com.xkcoding.distributed.lock.autoconfigure;
+
+import com.xkcoding.distributed.lock.api.DistributedLock;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * 基于 RedisTemplate 实现的分布式锁
+ *
+ *
+ * @author yangkai.shen
+ * @date 2022-09-03 12:33
+ */
+public class RedisDistributedLock extends DistributedLock {
+ private final StringRedisTemplate redisTemplate;
+ /**
+ * 锁的唯一标识,格式:主机标识(UUID)+线程编号
+ * 防误删
+ */
+ private final String uniqueId;
+
+ protected RedisDistributedLock(StringRedisTemplate redisTemplate, String uniqueIdPrefix, String lockKey, long lockTime,
+ TimeUnit timeUnit) {
+ super(lockKey, lockTime, timeUnit);
+ this.redisTemplate = redisTemplate;
+ this.uniqueId = uniqueIdPrefix + ":" + Thread.currentThread().getId();
+ }
+
+ @Override
+ public void lock() {
+ // 加锁失败,自旋阻塞
+ while (!tryLock()) {
+ try {
+ TimeUnit.MILLISECONDS.sleep(50);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean tryLock() {
+ try {
+ return tryLock(lockTime, timeUnit);
+ } catch (InterruptedException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedException {
+ String script = "if redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
+ "then " +
+ " redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
+ " redis.call('expire', KEYS[1], ARGV[2]) " +
+ " return 1 " +
+ "else " +
+ " return 0 " +
+ "end";
+
+ long expire = unit.toSeconds(time);
+ Boolean getLock = Optional.ofNullable(
+ redisTemplate.execute(
+ new DefaultRedisScript<>(script, Boolean.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire)))
+ .orElse(Boolean.FALSE);
+ // 如果获得锁,开启自动续期
+ if (getLock) {
+ renewLockTime(time, unit);
+ }
+ return getLock;
+ }
+
+ private void renewLockTime(long time, TimeUnit unit) {
+ String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1 " +
+ "then " +
+ " return redis.call('expire', KEYS[1], ARGV[2]) " +
+ "else " +
+ " return 0 " +
+ "end";
+
+ long expire = unit.toSeconds(time);
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Boolean renewed = Optional.ofNullable(
+ redisTemplate.execute(
+ new DefaultRedisScript<>(script, Boolean.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire))
+ ).orElse(Boolean.FALSE);
+ // 续期成功,代表未被解锁,则需要进行下一次续期
+ if (renewed) {
+ renewLockTime(time, unit);
+ }
+ }
+ }, expire * 1000 / 3);
+ }
+
+ @Override
+ public void unlock() {
+ String script = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 " +
+ "then " +
+ " return nil " +
+ "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0 " +
+ "then " +
+ " return redis.call('del', KEYS[1]) " +
+ "else " +
+ " redis.call('expire', KEYS[1], ARGV[2]) " +
+ " return 0 " +
+ "end";
+
+ // 如果解锁,发现是重入的,需要重新续期
+ long expire = timeUnit.toSeconds(lockTime);
+ Long flag = this.redisTemplate.execute(
+ new DefaultRedisScript<>(script, Long.class),
+ Collections.singletonList(lockKey),
+ uniqueId, String.valueOf(expire)
+ );
+ if (flag == null) {
+ throw new IllegalMonitorStateException("this lock doesn't belong to you!");
+ }
+ }
+
+}
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockAutoConfiguration.java b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockAutoConfiguration.java
new file mode 100644
index 0000000..48d4f57
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockAutoConfiguration.java
@@ -0,0 +1,23 @@
+package com.xkcoding.distributed.lock.autoconfigure;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+/**
+ *
+ * 基于 RedisTemplate 分布式锁自动装配类
+ *
+ *
+ * @author yangkai.shen
+ * @date 2022-09-03 15:43
+ */
+@Configuration(proxyBeanMethods = false)
+public class RedisDistributedLockAutoConfiguration {
+
+ @Bean
+ public RedisDistributedLockClient distributedLockClient(StringRedisTemplate redisTemplate) {
+ return new RedisDistributedLockClient(redisTemplate);
+ }
+
+}
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockClient.java b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockClient.java
new file mode 100644
index 0000000..b6373f8
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/src/main/java/com/xkcoding/distributed/lock/autoconfigure/RedisDistributedLockClient.java
@@ -0,0 +1,44 @@
+package com.xkcoding.distributed.lock.autoconfigure;
+
+import com.xkcoding.distributed.lock.api.DistributedLock;
+import com.xkcoding.distributed.lock.api.DistributedLockClient;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ *
+ * 获取一把 RedisTemplate 实现的分布式锁
+ *
+ *
+ * @author yangkai.shen
+ * @date 2022-09-03 12:35
+ */
+public class RedisDistributedLockClient implements DistributedLockClient {
+ private final StringRedisTemplate redisTemplate;
+
+ /**
+ * 唯一标识前缀,用于集群环境下的主机标识,会在 Bean 初始化到 Spring 容器的时候设置
+ */
+ private final String uniqueIdPrefix;
+
+ public RedisDistributedLockClient(StringRedisTemplate redisTemplate) {
+ this.redisTemplate = redisTemplate;
+ uniqueIdPrefix = UUID.randomUUID().toString();
+ }
+
+
+ /**
+ * 获取一把锁
+ *
+ * @param lockKey 锁的标识
+ * @param lockTime 锁的时间
+ * @param timeUnit 锁的时间单位
+ * @return 锁
+ */
+ @Override
+ public DistributedLock getLock(String lockKey, long lockTime, TimeUnit timeUnit) {
+ return new RedisDistributedLock(redisTemplate, uniqueIdPrefix, lockKey, lockTime, timeUnit);
+ }
+}
diff --git a/demo-distributed-lock/demo-distributed-lock-redis/src/main/resources/application.yml b/demo-distributed-lock/demo-distributed-lock-redis/src/main/resources/application.yml
new file mode 100644
index 0000000..6d472b7
--- /dev/null
+++ b/demo-distributed-lock/demo-distributed-lock-redis/src/main/resources/application.yml
@@ -0,0 +1,19 @@
+server:
+ port: 8080
+ servlet:
+ context-path: /demo
+spring:
+ sql:
+ init:
+ continue-on-error: true
+ mode: always
+ schema-locations:
+ - "classpath:db/schema.sql"
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:3306/spring-boot-demo
+ username: root
+ password: root
+ redis:
+ host: 127.0.0.1
+ port: 6379