上一篇我们深度拆解了Redis过期策略与内存淘汰机制,守住了内存安全底线,让Redis在高并发下不会因内存溢出宕机。但在分布式、多线程场景中,多个请求同时操作Redis数据时,很容易出现并发修改、数据错乱、超卖/超扣等问题,这时候就需要依靠事务锁机制来保证操作原子性、维护数据一致性。

很多人会混淆Redis事务和传统数据库事务,也分不清分布式锁的选型与坑点。本篇就从Redis事务原理、命令实操、局限性,到分布式锁实现、优化方案、避坑指南,彻底讲透如何用Redis保证原子操作,解决各类并发冲突问题。

核心定位:Redis事务保证单客户端命令批量执行的原子性,分布式锁解决多客户端/多服务的并发竞争问题,二者配合覆盖绝大多数Redis并发场景。

一、Redis事务:批量命令的原子性执行

1. 核心原理:与传统DB事务的区别

Redis事务是批量命令的队列执行,通过MULTI、EXEC等命令把一组命令打包,一次性执行,具备隔离性、一次性,但和MySQL等关系型数据库事务有本质区别:

  • 无回滚特性:某条命令执行失败,其余命令仍会继续执行,不会回滚(Redis设计取舍,保证高性能)
  • 不保证完全ACID:满足隔离性、一致性,但不支持回滚(原子性残缺),持久性依赖持久化配置
  • 执行期间独占:事务队列执行时,不会被其他客户端命令打断,保证隔离性

2. 事务核心命令(4个)

命令作用说明
MULTI开启事务标记事务开始,后续命令进入队列,不立即执行
EXEC执行事务批量执行队列中所有命令,返回每条命令结果
DISCARD放弃事务清空事务队列,取消事务执行
WATCH监听Key监控Key是否被修改,若修改则事务执行失败(乐观锁)

3. 事务实操:标准执行流程

# 1. 开启事务
MULTI
# 2. 命令入队(依次执行读写命令)
SET user:1001 "zhangsan"
INCR user:count
HSET user:info:1001 age 25 sex 1
# 3. 执行事务,返回批量结果
EXEC

# 放弃事务示例
MULTI
SET key1 value1
DISCARD  # 清空队列,事务取消

4. WATCH乐观锁:解决事务并发竞争

Redis事务默认不处理并发修改,通过WATCH监听关键Key,可实现乐观锁效果:

  • 执行MULTI前先用WATCH key监听目标Key
  • 若事务执行前,Key被其他客户端修改,EXEC会返回nil,事务执行失败
  • 业务层可重试事务,保证数据一致性
# 监听库存Key,防止超扣
WATCH goods:stock:1001
MULTI
DECRBY goods:stock:1001 1
EXEC
# 若EXEC返回nil,说明库存被修改,需重试

5. Redis事务局限性

  • 不支持命令回滚,单条失败不影响整体执行
  • 不支持跨Key事务,集群环境下无法操作多个Slot的Key
  • 仅适用于单客户端、简单批量操作,分布式场景无能为力

关键结论:Redis事务适合单服务、批量命令、无强一致性场景;分布式高并发场景,必须依靠分布式锁保证原子性。


二、Redis分布式锁:解决跨服务并发冲突

1. 为什么需要分布式锁?

单机Java锁(synchronized、Lock)仅适用于单JVM进程,分布式架构下多服务、多节点部署时,单机锁失效,会出现:

  • 商品超卖、优惠券重复领取
  • 接口重复提交、数据重复修改
  • 库存超扣、余额错乱

Redis分布式锁利用单线程执行特性,实现跨服务的互斥访问,是生产最常用的分布式锁方案。

2. 基础版分布式锁:SETNX实现

核心原理

利用SETNX key value(SET if Not eXists)命令,Key不存在时才设置成功,代表加锁成功;已存在则加锁失败,其他线程等待。

实操命令

# 加锁:设置锁Key,同时加过期时间(防止死锁)
SET lock:goods:1001 1 EX 10 NX
# 解锁:删除锁Key
DEL lock:goods:1001

避坑点

  • ❌ 禁止单独用SETNX+EXPIRE:两条命令非原子,加锁后宕机会导致死锁
  • ✅ 必须用SET key value EX 过期时间 NX:原子加锁+设超时

3. 进阶版:防误删、可重入分布式锁

问题1:锁被其他线程误删

线程A加锁后执行业务超时,锁自动过期,线程B加锁成功,线程A执行完删除锁,会删掉线程B的锁。

解决方案:锁Value设为唯一标识(UUID+线程ID),解锁前先校验归属,仅自己能删锁。

// 加锁:存入唯一标识
String lockKey = "lock:goods:" + goodsId;
String requestId = UUID.randomUUID().toString();
boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);

// 解锁:先校验再删除(Lua脚本保证原子性)
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList(lockKey), requestId);

问题2:锁不可重入,同一线程重复加锁死锁

解决方案:基于Redis Hash实现可重入锁,记录加锁次数,重入时计数+1,解锁时计数-1,计数为0再删除锁(Redisson框架已封装)。

4. 生产级方案:Redisson分布式锁

基础手写锁存在锁超时、主从切换丢锁、不可重入等问题,生产环境直接用Redisson框架,封装了完善的分布式锁实现:

  • 支持可重入、公平锁、红锁(解决集群丢锁)
  • 自动续期(看门狗机制),避免业务未执行完锁过期
  • 解锁原子化,防误删,兼容单机、哨兵、集群架构
// Redisson锁实操
@Autowired
private RedissonClient redissonClient;

public void deductStock(Long goodsId) {
    String lockKey = "lock:goods:" + goodsId;
    RLock lock = redissonClient.getLock(lockKey);
    try {
        // 加锁:等待时间10s,锁超时30s
        boolean tryLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
        if (tryLock) {
            // 执行业务:扣减库存
            doDeductStock(goodsId);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 解锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

5. 分布式锁避坑终极清单

  • 必须设置锁超时时间,杜绝死锁
  • 加锁+设超时必须是原子操作
  • 解锁前校验锁归属,防止误删
  • 集群环境用Redisson红锁,避免主从切换丢锁
  • 避免锁粒度太粗,降低并发效率

三、Redis事务 vs 分布式锁:场景选型

维度Redis事务分布式锁
适用范围单客户端、批量命令执行分布式、多服务、多线程并发竞争
原子性批量命令执行原子,无回滚代码块互斥执行,保证业务原子性
复杂度低,命令简单高,需考虑超时、误删、重入等
典型场景批量读写、简单计数秒杀扣库存、防重复提交、分布式限流

四、总结与核心要点

  • Redis事务是命令队列批量执行,无回滚、适合单机简单场景,配合WATCH实现乐观锁
  • 分布式锁解决跨服务并发冲突,基础版用SETNX原子命令,生产直接用Redisson
  • 保证Redis原子操作的核心:单命令天然原子,多命令用事务,分布式用锁

生产建议:优先用分布式锁处理并发冲突,Redis事务仅用于无依赖的批量命令执行,复杂业务务必结合Lua脚本进一步保证原子性。

发表回复