SpringBoot实战:利用Redis Lua脚本实现分布式多命令原子操作与锁

在分布式系统中,多个客户端同时访问同一份共享资源时,往往需要保证操作的原子性与并发安全。Redis 天然支持高并发场景,但如果仅依赖其单命令原子性,对于多命令组合场景(比如同时修改多个键、检查并更新等)就无法保证原子性。而借助 Lua 脚本,Redis 可以将多条命令包装在同一个脚本里执行,保证**“一组命令”**在 Redis 侧原子执行,从而避免并发冲突。此外,Lua 脚本也常用于实现可靠的分布式锁逻辑。

本文将以 Spring Boot + Spring Data Redis 为基础,全面讲解如何通过 Redis Lua 脚本实现:

  1. 多命令原子操作
  2. 分布式锁(含锁超时续命令与安全释放)

内容包含环境准备、概念介绍、关键代码示例、以及图解说明,帮助你更容易上手并快速应用到项目中。


目录

  1. 环境准备
    1.1. 技术栈与依赖
    1.2. Redis 环境部署
  2. Lua 脚本简介
  3. Spring Boot 集成 Spring Data Redis
    3.1. 引入依赖
    3.2. RedisTemplate 配置
  4. Redis Lua 脚本的原子性与执行流程
    4.1. 为什么要用 Lua 脚本?
    4.2. Redis 调用 Lua 脚本执行流程(图解)
  5. 分布式多命令原子操作示例
    5.1. 场景描述:库存扣减 + 订单状态更新
    5.2. Lua 脚本编写
    5.3. Java 端调用脚本
    5.4. 代码示例详解
    5.5. 执行流程图示
  6. 分布式锁实现示例
    6.1. 分布式锁设计思路
    6.2. 简易版锁:SETNX + TTL
    6.3. 安全释放锁:Lua 脚本检测并删除
    6.4. Java 实现分布式锁类
    6.5. 使用示例与图解
  7. 完整示例项目结构一览
  8. 总结

环境准备

1.1 技术栈与依赖

  • JDK 1.8+
  • Spring Boot 2.5.x 或更高
  • Spring Data Redis 2.5.x
  • Redis 6.x 或更高版本
  • Maven 构建工具

主要依赖示例如下(摘自 pom.xml):

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- Lettuce (Redis Client) -->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
    </dependency>

    <!-- 可选:用于 Lombok 简化代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    
    <!-- 可选:用于日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
    </dependency>
</dependencies>

1.2 Redis 环境部署

本地调试可通过 Docker 快速启动 Redis 实例,命令示例:

docker run -d --name spring-redis -p 6379:6379 redis:6.2.6 redis-server --appendonly yes

如果已经安装 Redis,可直接在本地启动:

redis-server /usr/local/etc/redis/redis.conf

确认 Redis 可用后,可使用 redis-cli 测试连接:

redis-cli ping
# 若返回 PONG 则表示正常

Lua 脚本简介

Lua 是一种轻量级脚本语言,语法简单且灵活。Redis 原生集成了一个 Lua 解释器(基于 Lua 5.1),允许客户端通过 EVAL 命令将“一段” Lua 脚本上传到 Redis 服务器并执行。Lua 脚本执行以下特点:

  1. 原子性
    整段脚本会以单个“调用”原子执行,中间不被其他客户端命令插入。
  2. 效率高
    避免了客户端-服务器之间多次网络往返,直接在服务器端执行多条命令。
  3. 可使用 Redis 原生命令
    在 Lua 脚本里,所有 Redis 命令都可通过 redis.call()redis.pcall() 调用。

常见指令:

  • EVAL script numkeys key1 key2 ... arg1 arg2 ...
  • EVALSHA sha1 numkeys key1 ... arg1 ...

其中:

  • script:Lua 代码
  • numkeys:脚本中要访问的 key 的数量
  • key1/key2...:传入的 key 列表
  • arg1/arg2...:传入的其他参数列表

Spring Boot 集成 Spring Data Redis

3.1 引入依赖

pom.xml 中,确保存在以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>

Spring Boot 自动配置了 Lettuce 作为 Redis 客户端。如果你想使用 Jedis,只需排除 Lettuce 并引入 Jedis 依赖即可。

3.2 RedisTemplate 配置

在 Spring Boot 中,推荐使用 RedisTemplate<String, Object> 来操作 Redis。我们需要在配置类中进行基础配置:

@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 默认 LettuceConnectionFactory 会读取 application.properties 中的配置
        return new LettuceConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 使用 StringRedisSerializer 序列化 key
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // 使用 Jackson2JsonRedisSerializer 序列化 value
        Jackson2JsonRedisSerializer<Object> jacksonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

application.properties 中,添加 Redis 连接配置:

spring.redis.host=127.0.0.1
spring.redis.port=6379
# 如果有密码,可加上:
# spring.redis.password=yourpassword

有了上述配置后,我们就能在其它组件或 Service 中注入并使用 RedisTemplate<String, Object> 了。


Redis Lua 脚本的原子性与执行流程

4.1 为什么要用 Lua 脚本?

  • 多命令原子性
    如果你在业务逻辑里需要对多个 Key 进行操作(例如:扣库存后更新订单状态),而只是使用多条 Redis 命令,就无法保证这几步操作“同时”成功或失败,存在中途出错导致数据不一致的风险。
  • 减少网络开销
    如果客户端需要执行多条命令,通常要经历 N 次网络往返(RTT)。而使用 Lua 脚本,只需要一次调用,就能在服务器端执行多条命令,极大提高性能。
  • 实现复杂逻辑
    某些场景下,需要复杂的判断、条件分支,这时可以在 Lua 中完成,而不必在客户端反复查询、再发命令,从而减少延迟和潜在的并发问题。

4.2 Redis 调用 Lua 脚本执行流程(图解)

下面是一次典型的 Lua 脚本调用流程示意图:

┌───────────┐               ┌───────────┐               ┌───────────┐
│ Client    │               │ Redis     │               │  Data     │
│ (Java)    │   EVAL LUA     │ Server    │               │ Storage   │
│           ├──────────────▶│           │               │(Key1,Key2)│
└───────────┘    (script)   │           │               └───────────┘
                            │           │
                            │ 1. 加载/执行│
                            │    Lua 脚本│
                            │ 2. 调用 lua │◀────────────┐
                            │    redis.call(... )          │
                            │    多命令执行               │
                            │ 3. 返回结果                  │
                            └───────────┘
                                      ▲
                                      │
                           响应结果    │
                                      │
                              ┌───────────┐
                              │ Client    │
                              │ (Java)    │
                              └───────────┘
  • Step 1:Java 客户端通过 RedisTemplate.execute() 方法,将 Lua 脚本和参数一起提交给 Redis Server。
  • Step 2:Redis 在服务器端加载并执行 Lua 脚本。脚本内可以直接调用 redis.call("GET", key)redis.call("SET", key, value) 等命令。此时,Redis 会对这整个脚本加锁,保证脚本执行期间,其他客户端命令不会插入。
  • Step 3:脚本执行完后,将返回值(可以是数字、字符串、数组等)返回给客户端。

分布式多命令原子操作示例

5.1 场景描述:库存扣减 + 订单状态更新

假设我们有一个电商场景,需要在用户下单时执行两步操作:

  1. 检查并扣减库存
  2. 更新订单状态为“已创建”

如果拆成两条命令:

IF stock > 0 THEN DECR stockKey
SET orderStatusKey "CREATED"

在高并发情况下,这两条命令无法保证原子性,可能出现以下问题:

  1. 扣减库存后,更新订单状态时程序异常,导致库存减少但订单未创建。
  2. 查询库存时,已被其他线程扣减,但未及时更新,导致库存不足。

此时,借助 Lua 脚本可以将“检查库存 + 扣减库存 + 更新订单状态”三步逻辑,放在一个脚本里执行,保证原子性。

5.2 Lua 脚本编写

创建一个名为 decr_stock_and_create_order.lua 的脚本,内容如下:

-- decr_stock_and_create_order.lua

-- 获取传入的参数
-- KEYS[1] = 库存 KEY (e.g., "product:stock:1001")
-- KEYS[2] = 订单状态 KEY (e.g., "order:status:abcd1234")
-- ARGV[1] = 扣减数量 (一般为 1)
-- ARGV[2] = 订单状态 (e.g., "CREATED")

local stockKey = KEYS[1]
local orderKey = KEYS[2]
local decrCount = tonumber(ARGV[1])
local statusVal = ARGV[2]

-- 查询当前库存
local currentStock = tonumber(redis.call("GET", stockKey) or "-1")

-- 如果库存不足,则返回 -1 代表失败
if currentStock < decrCount then
    return -1
end

-- 否则,扣减库存
local newStock = redis.call("DECRBY", stockKey, decrCount)

-- 将订单状态写入 Redis
redis.call("SET", orderKey, statusVal)

-- 返回剩余库存
return newStock

脚本说明:

  1. local stockKey = KEYS[1]:第一个 Redis Key,表示商品库存
  2. local orderKey = KEYS[2]:第二个 Redis Key,表示订单状态
  3. ARGV[1]:要扣减的库存数量
  4. ARGV[2]:订单状态值
  5. 先做库存检查:若不足,直接返回 -1
  6. 再做库存扣减 + 写入订单状态,最后返回剩余库存

5.3 Java 端调用脚本

在 Spring Boot 项目中,我们可以将上述 Lua 脚本放在 resources/scripts/ 目录下,然后通过 DefaultRedisScript 加载并执行。

1)加载脚本

@Component
public class LuaScriptLoader {

    /**
     * 加载 "decr_stock_and_create_order.lua" 脚本文件
     * 脚本返回值类型是 Long
     */
    @Bean
    public DefaultRedisScript<Long> decrStockAndCreateOrderScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // 指定脚本文件路径(classpath 下)
        redisScript.setLocation(new ClassPathResource("scripts/decr_stock_and_create_order.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}
注意ClassPathResource("scripts/decr_stock_and_create_order.lua") 要与 src/main/resources/scripts/ 目录对应。

2)Service 层执行脚本

@Service
public class OrderService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate; // 也可用 RedisTemplate<String, Object>

    @Autowired
    private DefaultRedisScript<Long> decrStockAndCreateOrderScript;

    /**
     * 尝试扣减库存并创建订单
     *
     * @param productId   商品ID
     * @param orderId     订单ID
     * @param decrCount   扣减数量,一般为1
     * @return 如果返回 -1 ,表示库存不足;否则返回扣减后的剩余库存
     */
    public long decrStockAndCreateOrder(String productId, String orderId, int decrCount) {
        // 组装 Redis key
        String stockKey = "product:stock:" + productId;
        String orderKey = "order:status:" + orderId;

        // KEYS 列表
        List<String> keys = Arrays.asList(stockKey, orderKey);
        // ARGV 列表
        List<String> args = Arrays.asList(String.valueOf(decrCount), "CREATED");

        // 执行 Lua 脚本
        Long result = stringRedisTemplate.execute(
                decrStockAndCreateOrderScript,
                keys,
                args.toArray()
        );

        if (result == null) {
            throw new RuntimeException("Lua 脚本返回 null");
        }
        return result;
    }
}
  • stringRedisTemplate.execute(...):第一个参数是 DefaultRedisScript,指定脚本和返回类型;
  • 第二个参数是 keys 列表;
  • 剩余可变参数 args 对应脚本中的 ARGV

如果 result == -1,代表库存不足,需在用户侧抛出异常或返回提示;否则返回剩余库存供业务使用。

5.4 代码示例详解

  1. Lua 脚本层面

    • 首先用 redis.call("GET", stockKey) 获取当前库存,这是原子操作。
    • 判断库存是否足够:如果 currentStock < decrCount,直接返回 -1,表示库存不足,并结束脚本。
    • 否则,使用 redis.call("DECRBY", stockKey, decrCount) 进行扣减,返回新的库存数。
    • 接着用 redis.call("SET", orderKey, statusVal) 将订单状态写入 Redis。
    • 最后将 newStock 返回给 Java 客户端。
  2. Java 层面

    • 通过 DefaultRedisScript<Long> 将 Lua 脚本加载到 Spring 容器中,该 Bean 名为 decrStockAndCreateOrderScript
    • OrderService 中注入 StringRedisTemplate(简化版 RedisTemplate<String, String>),同时注入 decrStockAndCreateOrderScript
    • 调用 stringRedisTemplate.execute(...),将脚本、Key 列表与参数列表一并传递给 Redis。
    • 使用脚本返回的 Long 值决定业务逻辑分支。

这样一来,无论在多高并发的场景下,这个“扣库存 + 生成订单”操作,都能在 Redis 侧以原子方式执行,避免并发冲突和数据不一致风险。

5.5 执行流程图示

下面用 ASCII 图解总体执行流程,帮助理解:

┌─────────────────┐      1. 发送 EVAL 脚本请求       ┌─────────────────┐
│  Java 客户端    │ ─────────────────────────────▶ │    Redis Server  │
│ (OrderService)  │    KEYS=[stockKey,orderKey]   │                 │
│                 │    ARGV=[1, "CREATED"]       │                 │
└─────────────────┘                                └─────────────────┘
                                                       │
                                                       │ 2. 在 Redis 端加载脚本
                                                       │   并执行以下 Lua 代码:
                                                       │   if stock<1 then return -1
                                                       │   else decr库存; set 订单状态; return newStock
                                                       │
                                                       ▼
                                                ┌─────────────────┐
                                                │  Redis 数据层    │
                                                │ (Key:product:   │
                                                │  stock:1001)    │
                                                └─────────────────┘
                                                       │
                                                       │ 3. 返回执行结果 = newStock 或 -1
                                                       │
                                                       ▼
┌─────────────────┐                                ┌─────────────────┐
│  Java 客户端    │ ◀──────────────────────────── │    Redis Server  │
│ (OrderService)  │    返回 Long result           │                 │
│                 │    (e.g. 99 或 -1)           │                 │
└─────────────────┘                                └─────────────────┘

分布式锁实现示例

在分布式系统中,很多场景需要通过分布式锁来控制同一资源在某一时刻只能一个客户端访问。例如:秒杀场景、定时任务并发调度、数据迁移等。

下面以 Redis + Lua 脚本方式实现一个安全、可靠的分布式锁。主要思路与步骤如下:

  1. 使用 SET key value NX PX timeout 来尝试获取锁
  2. 如果获取成功,返回 OK
  3. 如果获取失败,返回 null,可重试或直接失败
  4. 释放锁时,需要先判断 value 是否和自己存储的标识一致,以防误删他人锁
注意:判断并删除的逻辑需要通过 Lua 脚本实现,否则会出现“先 GET 再 DEL”期间锁被别的客户端抢走,造成误删。

6.1 分布式锁设计思路

  • 锁 Key:比如 lock:order:1234
  • 值 Value:每个客户端生成一个唯一随机值(UUID),保证释放锁时只删除自己持有的锁
  • 获取锁SET lockKey lockValue NX PX expireTime,NX 表示只有当 key 不存在时才设置,PX 表示设置过期时间
  • 释放锁:通过 Lua 脚本,判断 redis.call("GET", lockKey) == lockValue 时,才执行 DEL lockKey

6.2 简易版锁:SETNX + TTL

在没有 Lua 脚本时,最简单的分布式锁(不推荐):

public boolean tryLockSimple(String lockKey, String lockValue, long expireTimeMillis) {
    // 使用 StringRedisTemplate
    Boolean success = stringRedisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockValue, Duration.ofMillis(expireTimeMillis));
    return Boolean.TRUE.equals(success);
}

public void unlockSimple(String lockKey) {
    stringRedisTemplate.delete(lockKey);
}

缺点:

  1. 释放锁时无法判断当前锁是否属于自己,会误删别人的锁。
  2. 如果业务执行时间超过 expireTimeMillis,锁过期后被别人获取,导致解锁删除了别人的锁。

6.3 安全释放锁:Lua 脚本检测并删除

编写一个 Lua 脚本 redis_unlock.lua,内容如下:

-- redis_unlock.lua
-- KEYS[1] = lockKey
-- ARGV[1] = lockValue

-- 只有当存储的 value 和传入 value 相同时,才删除锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

运行流程:

  1. client 传入 lockKeylockValue
  2. 脚本先执行 GET lockKey,若值等于 lockValue,则执行 DEL lockKey,并返回删除结果(1)
  3. 否则直接返回 0,不做任何删除

这样就保证了“只删除自己加的锁”,避免误删锁的问题。

6.4 Java 实现分布式锁类

在 Spring Boot 中,我们可以封装一个 RedisDistributedLock 工具类,封装锁的获取与释放逻辑。

1)加载解锁脚本

@Component
public class RedisScriptLoader {

    // 前面已经加载了 decrStock 脚本,下面加载解锁脚本
    @Bean
    public DefaultRedisScript<Long> unlockScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setLocation(new ClassPathResource("scripts/redis_unlock.lua"));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

2)封装分布式锁工具类

@Service
public class RedisDistributedLock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private DefaultRedisScript<Long> unlockScript;

    /**
     * 尝试获取分布式锁
     *
     * @param lockKey        锁 Key
     * @param lockValue      锁 Value(通常为 UUID)
     * @param expireTimeMillis 过期时间(毫秒)
     * @return 是否获取成功
     */
    public boolean tryLock(String lockKey, String lockValue, long expireTimeMillis) {
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, Duration.ofMillis(expireTimeMillis));
        return Boolean.TRUE.equals(success);
    }

    /**
     * 释放锁:只有锁的持有者才能释放
     *
     * @param lockKey   锁 Key
     * @param lockValue 锁 Value
     * @return 是否释放成功
     */
    public boolean unlock(String lockKey, String lockValue) {
        List<String> keys = Collections.singletonList(lockKey);
        List<String> args = Collections.singletonList(lockValue);
        // 执行 lua 脚本,返回 1 代表删除了锁,返回 0 代表未删除
        Long result = stringRedisTemplate.execute(unlockScript, keys, args.toArray());
        return result != null && result > 0;
    }
}
方法解析
  • tryLock

    • 使用 stringRedisTemplate.opsForValue().setIfAbsent(key,value,timeout)SETNX + TTL,保证只有当 key 不存在时,才设置成功
    • expireTimeMillis 用于避免死锁,防止业务没有正常释放锁导致锁永远存在
  • unlock

    • 通过先 GET lockKeylockValue 做对比,等于时再 DEL lockKey,否则不删除
    • 这部分通过 redis_unlock.lua Lua 脚本实现原子“校验并删除”

6.5 使用示例与图解

1)使用示例

@RestController
@RequestMapping("/api/lock")
public class LockController {

    @Autowired
    private RedisDistributedLock redisDistributedLock;

    @GetMapping("/process")
    public ResponseEntity<String> processTask() {
        String lockKey = "lock:task:123";
        String lockValue = UUID.randomUUID().toString();
        long expireTime = 5000; // 5秒过期

        boolean acquired = redisDistributedLock.tryLock(lockKey, lockValue, expireTime);
        if (!acquired) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("获取锁失败,请稍后重试");
        }

        try {
            // 业务处理逻辑
            Thread.sleep(3000); // 模拟执行 3 秒
            return ResponseEntity.ok("任务执行成功");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("任务执行异常");
        } finally {
            // 释放锁(安全释放)
            boolean released = redisDistributedLock.unlock(lockKey, lockValue);
            if (!released) {
                // 日志记录:释放锁失败(可能锁已过期被其他人持有)
                System.err.println("释放锁失败,lockKey=" + lockKey + ", lockValue=" + lockValue);
            }
        }
    }
}

2)解锁 Lua 脚本流程图(图解)

┌────────────────┐         1. EVAL redis_unlock.lua         ┌─────────────────┐
│ Java 客户端    │ ─────────────────────────────────────────▶ │  Redis Server    │
│ (unlock 方法) │    KEYS=[lockKey], ARGV=[lockValue]      │                  │
└────────────────┘                                         └─────────────────┘
                                                              │
                                                              │ 2. 执行 Lua:
                                                              │    if GET(key)==value 
                                                              │       then DEL(key)
                                                              │       else return 0
                                                              │
                                                              ▼
                                                    ┌──────────────────────────┐
                                                    │   Redis Key-Value 存储     │
                                                    │   lockKey -> lockValue     │
                                                    └──────────────────────────┘
                                                              │
                                                              │ 3. 返回结果 1 或 0
                                                              ▼
┌────────────────┐                                         ┌─────────────────┐
│ Java 客户端    │ ◀───────────────────────────────────────── │  Redis Server    │
│ (unlock 方法) │   返回 1(删除成功)或 0(未删除)         │                  │
└────────────────┘                                         └─────────────────┘

这样,分布式锁的获取与释放就得到了很好的保障,在高并发分布式场景中能避免竞态条件与误删锁带来的风险。


完整示例项目结构一览

以下是本文示例代码对应的典型项目目录结构:

springboot-redis-lua-demo/
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.example.redisluademo
│   │   │       ├── RedisConfig.java
│   │   │       ├── LuaScriptLoader.java
│   │   │       ├── OrderService.java
│   │   │       ├── RedisDistributedLock.java
│   │   │       └── controller
│   │   │            ├── OrderController.java
│   │   │            └── LockController.java
│   │   └── resources
│   │       ├── application.properties
│   │       └── scripts
│   │           ├── decr_stock_and_create_order.lua
│   │           └── redis_unlock.lua
│   └── test
│       └── java
│           └── com.example.redisluademo
│               └── RedisLuaDemoApplicationTests.java
└── README.md

简要说明:

  • RedisConfig.java:配置 RedisTemplate
  • LuaScriptLoader.java:加载 Lua 脚本
  • OrderService.java:演示多命令原子操作脚本调用
  • RedisDistributedLock.java:分布式锁工具类
  • OrderController.java:演示下单调用示例(可选,适当演示接口)
  • LockController.java:演示分布式锁场景
  • decr_stock_and_create_order.luaredis_unlock.lua:两个核心 Lua 脚本

总结

本文详细介绍了在 Spring Boot 项目中,如何借助 Redis Lua 脚本,实现:

  1. 分布式多命令原子操作

    • 通过 Lua 脚本将 “检查库存、扣库存、写订单状态” 三步逻辑打包在一起,保证在 Redis 端以原子方式执行,避免中途失败导致数据不一致。
    • 在 Java 侧,通过 DefaultRedisScript 加载脚本并配合 RedisTemplate.execute() 调用脚本。
  2. 分布式锁

    • 结合 SETNX + TTL 实现基本的加锁操作;
    • 利用 Lua 脚本保证“先校验 Value 再删除”这一操作的原子性,避免误删除锁的问题。
    • 在 Java 侧封装加锁与解锁逻辑,确保业务执行期间获取到合适的并发控制。

通过“代码示例 + 图解”,本文帮助你较为清晰地理解 Redis Lua 脚本在高并发场景下的威力,以及如何在 Spring Boot 中优雅地集成使用。你可以将上述示例直接复制到项目中,根据业务需求进行扩展和优化。

Tip

  • 如果业务中有更复杂的并发控制需求,也可以借助像 Redisson 这样的 Redis 客户端,直接使用它封装好的分布式锁和信号量功能。
  • 发布时间和配置请根据线上的 Redis 版本进行测试,注意 Redis 集群模式下 Lua 脚本涉及到多节点 key 存取时,需要将所有 key 定位到同一个 slot,否则脚本会报错。

Spring Boot项目中MyBatis-Plus多容器分布式部署ID重复问题深度剖析

一、引言

在微服务架构或容器化部署环境下,往往会将同一个 Spring Boot 应用镜像在多台机器或多个容器中运行,以实现高可用与负载均衡。若项目使用 MyBatis-Plus 默认的自增主键策略(AUTO_INCREMENT),多容器并发写入数据库时,就会出现 ID 冲突或重复的问题,严重影响数据一致性。本文将从问题产生的根本原因出发,结合代码示例与图解,深入剖析常见的 ID 生成方案,并演示如何在 MyBatis-Plus 中优雅地解决分布式部署下的 ID 重复问题。


二、问题背景与分析

2.1 单实例 vs 多容器部署的差异

  • 单实例部署:Spring Boot 应用只有一个实例访问数据库,使用 AUTO_INCREMENT 主键时,数据库会为每条插入操作自动分配连续且唯一的主键,几乎不存在 ID 冲突问题。
  • 多容器部署:在 Kubernetes 或 Docker Swarm 等环境下,我们可能将相同应用运行多份,容器 A 和容器 B 同时向同一张表批量插入数据。如果依赖数据库自增字段,就需要确保所有写请求串行化,否则在高并发下仍会依赖数据库锁定机制。尽管数据库会避免同一时刻分配相同自增值,但在水平扩展且读写分离、分库分表等场景中,自增 ID 仍然可能产生冲突或不连续(例如各库自增起始值相同)。

另外,如果采用了分库分表,数据库层面的自增序列在不同分表间并不能保证全局唯一。更重要的是,在多副本缓存层、分布式消息队列中回写数据时,单纯的自增 ID 也会带来重复风险。

2.2 MyBatis-Plus 默认主键策略

MyBatis-Plus 的 @TableId 注解默认使用 IdType.NONE,若数据库表主键列是自增类型(AUTO_INCREMENT),MyBatis-Plus 会从 JDBC 执行插入后获取数据库生成的自增 ID。参考代码:

// 实体类示例
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private String name;
    // ... Getter/Setter ...
}

上述映射在单实例场景下工作正常,但无法在多容器分布式部署中避免 ID 重复。


三、常见分布式ID生成方案

3.1 UUID

  • 原理:通过 java.util.UUIDUUID.randomUUID() 生成一个全局唯一的 128 位标识(字符串格式),几乎不会重复。
  • 优缺点

    • 优点:不需集中式协调,简单易用;
    • 缺点:UUID 较长,存储与索引成本高;对于数字型主键需要额外转换;无法按顺序排列,影响索引性能。

示例代码:

// 在实体类中使用 UUID 作为 ID
public class Order {
    @TableId(value = "id", type = IdType.ASSIGN_UUID)
    private String id;
    private BigDecimal amount;
    // ...
}

MyBatis-Plus IdType.ASSIGN_UUID 会在插入前调用 UUID.randomUUID().toString().replace("-", ""),得到 32 位十六进制字符串。

3.2 数据库全局序列(Sequence)

  • 多数企业数据库(如 Oracle、PostgreSQL)支持全局序列。每次从序列获取下一个值,保证全局唯一。
  • 缺点:MySQL 直到 8.0 才支持 CREATE SEQUENCE,很多旧版 MySQL 仍需通过“自增表”或“自增列+段值”来模拟序列,略显麻烦。且跨分库分表场景下,需要集中式获取序列,略损性能。

MyBatis-Plus 在 MySQL 上也可通过以下方式使用自定义序列:

// 在数据库中创建一个自增表 seq_table(id BIGINT AUTO_INCREMENT)
@TableId(value = "id", type = IdType.INPUT)
private Long id;

// 插入前通过 Mapper 获取 seq_table 的下一个自增值
Long nextId = seqTableMapper.nextId();
user.setId(nextId);
userMapper.insert(user);

3.3 Redis 全局自增

  • 利用 Redis 的 INCRINCRBY 操作,保证在单个 Redis 实例或集群的状态下,自增序列全局唯一。
  • 优缺点

    • 优点:性能高(内存操作),可集群部署;
    • 缺点:Redis 宕机或分区时需要方案保证可用性与数据持久化,且 Redis 也是单点写。

示例代码(Spring Boot + Lettuce/Redisson):

@Autowired
private StringRedisTemplate redisTemplate;

public Long generateOrderId() {
    return redisTemplate.opsForValue().increment("global:order:id");
}

// 在实体插入前设置 ID
Long id = generateOrderId();
order.setId(id);
orderMapper.insert(order);

3.4 Twitter Snowflake 算法

  • 原理:Twitter 开源的 Snowflake 算法生成 64 位整型 ID,结构为:1 位符号(0),41 位时间戳(毫秒)、10 位机器标识(datacenterId + workerId,可自定义位数),12 位序列号(同一毫秒内自增)。
  • 优缺点

    • 优点:整体性能高、单机无锁,支持多节点同时生成;ID 有时间趋势,可按时间排序。
    • 缺点:需要配置机器 ID 保证不同实例的 datacenterId+workerId 唯一;时间回拨会导致冲突。

MyBatis-Plus 内置对 Snowflake 的支持,只需将 @TableId(type = IdType.ASSIGN_ID)IdType.ASSIGN_SNOWFLAKE 应用在实体类上。


四、MyBatis-Plus 中使用 Snowflake 的实战演示

下面以 Snowflake 为例,演示如何在 Spring Boot + MyBatis-Plus 多容器分布式环境中确保 ID 唯一。示例将演示:

  1. 配置 MyBatis-Plus 使用 Snowflake
  2. 生成唯一的 workerId / datacenterId
  3. 在实体中声明 @TableId(type = IdType.ASSIGN_ID)
  4. 演示两个容器同时插入数据不冲突

4.1 Spring Boot 项目依赖

pom.xml 中引入 MyBatis-Plus:

<dependencies>
    <!-- MyBatis-Plus Starter -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.31</version>
    </dependency>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

4.2 创建一个雪花算法 ID 生成器 Bean

在 Spring Boot 启动类或单独的配置类中,注册 MyBatis-Plus 提供的 IdentifierGenerator 实现:

import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SnowflakeConfig {

    /**
     * MyBatis-Plus 默认的雪花算法实现 DefaultIdentifierGenerator
     * 使用前请确保在 application.properties 中配置了以下属性:
     * mybatis-plus.snowflake.worker-id=1
     * mybatis-plus.snowflake.datacenter-id=1
     */
    @Bean
    public IdentifierGenerator idGenerator() {
        return new DefaultIdentifierGenerator();
    }
}

DefaultIdentifierGenerator 会读取 Spring 环境变量 mybatis-plus.snowflake.worker-idmybatis-plus.snowflake.datacenter-id 来初始化 Snowflake 算法实例,workerIddatacenterId 需要保证在所有容器实例中不重复。

4.3 application.yml / application.properties 配置

假设使用 YAML,分别为不同实例配置不同的 worker-id

spring:
  application:
    name: mybatisplus-demo

mybatis-plus:
  snowflake:
    worker-id: ${WORKER_ID:0}
    datacenter-id: ${DATACENTER_ID:0}
  global-config:
    db-config:
      id-type: ASSIGN_ID
  • ${WORKER_ID:0} 允许通过环境变量注入,每个容器通过 Docker 或 Kubernetes 环境变量指定不同值。
  • id-type: ASSIGN_ID 表示全局主键策略为 MyBatis-Plus 内置雪花算法生成。

启动时,在容器 A 中设置 WORKER_ID=1,在容器 B 中设置 WORKER_ID=2,二者保证不同,即可避免冲突。

4.4 实体类示例

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.time.LocalDateTime;

@TableName("user")
public class User {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String username;
    private String email;

    // 自动填充示例(可选)
    private LocalDateTime createTime;
    private LocalDateTime updateTime;

    // Getter/Setter...
}
  • @TableId(type = IdType.ASSIGN_ID):MyBatis-Plus 在插入前会调用默认的 IdentifierGenerator(即 DefaultIdentifierGenerator),按 Snowflake 算法生成唯一 Long 值。

4.5 Mapper 接口与 Service 层示例

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 继承 BaseMapper 即可具有基本 CRUD 操作
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public User createUser(String username, String email) {
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        userMapper.insert(user);
        return user;
    }
}

不需要手动设置 id,MyBatis-Plus 会自动调用 Snowflake 生成。

4.6 演示多容器插入

启动两个容器实例:

  • 容器 A(WORKER_ID=1
  • 容器 B(WORKER_ID=2

同时发送如下 HTTP 请求(假设 REST API 已暴露):

POST /users  请求体: {"username":"alice","email":"alice@example.com"}
  • 在容器 A 中处理时,Snowflake 算法产生的 id 例如 140xxxxx0001
  • 在容器 B 中处理时,Snowflake 算法产生的 id 例如 140xxxxx1001
    两者不会重复;如“图:多容器部署中基于Snowflake的ID生成示意图”所示,分别对应不同 workerId 的实例同时向同一个共享数据库插入数据,主键不会冲突。

五、图解:多容器部署中 Snowflake ID 生成示意图

(上方已展示“图:多容器部署中基于Snowflake的ID生成示意图”)

  • Container1(workerId=1)Container2(workerId=2)
  • 各自使用 Snowflake 算法,通过高位的 workerId 区分,生成不同 ID
  • 两者同时插入到共享数据库,不会产生重复的主键

六、其他分布式ID生成方案对比与选型

6.1 UUID vs Snowflake

方案唯一性长度时间趋势索引效率配置复杂度
UUID (String)极高36/32 字符较差
Snowflake极高64 位数值
  • 如果对 ID 长度与排序性能要求高,推荐 Snowflake。
  • 若对二进制 ID 不能接受、只需简单唯一值,可使用 UUID。

6.2 Redis 全局自增 vs Snowflake

方案唯一性性能单点压力配置复杂度
Redis INCR极高Redis 单点写
Snowflake极高无单点写
  • Redis 需考虑高可用切换与持久化,对运维要求高;Snowflake 纯 Java 实现,无额外依赖,更易水平扩展。

七、总结与实践建议

  1. 避免数据库自增主键
    多容器部署时不要再依赖单一数据库自增,应选用分布式 ID 生成方案。
  2. 选择合适的方案

    • Snowflake:大多数场景下的首选,性能高、可排序;
    • UUID:对性能与索引要求不高、需要跨语言兼容时可采纳;
    • Redis:需谨慎考虑 Redis 高可用与分区容错。
  3. 环境变量注入 workerId
    在 Kubernetes 中可通过 ConfigMap 或 Deployment 环境变量注入不同的 WORKER_ID,确保各实例唯一。
  4. 注意时钟回拨问题
    如果服务器时间被回调,会导致 Snowflake 生成重复或回退 ID,请使用 NTP 保证时钟一致或引入时间回拨处理逻辑。
  5. 回源策略
    如果数据库或 ID 服务不可用,应对插入操作进行失败重试或降级,避免影响业务可用性。

综上所述,通过在 Spring Boot + MyBatis-Plus 中使用 Snowflake(IdType.ASSIGN_ID)或其他分布式 ID 生成器,可以有效避免多容器部署下的 ID 重复问题,保障系统高可用与数据一致性。

2024-09-09

在Spring Cloud学习系列中,我们已经介绍了Elasticsearch的基本概念和安装配置。在这一部分中,我们将使用Spring Data Elasticsearch来实现分布式搜索功能。

首先,我们需要在Spring Boot项目中添加Spring Data Elasticsearch的依赖。




<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <!-- 其他依赖 -->
</dependencies>

接下来,我们需要配置Elasticsearch的客户端。




@Configuration
public class ElasticsearchConfig {
 
    @Value("${elasticsearch.host}")
    private String hostname;
 
    @Value("${elasticsearch.port}")
    private int port;
 
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        RestClientBuilder builder = RestClient.builder(new HttpHost(hostname, port));
        return new RestHighLevelClient(builder);
    }
}

然后,我们可以创建一个Elasticsearch的Repository接口。




public interface ProductRepository extends ElasticsearchRepository<Product, String> {
}

最后,我们可以使用这个Repository来执行搜索操作。




@Service
public class ProductSearchService {
 
    @Autowired
    private ProductRepository productRepository;
 
    public List<Product> searchByName(String name) {
        return productRepository.findByName(name);
    }
}

在这个例子中,我们定义了一个简单的搜索方法searchByName,它使用了Elasticsearch的Repository提供的查询方法findByName。这个方法会在Elasticsearch中搜索所有名称字段包含指定关键字的文档。

这只是一个基本的例子,实际应用中可能需要更复杂的查询逻辑,例如基于多个字段的搜索、分页、排序等。Spring Data Elasticsearch提供了丰富的查询方法定义,可以通过定义相应的接口来实现复杂的搜索需求。

2024-09-09

在Spring Cloud中使用Nacos作为配置中心,可以通过以下步骤实现:

  1. 引入Nacos客户端依赖:



<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. bootstrap.propertiesbootstrap.yml中配置Nacos服务器地址和应用名:



spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=my-spring-cloud-application
  1. 在应用代码中注入配置:



import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class ConfigController {
 
    @Value("${my.config}")
    private String myConfig;
 
    @GetMapping("/config")
    public String getConfig() {
        return myConfig;
    }
}
  1. 在Nacos控制台配置对应的配置数据。

以上步骤可以让你的Spring Cloud应用从Nacos配置中心加载配置。在Nacos控制台可以动态管理这些配置,修改后即时生效。

2024-09-09

在Spring Cloud中使用Nacos作为配置中心,可以通过以下步骤实现:

  1. 引入Nacos客户端依赖:



<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. bootstrap.propertiesbootstrap.yml中配置Nacos服务器地址和应用名:



spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.application.name=my-spring-cloud-application
  1. 在应用代码中注入配置:



import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class ConfigController {
 
    @Value("${my.config}")
    private String myConfig;
 
    @GetMapping("/config")
    public String getConfig() {
        return myConfig;
    }
}
  1. 在Nacos控制台配置对应的配置数据。

以上步骤可以让你的Spring Cloud应用从Nacos配置中心加载配置。在Nacos控制台可以动态管理这些配置,修改后即时生效。

2024-09-09

Spring Boot 是一个用于简化 Spring 应用程序的初始化和开发过程的开源框架。它使用自动配置和依赖项管理来简化 Spring 项目的配置。

Spring Cloud 是一系列框架,提供了具有服务发现、配置管理、负载均衡、断路器、分布式消息传递等能力的服务。

问题中的 "Springboot基础知识2" 可能指的是 Spring Boot 的进阶主题,比如:

  1. Spring Boot 配置文件加载和优先级。
  2. Spring Boot 使用 JPA 或 JDBC 连接数据库。
  3. Spring Boot 使用消息队列(如 Kafka、RabbitMQ)。
  4. Spring Boot 集成安全控制(如 OAuth2、JWT)。
  5. Spring Boot 监控和管理应用(如 Actuator)。

问题中的 "SpringCould 相关知识1" 可能指的是 Spring Cloud 的基本概念,比如:

  1. 服务注册与发现(如 Eureka、Consul)。
  2. 分布式配置管理(如 Spring Cloud Config)。
  3. 服务间调用(如 OpenFeign、RestTemplate)。
  4. 路由网关(如 Spring Cloud Gateway)。
  5. 服务保护(断路器模式)。

由于问题不具体,以上列举的都是可能的主题。具体的学习路径和实践代码取决于具体的需求和场景。

2024-09-09

由于篇幅所限,我将提供一个简化的商铺管理系统的核心实体类代码示例。这个例子展示了如何使用Spring Boot和JPA创建实体类。




import javax.persistence.*;
 
@Entity
public class Stall {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String name;
    private String location;
    private String status; // 可用 "空闲", "预定", "占用" 等状态
 
    // 构造函数、getter和setter省略
}
 
// 实体类对应的仓库接口
public interface StallRepository extends JpaRepository<Stall, Long> {
    // 根据位置查询商铺,可以扩展更多查询方法
    List<Stall> findByLocation(String location);
}

在这个例子中,我们定义了一个Stall实体类来表示商铺,并使用StallRepository接口继承JpaRepository来实现基本的数据访问操作。这个例子展示了如何通过JPA注解来映射实体到数据库表,并提供了一个自定义的查询方法findByLocation

在实际应用中,你还需要配置Spring Boot的主类来启动Spring Data JPA的特性,并且需要定义服务层来处理业务逻辑,同时需要创建相应的REST控制器来提供API接口。

2024-09-09



@Configuration
public class RedisConfig {
 
    @Value("${spring.redis.host}")
    private String host;
 
    @Value("${spring.redis.port}")
    private int port;
 
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
    }
 
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }
}

这段代码定义了一个配置类RedisConfig,它使用Lettuce客户端连接到Redis服务器。它创建了一个LettuceConnectionFactory实例,并配置了主机和端口。同时,它还定义了一个RedisTemplate的Bean,这是一个用于操作Redis的Spring数据模板,它使用提供的连接工厂。这个模板可以用于存储、检索和操作Redis数据。

2024-09-09

在Spring Boot中,我们可以使用Spring Boot Actuator来实现健康检查、度量指标和应用程序监控。Spring Boot Actuator是Spring Boot的一个子项目,提供了在应用程序生产环境下监控和管理应用程序的功能。

以下是一个简单的例子,展示如何在Spring Boot应用程序中使用Spring Boot Actuator。

  1. 首先,在Spring Boot项目的pom.xml中添加Spring Boot Actuator依赖:



<dependencies>
    ...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    ...
</dependencies>
  1. 接下来,在application.properties或application.yml文件中配置Actuator端点的访问权限,例如:



# application.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always



# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always
  1. 启动Spring Boot应用程序,你可以通过访问http://<hostname>:<port>/actuator/health来检查应用程序健康状况,通过http://<hostname>:<port>/actuator/metrics来获取应用程序的度量指标。

以上只是一个简单的示例,Spring Boot Actuator提供了更多的端点和功能,例如:

  • /actuator/health:显示应用程序的健康信息。
  • /actuator/info:显示自定义的应用程序信息。
  • /actuator/metrics:显示当前应用程序的度量指标。
  • /actuator/logfile:查看日志文件(需要logback或log4j配置)。
  • /actuator/threaddump:执行线程转储。
  • /actuator/shutdown:关闭应用程序(需要配置允许关闭)。

要进一步深入学习和使用Spring Boot Actuator,请查阅官方文档或相关教程。

2024-09-09

在Spring Boot中,我们可以使用Spring Boot Actuator来实现健康检查、度量指标和应用程序监控。Spring Boot Actuator是Spring Boot的一个子项目,提供了在应用程序生产环境下监控和管理应用程序的功能。

以下是一个简单的例子,展示如何在Spring Boot应用程序中使用Spring Boot Actuator。

  1. 首先,在Spring Boot项目的pom.xml中添加Spring Boot Actuator依赖:



<dependencies>
    ...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    ...
</dependencies>
  1. 接下来,在application.properties或application.yml文件中配置Actuator端点的访问权限,例如:



# application.properties
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always



# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics
  endpoint:
    health:
      show-details: always
  1. 启动Spring Boot应用程序,你可以通过访问http://<hostname>:<port>/actuator/health来检查应用程序健康状况,通过http://<hostname>:<port>/actuator/metrics来获取应用程序的度量指标。

以上只是一个简单的示例,Spring Boot Actuator提供了更多的端点和功能,例如:

  • /actuator/health:显示应用程序的健康信息。
  • /actuator/info:显示自定义的应用程序信息。
  • /actuator/metrics:显示当前应用程序的度量指标。
  • /actuator/logfile:查看日志文件(需要logback或log4j配置)。
  • /actuator/threaddump:执行线程转储。
  • /actuator/shutdown:关闭应用程序(需要配置允许关闭)。

要进一步深入学习和使用Spring Boot Actuator,请查阅官方文档或相关教程。