带你从业务代码了解在使用分布式锁后为什么需要乐观锁兜底? Redis在单机、主从复制、集群情况下的潜在一致性问题-CSDN博客
最新推荐文章于 2026-03-01 17:44:30 发布
原创 于 2026-01-23 15:23:55 发布 · 公开 · 369 阅读
·
12
·
6 ·
CC 4.0 BY-SA版权
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
一、Redis 分布式锁真的靠谱吗?聊聊它为什么必须用乐观锁兜底?
在并发场景下,很多人第一反应都是:
“加个 Redis 分布式锁就好了。”
那么是否所有场景下 就简单的加一个Redis分布式锁就能解决问题了呢?
本篇文章将给你介绍
1、面试中 面试官问题 Redis 是否是强一致的 你如何回答?
2、实际业务中 (主从 集群Redis)是否分布式锁会存在问题?
3、带你用实际的业务代码去使用乐观锁去兜底。
二、Redis不是强一致性的
先给出场景:
- Redis:主从 + 哨兵
- 服务:多实例部署
- 场景:一段“理论上只能执行一次”的逻辑
发生了什么?
- 实例 A 在 主节点 成功写入锁
- 这条锁数据 还没同步到从节点
- 主节点突然宕机
- 从节点被提升为新主
- 实例 B 在新主上再次成功加锁
结果是:
A 和 B 同时认为自己拿到了锁
更何况还存在加锁 锁已经过期了 但是业务还没有执行的情况,所以Redis 锁并不是强一致性的。
三、解决方法:乐观锁兜底
Redis 分布式锁的核心作用,是“降低并发”,而不是“保证绝对正确”。
常见的两种乐观锁方式
1、 version 版本号
UPDATE order
SET status = 'PAID',
version = version + 1
WHERE id = 123
AND version = 7;
- 影响行数 = 1:更新成功
- 影响行数 = 0:说明被并发修改过
2、 基于状态的条件更新
UPDATE order
SET status = 'PAID'
WHERE id = 123
AND status = 'UNPAID';
这种方式的好处是:
- 不依赖额外字段
- 天然符合业务流程
- 不存在“非法状态跳转”
四、实际代码去理解乐观锁
public void updateByBizKeyWithOptimisticLock(GenericEntityDTO dto, String operatorId) {
// 1) 参数校验(简化)
Assert.notNull(dto, "dto must not be null");
Assert.hasText(dto.getBizKey(), "bizKey must not be empty");
Assert.notNull(dto.getTenantId(), "tenantId must not be null");
Assert.notNull(dto.getVersion(), "version must not be null");
// 2) 审计字段(示例)
dto.setLastModifiedBy(operatorId);
dto.setLastModifiedAt(new Date());
Integer oldVersion = dto.getVersion();
// 3) 核心:where 带 version = oldVersion,并且 set version = oldVersion + 1
int updatedRows = baseMapper.update(
null,
Wrappers.<GenericEntityPO>lambdaUpdate()
// --- 需要更新的字段(按需 set,避免把 null 覆盖到 DB)
.set(dto.getStatus() != null, GenericEntityPO::getStatus, dto.getStatus())
.set(dto.getRemark() != null, GenericEntityPO::getRemark, dto.getRemark())
.set(GenericEntityPO::getLastModifiedBy, dto.getLastModifiedBy())
.set(GenericEntityPO::getLastModifiedAt, dto.getLastModifiedAt())
// --- 乐观锁:版本号 + 1(注意:更新条件用旧版本)
.set(GenericEntityPO::getVersion, oldVersion + 1)
// --- 更新条件:业务主键 + tenant + version(旧值)
.eq(GenericEntityPO::getBizKey, dto.getBizKey())
.eq(GenericEntityPO::getTenantId, dto.getTenantId())
.eq(GenericEntityPO::getVersion, oldVersion)
);
// 4) updatedRows 必须为 1,否则说明版本冲突 / 记录不存在
if (updatedRows != 1) {
throw new OptimisticLockingFailureException(
"Optimistic lock conflict or record not found, bizKey=" + dto.getBizKey()
+ ", version=" + oldVersion
);
}
}
五、主从复制 集群情况下 的一致性问题
主从情况下 主节点写成功 不等于从节点同步
集群情况下 key 首先通过hash 映射到某一个master
意味着锁只存在于一个节点,所以如何这个master节点出问题 那么当前的锁就会丢失
所以不管如何都要有一个兜底机制:数据库的乐观锁

浙公网安备 33010602011771号