|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- # 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
- <dependencies>
- <dependency>
- <groupId>com.xkcoding</groupId>
- <artifactId>demo-distributed-lock-api</artifactId>
- <version>1.0.0-SNAPSHOT</version>
- </dependency>
-
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- </dependencies>
- ```
-
- ### 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` 类全部注释掉,这将不会装配 RedisTemplate 分布式锁
- 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.测试 RedisTemplate 分布式锁
-
- 1. 把 `RedisDistributedLockAutoConfiguration` 类的注释解开,此时 Spring Boot 会自动装配我们的 RedisTemplate 分布式锁
- 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)
|