Lua 脚本可以确保在 Redis 中原子地存储 Hash 数据,并同时设置 Key 的过期时间,防止 Redis 在两步操作之间发生宕机,从而导致缓存数据未能正确设置过期时间的问题。


1. 传统方式的问题

假设我们直接用 Java 代码执行 HMSET 和 EXPIREAT:

stringRedisTemplate.opsForHash().putAll(couponTemplateCacheKey, actualCacheTargetMap);
stringRedisTemplate.expireAt(couponTemplateCacheKey, couponTemplateDO.getValidEndTime());

这可能会出现极端情况

  • HMSET 执行完,数据成功存入 Redis。
  • 但在执行 EXPIREAT 之前,Redis 宕机了,导致这个 Key 不会过期,形成脏数据(数据永远留在 Redis)。

2. 解决方案:使用 Lua 脚本

Lua 脚本在 Redis 中是原子执行的,Redis 不会在执行脚本的过程中被其他命令打断,所以可以保证 HMSET 和 EXPIREAT 要么 全部成功,要么 全部失败,从而避免宕机导致数据永久存储的问题。

Lua 脚本

redis.call('HMSET', KEYS[1], unpack(ARGV, 1, #ARGV - 1)) 
redis.call('EXPIREAT', KEYS[1], ARGV[#ARGV])

解释

  1. redis.call(‘HMSET’, KEYS[1], unpack(ARGV, 1, #ARGV – 1))
    • HMSET 用于存储 Hash 数据。
    • KEYS[1] 是 Redis 的 Key(这里是 couponTemplateCacheKey)。
    • unpack(ARGV, 1, #ARGV – 1) 代表 ARGV 中的前 N-1 个参数,用于 HMSET。
  2. redis.call(‘EXPIREAT’, KEYS[1], ARGV[#ARGV])
    • EXPIREAT 设置 Key 的过期时间,确保 Key 在指定的 Unix 时间戳自动删除
    • ARGV[#ARGV] 获取 ARGV 的最后一个参数,即过期时间(Unix 时间戳,秒级别)

3. Java 代码解析

3.1 定义 Lua 脚本

String luaScript = "redis.call('HMSET', KEYS[1], unpack(ARGV, 1, #ARGV - 1)) " +
        "redis.call('EXPIREAT', KEYS[1], ARGV[#ARGV])";

这段 Lua 代码会:

  • 一步完成存储数据(HMSET)。
  • 紧接着设置过期时间(EXPIREAT),确保数据不会意外永久存留。

3.2 准备 Redis Key

List<String> keys = Collections.singletonList(couponTemplateCacheKey);
  • keys 代表 Redis 的 Key(即 couponTemplateCacheKey)。

3.3 构造 Lua 脚本的参数

List<String> args = new ArrayList<>(actualCacheTargetMap.size() * 2 + 1);
actualCacheTargetMap.forEach((key, value) -> {
    args.add(key);
    args.add(value);
});
  • actualCacheTargetMap 是需要存入 Redis 的 Hash 数据(多个字段和值)。
  • 这里把 actualCacheTargetMap 转换为 一维数组形式,用于传入 Lua 的 ARGV。

3.4 添加 Key 的过期时间

args.add(String.valueOf(couponTemplateDO.getValidEndTime().getTime() / 1000));
  • couponTemplateDO.getValidEndTime().getTime() 以 毫秒级 返回时间。
  • / 1000 转换为 秒级 Unix 时间戳,符合 EXPIREAT 的要求。

3.5 执行 Lua 脚本

stringRedisTemplate.execute(
        new DefaultRedisScript<>(luaScript, Long.class),
        keys,
        args.toArray()
);
  • DefaultRedisScript<>(luaScript, Long.class):创建一个 Redis 脚本对象,返回值是 Long 类型。
  • keys:传入 Redis Key。
  • args.toArray():传入 Hash 字段和值 + 过期时间 作为 ARGV 参数。

4. 总结

方案可能问题是否原子执行
普通 Java 代码HMSET 执行后,EXPIREAT 可能因 Redis 宕机未执行,导致缓存数据不会自动过期❌ 不是原子操作
Lua 脚本HMSET 和 EXPIREAT 在 Redis 内部原子执行,不会被中断,保证 Key 一定有过期时间✅ 100% 原子操作

5. 适用场景

分布式缓存:防止缓存数据因为 Redis 宕机而永久存储。
秒杀 / 限流:保证 Redis 的限流数据一定会自动过期。
高并发场景:减少 Redis 网络开销,提高执行效率。

在 Redis 需要同时存储数据并保证自动过期 的情况下,Lua 脚本是最安全的方案!

✅ 在Java 定义 Lua 脚本,可以是xxx.lua文件,也可是写成字符串,并将它发送给 Redis 服务器,真正的 是 在 Redis 服务器内部执行的

这样做的好处是:

  • 减少 Java 与 Redis 之间的网络通信(一次请求执行多个命令)。
  • 保证多个 Redis 命令的原子性,避免并发问题。
  • 提升性能,避免多次请求带来的开销。
  1. Java 代码只是把 Lua 脚本作为字符串传递给 Redis String luaScript = “redis.call(‘SET’, KEYS[1], ARGV[1])”; 这里 redis.call 还没有执行,只是一个字符串
  2. Java 通过 Redis 客户端(如 Spring Data Redis)把这个 Lua 脚本发送给 Redis stringRedisTemplate.execute( new DefaultRedisScript<>(luaScript, String.class), Collections.singletonList(“name”), “huangkeqin” ); 这里 Java 只是把 Lua 代码交给 Redis 服务器,执行由 Redis 负责。
  3. Redis 服务器接收到 Lua 代码后,在内部执行 redis.call redis.call(‘SET’, ‘name’, ‘huangkeqin’) 这个命令是在 Redis 服务器内部执行的,不会经过 Java 端的处理,也不会被其他命令打断。

🚀 Java + Lua + Redis = 高性能方案

  • Java 只是一个发送 Lua 脚本的客户端,不执行 Redis 命令。
  • Redis 真正执行 Lua 脚本,并在服务器内部完成所有操作。
  • redis.call 必须在 Redis 服务器内部执行,它的作用是执行 Redis 命令,并返回结果给 Java。

Categories:

Tags:

暂时没有回复

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注