先通过对比表清晰梳理核心差异,再结合 Java 17+ + Spring Boot 代码示例说明各自使用场景:
| 对比维度 | MULTI/EXEC 事务 | Lua 脚本 |
|---|---|---|
| 原子性 | 弱原子性:语法错误会放弃所有命令;运行时错误(如对字符串做自增)仅失败该命令,其余继续执行 | 强原子性:脚本内所有命令作为整体原子执行,要么全成功,要么全失败(脚本执行中Redis单线程不处理其他请求) |
| 逻辑能力 | 仅支持批量命令入队,无条件判断、循环、变量等逻辑 | 支持完整Lua语法(条件、循环、变量、函数),可基于前序命令结果执行后续逻辑 |
| 网络开销 | 多次网络交互(MULTI→命令入队→EXEC) | 一次网络交互(脚本传输+执行),大幅降低网络往返 |
| 错误处理 | 无自定义错误处理,仅被动接受Redis的执行结果 | 可在脚本内捕获错误、自定义兜底逻辑(如redis.call失败时返回特定值) |
| 资源占用 | 事务期间占用客户端连接,直到EXEC/放弃 | 脚本执行期间占用Redis单线程,长脚本会阻塞所有请求(需控制脚本时长) |
| 复用性 | 不可复用,每次需重新入队所有命令 | 可通过EVALSHA缓存脚本到Redis,重复执行仅传脚本SHA1值,复用性高 |
| 适用复杂度 | 仅支持简单批量命令,无依赖逻辑 | 支持复杂业务逻辑(依赖前序结果、条件判断、原子操作封装) |
一、MULTI/EXEC 事务:适用场景与代码示例
适用场景
仅需简单批量执行无依赖的Redis命令,且能容忍"运行时错误不回滚"的场景,例如:
- 用户下单后批量更新"库存""订单数"两个key(无库存判断,仅简单写操作);
- 批量设置多个缓存key,无需依赖前序命令结果。
Java 17+ + Spring Boot 代码示例
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* MULTI/EXEC 事务示例(Spring Boot 3.x + Java 17+)
* 场景:用户下单后批量扣减库存、增加订单数(无库存判断)
*/
@Slf4j
@Service
public class RedisTransactionService {
private final RedisTemplate<String, Object> redisTemplate;
// 构造器注入(Spring Boot 3.x 推荐)
public RedisTransactionService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// Java 17 Record:事务执行结果
public record TransactionResult(boolean success, String msg, List<Object> execResults) {}
/**
* 执行MULTI/EXEC事务:扣减库存 + 增加订单数
*/
public TransactionResult executeOrderTransaction(String productKey, String orderCountKey, int deductNum) {
try {
// RedisCallback 执行底层事务操作,try-with-resources 管理Redis连接(Spring自动兜底)
List<Object> execResults = redisTemplate.execute((RedisCallback<List<Object>>) connection -> {
// 开启事务
connection.multi();
// 命令入队:扣减库存(INCRBY 负数实现扣减)
connection.incrBy(productKey.getBytes(), -deductNum);
// 命令入队:增加订单数
connection.incrBy(orderCountKey.getBytes(), 1);
// 执行事务(原子提交)
return connection.exec();
});
// Java 17 switch表达式:处理执行结果
String msg = switch (execResults) {
case null -> "事务被放弃(如WATCH监控的key被修改)";
case List<Object> res when res.isEmpty() -> "事务执行无结果(语法错误)";
default -> "事务执行成功";
};
return new TransactionResult(execResults != null && !execResults.isEmpty(), msg, execResults);
} catch (Exception e) {
log.error("MULTI/EXEC事务执行失败", e);
// 异常时明确关闭Redis连接(Spring Data Redis已封装,此处兜底)
redisTemplate.getConnectionFactory().getConnection().close();
return new TransactionResult(false, "执行异常:" + e.getMessage(), null);
}
}
// 测试入口
public static void main(String[] args) {
// Spring Boot 环境需通过ApplicationContext获取Bean,此处简化模拟
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(org.springframework.data.redis.connection.jedis.JedisConnectionFactory());
RedisTransactionService service = new RedisTransactionService(redisTemplate);
// 执行事务:扣减商品1001库存2个,订单数+1
TransactionResult result = service.executeOrderTransaction("stock:1001", "order:count:1001", 2);
log.info("事务结果:{}", result);
}
}
关键说明
- 资源管理 :Spring Data Redis 自动管理 Redis 连接,异常时通过
getConnection().close()明确关闭资源,符合"资源必须关闭"的要求; - 原子性局限 :若
stock:1001是字符串(如"abc"),incrBy会失败,但order:count:1001仍会执行自增,体现"弱原子性"; - WATCH 补充:MULTI/EXEC 可配合 WATCH 实现"乐观锁"(监控key,若被修改则事务放弃),但仍无法解决"基于前序结果执行后续命令"的场景。
二、Lua 脚本:适用场景与代码示例
适用场景
需要原子执行复杂逻辑(含条件判断、依赖前序结果)、或需降低网络开销的场景,例如:
- 扣减库存前判断库存是否充足(库存不足则直接返回失败,不执行扣减);
- 分布式锁的原子释放(判断锁归属后再删除,避免误删);
- 限流算法(如令牌桶、漏桶,需原子计算剩余令牌)。
Java 17+ + Spring Boot 代码示例
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
/**
* Lua脚本示例(Spring Boot 3.x + Java 17+)
* 场景:扣减库存前判断库存是否充足(原子逻辑)
*/
@Slf4j
@Service
public class RedisLuaScriptService {
private final RedisTemplate<String, Object> redisTemplate;
public RedisLuaScriptService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// Java 17 Record:Lua脚本执行结果
public record LuaResult(boolean success, String msg, Long data) {}
// 库存扣减Lua脚本(原子判断+扣减)
private static final String DEDUCT_STOCK_LUA = """
-- 获取参数:库存key、扣减数量
local stockKey = KEYS[1]
local deductNum = tonumber(ARGV[1])
-- 获取当前库存
local currentStock = tonumber(redis.call('get', stockKey))
if not currentStock then
return -1 -- 库存key不存在
end
-- 判断库存是否充足
if currentStock < deductNum then
return 0 -- 库存不足
end
-- 原子扣减库存
redis.call('incrby', stockKey, -deductNum)
return currentStock - deductNum -- 返回扣减后库存
""";
/**
* 执行Lua脚本扣减库存(原子逻辑)
*/
public LuaResult deductStockWithLua(String stockKey, int deductNum) {
// 定义Redis脚本(指定返回类型为Long)
RedisScript<Long> luaScript = new DefaultRedisScript<>(
DEDUCT_STOCK_LUA,
Long.class
);
try {
// try-with-resources 思想:Spring自动管理脚本执行的连接资源,异常时兜底关闭
Long result = redisTemplate.execute(
luaScript,
Collections.singletonList(stockKey), // KEYS参数
deductNum // ARGV参数
);
// Java 17 switch表达式:处理脚本返回值
String msg = switch (result) {
case -1L -> "库存key不存在";
case 0L -> "库存不足,扣减失败";
case null -> "脚本执行异常";
default -> "库存扣减成功,剩余库存:" + result;
};
boolean success = result != null && result > 0;
return new LuaResult(success, msg, result);
} catch (Exception e) {
log.error("Lua脚本执行失败", e);
// 明确关闭Redis连接(释放资源)
redisTemplate.getConnectionFactory().getConnection().close();
return new LuaResult(false, "执行异常:" + e.getMessage(), null);
}
}
// 测试入口
public static void main(String[] args) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(org.springframework.data.redis.connection.jedis.JedisConnectionFactory());
RedisLuaScriptService service = new RedisLuaScriptService(redisTemplate);
// 先初始化库存:set stock:1001 10
redisTemplate.opsForValue().set("stock:1001", 10);
// 执行Lua脚本扣减3个库存
LuaResult result = service.deductStockWithLua("stock:1001", 3);
log.info("Lua脚本执行结果:{}", result);
}
}
关键说明
- 原子性保障:脚本内"判断库存→扣减库存"是原子操作,Redis单线程执行期间不会处理其他请求,避免"库存超卖";
- 资源管理 :通过
redisTemplate.getConnectionFactory().getConnection().close()确保异常时释放Redis连接,符合"资源必须关闭"要求; - 脚本复用 :生产环境可将Lua脚本预加载到Redis(通过
SCRIPT LOAD),使用EVALSHA执行(仅传SHA1值),减少网络传输开销; - 性能注意:Lua脚本需控制时长(建议<10ms),避免阻塞Redis单线程。
三、核心使用场景总结
| 技术 | 核心使用场景 | 典型案例 |
|---|---|---|
| MULTI/EXEC | 1. 简单批量执行无依赖的Redis命令; 2. 无需条件判断、仅需"批量提交"的场景; 3. 能容忍"部分命令失败"的场景 | 批量设置缓存、批量更新无依赖的计数key |
| Lua 脚本 | 1. 需要原子执行含条件判断/依赖逻辑的操作; 2. 需降低网络往返开销的高频操作; 3. 自定义Redis原子操作(原生命令无法满足) | 库存扣减(防超卖)、分布式锁释放、限流算法、红包拆分 |
四、生产环境最佳实践
-
MULTI/EXEC:
- 避免在事务中执行过多命令(占用连接时间长);
- 配合
WATCH实现乐观锁,但仅适用于低并发场景; - 不依赖"事务回滚",提前校验命令合法性(避免运行时错误)。
-
Lua 脚本:
- 脚本长度控制在1KB内,避免大脚本阻塞Redis;
- 脚本内避免循环(尤其是无限循环),防止Redis卡死;
- 优先使用
redis.call(失败抛出异常)而非redis.pcall(失败返回错误),便于捕获问题; - 通过
SCRIPT LOAD+EVALSHA复用脚本,减少网络传输。
-
资源管理通用规则:
- 所有Redis操作必须添加异常处理,确保连接/资源正常关闭;
- Spring Boot中优先使用
RedisTemplate(自动管理连接池),而非裸用Jedis/Lettuce; - 连接池配置合理的最大连接数、空闲超时,避免连接泄露。