1.1 事务的本质是什么?

在餐厅点餐时,我们常常把多个菜品一次性交给服务员——这就是事务的朴素理解。Redis事务的本质就是将多个命令打包成一个原子操作序列。想象你正在开发一个秒杀系统,扣库存、记录订单、发优惠券这三个动作必须同时成功或失败,这就是事务存在的意义。

1.2 与传统数据库的对比

相较于传统关系型数据库的事务(ACID特性):

  • Redis事务不支持回滚(ROLLBACK),但可通过DISCARD放弃执行
  • 执行期间不会中断,适合高并发场景
  • 命令先入队后执行(类似批处理)

2. Java操作Redis事务的完整代码示例

(Jedis技术栈)

2.1 基础事务演示

// 创建Jedis连接
try (Jedis jedis = new Jedis("localhost", 6379)) {
    
    // 开启事务
    Transaction tx = jedis.multi();
    
    // 连续发送命令到队列
    tx.set("user:101:status", "locked");
    tx.incrBy("inventory:item1001", -1);
    tx.sadd("orders:202308", "order10001");
    
    // 执行事务(返回所有命令的响应列表)
    List<Object> results = tx.exec();
    
    // 处理执行结果
    for(Object result : results) {
        System.out.println("命令执行结果:" + result);
    }
} catch (Exception e) {
    // 异常时自动调用DISCARD
    System.out.println("事务执行失败:" + e.getMessage());
}

2.2 带有Watch的乐观锁实现

jedis.watch("account:2001:balance"); // 监控关键键

try {
    int currentBalance = Integer.parseInt(jedis.get("account:2001:balance"));
    if(currentBalance >= 500) {
        Transaction tx = jedis.multi();
        tx.decrBy("account:2001:balance", 500);
        tx.incrBy("account:2002:balance", 500);
        List<Object> results = tx.exec();
        if(results == null) {
            System.out.println("转账失败:余额被其他操作修改");
        }
    } else {
        jedis.unwatch();
        System.out.println("余额不足");
    }
} catch (Exception e) {
    jedis.unwatch();
    System.out.println("系统异常:" + e.getMessage());
}

2.3 事务中的错误处理模式

Transaction tx = jedis.multi();
try {
    // 错误命令示例:对字符串进行集合操作
    tx.sadd("user:101:profile", "vip"); // 正确用法
    tx.get("invalidKey").trim();        // 故意制造语法错误
    tx.exec();  // 此处不会执行,在入队时就检测到错误
} catch (JedisDataException e) {
    System.out.println("命令入队错误:" + e.getMessage());
    tx.discard();
}

3. Redis事务的四大核心命令解析

3.1 MULTI:事务启动开关

  • 语法:MULTI
  • 作用:标志事务开始,后续命令进入队列

3.2 EXEC:事务执行器

  • 语法:EXEC
  • 效果:
    • 执行队列中的所有命令
    • 返回命令执行结果的数组

3.3 DISCARD:事务终止符

  • 语法:DISCARD
  • 注意点:
    • 会清空命令队列
    • 自动取消WATCH监控

3.4 WATCH:数据变更监听者

  • 使用场景:转账前的余额监控
  • 实现原理:基于CAS(Compare and Swap)机制
  • 重要特性:
    // 监控多个key的示例
    jedis.watch("key1", "key2", "key3");
    
    // 当任一被监控键被修改时事务将失败
    

4. 事务应用场景深度解析

4.1 库存精确扣减系统

在电商秒杀场景中:

Transaction tx = jedis.multi();
tx.decr("product:1001:stock");
tx.hincrBy("user:5001:activity", "seckill_count", 1);
tx.exec();

4.2 分布式锁的原子续期

// 续期操作的原子性保障
if(jedis.setnx("lock:order", "client1") == 1) {
    Transaction tx = jedis.multi();
    tx.expire("lock:order", 30);
    tx.pexpire("order:status", 30000);
    tx.exec();
}

4.3 用户行为批处理

收集用户行为日志时:

jedis.multi()
    .rpush("user:log:1001", "click:buttonA")
    .rpush("user:log:1001", "view:product100")
    .expire("user:log:1001", 3600)
    .exec();

5. Redis事务的优缺点全景剖析

5.1 优势集中体现

  1. 性能优异:10万次事务操作仅需约1.2秒(基准测试数据)
  2. 原子性保障:命令队列的全执行或全放弃
  3. 监控机制灵活:WATCH实现乐观锁控制

5.2 局限性认知

  • 无回滚机制:需自行实现补偿逻辑
  • 不支持条件判断:需依赖WATCH实现类似功能
  • 长事务阻塞风险:单线程架构下的性能陷阱

6. 开发者必须掌握的注意事项

6.1 事务中的Lua脚本优化

String luaScript = 
    "local current = redis.call('GET', KEYS[1])\n" +
    "if tonumber(current) >= tonumber(ARGV[1]) then\n" +
    "    redis.call('DECRBY', KEYS[1], ARGV[1])\n" +
    "    return 1\n" +
    "else\n" +
    "    return 0\n" +
    "end";

// 执行原子操作
Object result = jedis.eval(luaScript, 1, "inventory:item2001", "5");

6.2 Spring整合Redis事务的要点

@Configuration
public class RedisConfig {

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setEnableTransactionSupport(true);
        // 其他配置项...
        return template;
    }
}

7. 从架构角度看待Redis事务

7.1 集群环境下的限制

  • 所有操作必须落在一个节点(Hash Tag的使用技巧)
  • 事务与Pipeline的结合应用

7.2 事务监控的黄金指标

  • 事务执行成功/失败率
  • 平均事务耗时(通常应<5ms)
  • Watch命令调用频率

8. 文章总结与实践建议

Redis事务在分布式系统中扮演着轻量级原子操作执行者的角色。当遇到需要批量操作且需要原子性的场景时,应该优先考虑Redis事务方案。但需要特别注意:

  1. 提前规划键值结构:良好的数据结构设计可以减少事务复杂度
  2. 事务隔离级别验证:根据业务需求测试竞态条件
  3. 压力测试不可少:建议在预发布环境进行全链路压测

实践中的两个黄金法则:

  • 简单事务直接用原生命令
  • 复杂业务优先采用Lua脚本