一个在并发场景下中特别容易混淆的话题:
Redisson 的分布式锁、MySQL 的乐观锁(version 字段)、还有状态机——这仨到底该用在哪儿?能不能互相替代?
很多同学一遇到“重复提交”或“并发冲突”,就一股脑全上:加个 Redis 锁、搞个 version 字段、再套个 if 判断状态……结果代码又重又慢,还可能漏掉关键问题。
其实,这三种机制根本不是一类东西,它们解决的是不同层面的问题。用错了,轻则性能浪费,重则数据出错。
下面我就结合我们真实的业务场景,把它们的区别说清楚,并告诉你怎么组合才最安全、最高效。
一、先说结论:它们各管一摊事
| 机制 | 解决什么问题? | 本质 |
|---|---|---|
| Redisson 分布式锁 | 控制“谁在操作” | 悲观锁:假设会冲突,先锁住(适用于同步请求) |
| MySQL 乐观锁(version) | 防止“基于旧数据的更新覆盖新数据” | 无锁并发控制:先改,失败再重试 |
| 状态机 | 防止“非法的状态跳转” | 必须放在 UPDATE 的 WHERE 中,作为写时兜底 |
状态机的兜底和乐观锁一样,放进 SQL 的 WHERE 条件里。
二、Redisson 分布式锁:同步请求的主力防线
Redisson 锁的核心思想很简单:
同一时间,只让一个人干这件事。
典型写法:
RLock lock = redisson.getLock("user_rights_update:" + userId);
if (lock.tryLock(0, TimeUnit.SECONDS)) { // 快速失败:拿不到立刻返回
try {
// 更新用户权益
} finally {
lock.unlock();
}
} else {
throw new BusinessException("操作太频繁,请稍后再试");
}
这种 “拿不到锁就直接失败” 的行为,就是典型的 悲观锁策略。
✅ 适合场景:
- 用户实时操作(如充值、消费、修改配置)
- 需要严格串行的高频写入
- 大部分日常同步业务流量
💡 在我们系统里,99% 的同步实时请求靠 Redisson 锁就能稳稳兜住。
但它无法覆盖所有场景——尤其是那些异步、延迟、重试的操作,比如:
- 支付/退款的第三方回调;
- MQ 消息的重复投递或延迟消费;
- 定时任务的补偿逻辑;
- 人工后台重推操作。
这些请求到来时,Redisson 锁早已释放,内存状态也无从谈起。
唯一可信的,只有数据库里持久化的状态和版本号。
所以,状态机 + 乐观锁 的双重 WHERE 条件,不仅是锁失效的兜底,更是整个异步系统的安全基石。
三、MySQL 乐观锁:批量导入、重试场景的救星
这里分享一个我们真实的业务场景:
我们要把一批老系统的客户迁移到新系统,初始化他们的用户权益(积分、等级、资格等)。
这个过程有几个关键特点:
- 数据量大(几十万甚至上百万用户);
- 允许多次重试(比如某次导入中途失败,后续可重新跑);
- 导入期间,用户可能已经在新系统活跃(比如自己注册、充值、消费),导致权益数据已被修改。
如果对每个用户都加 Redisson 锁?
- 锁数量巨大,Redis 压力剧增;
- 导入速度极慢,无法接受;
- 更重要的是——没必要,因为这不是高并发实时请求,而是后台异步任务。
于是我们选择:给用户权益表加上 version 字段,用乐观锁安全更新。
具体做法是:
-
先查询当前用户权益的最新状态和 version:
UserRights current = userRightsMapper.selectById(userId); -
尝试基于当前 version 更新:
UPDATE user_rights SET points = #{newPoints}, level = #{newLevel}, version = version + 1 WHERE user_id = #{userId} AND version = #{currentVersion};
- 如果用户从未在新系统操作过 → 更新成功;
- 如果用户已经充值或修改过权益(version 已变更)→
WHERE条件不满足,更新失败,自动跳过该用户; - 后续重试时,依然会读取最新的 version,不会覆盖用户的真实操作。
✅ 这种设计的优势非常明显:
- 无需加任何锁,性能极高,适合大批量处理;
- 天然支持幂等重试:跑多少次都不怕;
- 保护用户已有数据:绝不覆盖用户在新系统产生的真实行为。
乐观锁的是“基于当前版本号更新”。
四、状态机:真正的兜底是在 UPDATE 的 WHERE 里(尤其为 MQ 而生)
很多人以为状态机就是写个 if 判断:
if (order.getStatus() != OrderStatus.CREATED) {
throw new Exception("不能重复支付");
}
但这种做法在高并发或异步场景下是无效的——因为判断和更新不是原子的。更糟的是,在 MQ 回调、定时任务等场景中,这个 if 根本没有上下文可言。
正确做法:状态条件必须放进 SQL 的 WHERE 子句
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = 123
AND status = 'CREATED' -- ← 状态机兜底:只允许从 CREATED 变 PAID
AND version = 5; -- ← 乐观锁兜底:基于最新版本
只有当当前状态确实是 CREATED 且 version 匹配时,更新才会成功。
这个设计,正是为了应对 MQ、回调、补偿等异步场景而存在的。
🌰 案例:支付回调晚到
- 用户下单 → 系统加锁 → 创建订单(status=CREATED);
- 3 分钟后,MQ 检测到未支付,自动取消订单(status=CANCELLED);
- 5 分钟后,微信支付回调终于到达,尝试将订单设为 PAID。
如果没有状态机兜底:
- 回调代码只查订单是否存在,就直接更新 → 已取消的订单被错误激活,资损!
如果有状态机在 WHERE 中:
status = 'CREATED'条件不满足 → 更新失败 → 安全退出。
这个场景里,Redisson 锁早在下单完成后就释放了,根本覆盖不到回调时刻。真正守住底线的,是状态机 + 乐观锁。
五、正确姿势:一锁、二判、三更新
真正健壮的高并发逻辑,往往是三者按顺序配合:
✅ 第一步:加锁(Redisson)
控制同步请求的并发入口,99% 的实时操作靠它就够了。
✅ 第二步:判断(可选内存预检)
快速拦截明显非法请求(提升用户体验),但不能依赖它做安全校验。
✅ 第三步:更新(乐观锁 + 状态机 双重兜底)
把 version 和 status 都放进 WHERE,让数据库原子地保证一致性。
场景1:用户实时充值(同步)
RLock lock = getLock(userId);
lock.lock();
try {
UserRights rights = loadRights(userId);
if (rights.status == FROZEN) throw error("冻结账户");
boolean success = updateWithVersionAndStatus(
userId, rights.version, RightsStatus.ACTIVE, newBalance
);
if (!success) throw error("请重试");
} finally {
lock.unlock();
}
场景2:MQ 消费支付回调(异步)
// 无锁!直接双重兜底更新
boolean success = updateOrderStatus(
orderId,
expectedVersion, // 可从消息体或 DB 查询
OrderStatus.CREATED, // 只允许从 CREATED 变 PAID
OrderStatus.PAID
);
if (!success) {
log.warn("订单 {} 状态已变更,跳过回调", orderId);
}
六、总结:什么时候用什么?
| 场景 | 推荐方案 |
|---|---|
| 用户实时操作(充值、下单) | Redisson 锁 +(可选预检)+ 乐观锁 + 状态机(WHERE 中) |
| 批量导入、数据迁移 | 乐观锁 + 状态机(WHERE 中),无需加锁 |
| MQ 消费、回调、补偿任务 | 无需锁!直接双重兜底更新 |
| 极端异常兜底(锁失效) | 数据库层的 WHERE 条件是最后防线 |
同步靠锁,异步靠状态;
安全不在 if,而在 WHERE。