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

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,否则脚本会报错。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日