Redis 事务(MULTI/EXEC)与 Lua 脚本的核心区别

先通过对比表清晰梳理核心差异,再结合 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);
    }
}
关键说明
  1. 资源管理 :Spring Data Redis 自动管理 Redis 连接,异常时通过 getConnection().close() 明确关闭资源,符合"资源必须关闭"的要求;
  2. 原子性局限 :若stock:1001是字符串(如"abc"),incrBy会失败,但order:count:1001仍会执行自增,体现"弱原子性";
  3. 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);
    }
}
关键说明
  1. 原子性保障:脚本内"判断库存→扣减库存"是原子操作,Redis单线程执行期间不会处理其他请求,避免"库存超卖";
  2. 资源管理 :通过redisTemplate.getConnectionFactory().getConnection().close()确保异常时释放Redis连接,符合"资源必须关闭"要求;
  3. 脚本复用 :生产环境可将Lua脚本预加载到Redis(通过SCRIPT LOAD),使用EVALSHA执行(仅传SHA1值),减少网络传输开销;
  4. 性能注意:Lua脚本需控制时长(建议<10ms),避免阻塞Redis单线程。

三、核心使用场景总结

技术 核心使用场景 典型案例
MULTI/EXEC 1. 简单批量执行无依赖的Redis命令; 2. 无需条件判断、仅需"批量提交"的场景; 3. 能容忍"部分命令失败"的场景 批量设置缓存、批量更新无依赖的计数key
Lua 脚本 1. 需要原子执行含条件判断/依赖逻辑的操作; 2. 需降低网络往返开销的高频操作; 3. 自定义Redis原子操作(原生命令无法满足) 库存扣减(防超卖)、分布式锁释放、限流算法、红包拆分

四、生产环境最佳实践

  1. MULTI/EXEC

    • 避免在事务中执行过多命令(占用连接时间长);
    • 配合WATCH实现乐观锁,但仅适用于低并发场景;
    • 不依赖"事务回滚",提前校验命令合法性(避免运行时错误)。
  2. Lua 脚本

    • 脚本长度控制在1KB内,避免大脚本阻塞Redis;
    • 脚本内避免循环(尤其是无限循环),防止Redis卡死;
    • 优先使用redis.call(失败抛出异常)而非redis.pcall(失败返回错误),便于捕获问题;
    • 通过SCRIPT LOAD+EVALSHA复用脚本,减少网络传输。
  3. 资源管理通用规则

    • 所有Redis操作必须添加异常处理,确保连接/资源正常关闭;
    • Spring Boot中优先使用RedisTemplate(自动管理连接池),而非裸用Jedis/Lettuce;
    • 连接池配置合理的最大连接数、空闲超时,避免连接泄露。
相关推荐
whn19772 小时前
寻找listener.log
数据库
代码游侠2 小时前
学习笔记——文件I/O
linux·数据库·笔记·学习·算法
Tanjia_kiki2 小时前
无法打开新数据库 ‘test‘。CREATE DATABASE 中止。 (Microsoft SQL Server,错误: 9004)
数据库
铭keny2 小时前
MySQL 误删数据恢复操作手册
数据库·mysql
2的n次方_2 小时前
Catlass 模板库调试调优经验与踩坑记录
服务器·数据库
马克学长2 小时前
SSM舞蹈房管理系统lq4q8(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·舞蹈房管理系统
摇滚侠3 小时前
分布式锁,etcd,redis,ZooKeeper
redis·分布式·etcd
心动啊1213 小时前
简单学习下redis
数据库·redis·学习
xuanloyer3 小时前
oracle从入门到精通--启动与关闭数据库实例
数据库·oracle