上一篇我们深入学习了Redis RDB快照持久化,通过定时全量备份解决了Redis断电丢数据的问题,筑牢了数据安全的第一道防线。而在实际业务中,我们常常需要批量执行多条Redis命令,并且要求这些命令要么全部成功、要么全部不执行,避免中间状态导致数据错乱,比如转账扣款、库存扣减、批量更新缓存等场景。

针对这类批量原子操作需求,Redis提供了轻量级事务机制,依靠MULTI/EXEC/DISCARD/WATCH核心命令实现。不同于数据库的强事务,Redis事务更简洁、性能更高,本篇从零入门Redis事务,聚焦最常用的MULTI/EXEC组合,从命令实操、业务实战到核心局限性,带你吃透Redis事务的正确用法。

核心定位:Redis事务是一组命令的集合执行单元,保证批量命令串行执行、不被打断,实现简单的原子性;但不支持回滚,和关系型数据库事务有本质区别。

一、为什么需要Redis事务?

普通Redis命令是单条执行,高并发场景下多条命令之间会插入其他客户端的请求,导致数据不一致。比如批量扣减多个用户积分、同时更新缓存和计数器,中途被打断就会出现数据异常。

  • 保证多条命令批量执行、不被插队,避免并发干扰
  • 实现简单的业务原子性,杜绝中间状态数据
  • 减少网络IO开销,批量命令一次性提交执行
  • 配合WATCH命令,实现乐观锁,防止并发修改冲突

Redis事务的核心目标:批量命令串行化、一次性执行,杜绝命令插队,保证执行逻辑完整性


二、Redis事务核心命令

Redis事务依靠4个核心命令完成,日常开发中MULTI+EXEC是最基础、最常用的组合,先掌握基础用法,再逐步拓展进阶命令。

命令作用执行时机
MULTI开启事务,标记命令队列开始批量命令执行前
EXEC执行事务,一次性运行队列中所有命令命令入队完毕后
DISCARD放弃事务,清空命令队列,取消事务状态EXEC执行前,取消事务
WATCH key [key...]监听Key,若Key被修改,事务执行失败MULTI开启事务前

Redis事务完整执行流程

  1. 开启事务:执行MULTI命令,Redis进入事务模式
  2. 命令入队:后续执行的Redis命令不会立即运行,而是存入事务队列
  3. 执行/取消事务:执行EXEC批量运行命令,或执行DISCARD清空队列
  4. 返回结果:EXEC执行后,按命令入队顺序返回所有结果

关键特性:命令入队时仅做语法校验,不执行;只有EXEC触发后才会批量执行,事务执行期间不会被其他客户端命令打断。


三、实操:MULTI/EXEC基础用法

通过Redis客户端,演示最基础的事务执行流程,实现批量设置用户信息、更新积分的原子操作。

1. 正常执行事务(成功案例)

# 1. 开启事务
127.0.0.1:6379> MULTI
OK

# 2. 命令入队(批量操作)
127.0.0.1:6379(MULTI)> SET user:1001:name "张三"
QUEUED  # 命令入队成功,返回QUEUED
127.0.0.1:6379(MULTI)> INCRBY user:1001:score 10
QUEUED
127.0.0.1:6379(MULTI)> SET user:1001:age 25
QUEUED

# 3. 执行事务,批量运行所有命令
127.0.0.1:6379(MULTI)> EXEC
1) OK  # 第一条命令结果
2) (integer) 10  # 第二条命令结果
3) OK  # 第三条命令结果

2. 放弃事务(清空队列)

# 开启事务
127.0.0.1:6379> MULTI
OK

# 命令入队
127.0.0.1:6379(MULTI)> SET test:key 123
QUEUED

# 放弃事务,清空队列
127.0.0.1:6379(MULTI)> DISCARD
OK

# 再次执行命令,事务已取消
127.0.0.1:6379> GET test:key
(nil)

3. 命令语法错误(事务入队失败)

如果命令存在语法错误,入队时会直接报错,EXEC执行后整个事务全部不执行。

127.0.0.1:6379> MULTI
OK
# 错误命令,语法不合法
127.0.0.1:6379(MULTI)> SETTT user:1001 1
(error) ERR unknown command `SETTT`, with args beginning with: `user:1001`, `1`,
# 执行事务,因入队错误,事务失败
127.0.0.1:6379(MULTI)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

四、实战:SpringBoot整合Redis事务

基于SpringBoot+RedisTemplate,实现代码层面的Redis事务,适配批量更新缓存、计数器同步等业务场景。

第一步:开启RedisTemplate事务支持

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // 开启事务支持
        template.setEnableTransactionSupport(true);
        // 序列化配置
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

第二步:代码实现MULTI/EXEC事务

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class RedisTransactionService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 批量更新用户信息(Redis事务实现)
     * @param userId 用户ID
     * @param name 用户名
     * @param score 积分
     * @return 事务执行结果
     */
    public List<Object> batchUpdateUserInfo(Long userId, String name, Integer score) {
        try {
            // 1. 开启事务
            redisTemplate.multi();
            // 2. 命令入队
            String keyPrefix = "user:" + userId + ":";
            redisTemplate.opsForValue().set(keyPrefix + "name", name);
            redisTemplate.opsForValue().increment(keyPrefix + "score", score);
            redisTemplate.opsForValue().set(keyPrefix + "status", 1);
            // 3. 执行事务,返回批量结果
            return redisTemplate.exec();
        } catch (Exception e) {
            // 异常时放弃事务
            redisTemplate.discard();
            throw new RuntimeException("Redis事务执行失败", e);
        }
    }
}

五、Redis事务的核心局限性(重点避坑)

很多新手会把Redis事务和MySQL事务等同,这是极大的误区,Redis事务不支持强一致性、不支持回滚,存在明显局限性。

1. 不支持命令执行失败回滚

如果命令入队时语法正确,执行时出现业务错误(如对String执行INCR),Redis不会回滚已执行的命令,错误命令会返回失败,其他命令继续执行。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(MULTI)> SET num abc  # 设置字符串
QUEUED
127.0.0.1:6379(MULTI)> INCR num     # 对字符串自增,执行时失败
QUEUED
127.0.0.1:6379(MULTI)> SET flag 1
QUEUED
# 执行事务
127.0.0.1:6379(MULTI)> EXEC
1) OK
2) (error) ERR value is not an integer or out of range  # 单条命令失败
3) OK  # 其他命令正常执行,无回滚

2. 不支持隔离级别

Redis事务仅保证自身命令不被打断,无法实现读未提交、读已提交等隔离级别,事务执行期间其他客户端仍可修改数据。

3. 无原子性保障(严格意义)

Redis事务仅保证命令批量执行,不满足ACID中的原子性,部分命令失败不会整体回滚,仅语法错误会导致整个事务取消。

4. 性能开销

事务命令需入队、批量执行,大量长事务会占用Redis内存,影响性能,不建议开启超大事务。


六、Redis事务适用场景与替代方案

✅ 适合场景

  • 批量执行Redis命令,避免命令插队
  • 缓存批量更新、计数器同步、简单数据写入
  • 对性能要求高、允许少量数据不一致的场景
  • 配合WATCH实现简单乐观锁,防止并发冲突

❌ 不适合场景

  • 强事务需求:金融转账、订单支付、库存扣减(严格一致性)
  • 需要回滚机制的核心业务
  • 长事务、大批量命令执行

强一致性替代方案

  • 分布式锁(SETNX):控制并发访问,保证单线程执行
  • 消息队列:异步批量处理,保证最终一致性
  • 数据库事务:核心业务优先使用MySQL等强事务

七、Redis事务最佳实践

  • 保持事务精简:尽量缩短事务逻辑,减少命令数量,降低内存占用
  • 禁用长事务:避免在事务中嵌套耗时业务,防止阻塞Redis
  • 提前校验数据:入队前做好参数校验,避免执行时出现业务错误
  • 高并发场景配合WATCH:监听关键Key,防止并发修改导致数据错乱
  • 核心业务不用Redis事务:改用分布式锁+数据库事务,保证数据安全

Redis事务速记:MULTI开事务,命令入队列;EXEC批量跑,出错不回滚;轻量性能高,强事务别用。 核心命令:MULTI(开启)、EXEC(执行)、DISCARD(放弃)

结语与下篇预告

本篇我们掌握了Redis事务MULTI/EXEC的基础用法、实战落地和核心局限性,理清了Redis事务与数据库事务的本质区别,避免了业务踩坑。

发表回复