带你从业务代码了解在使用分布式锁后为什么需要乐观锁兜底? Redis在单机、主从复制、集群情况下的潜在一致性问题

5 阅读3分钟

一、Redis 分布式锁真的靠谱吗?聊聊它为什么必须用乐观锁兜底?

在并发场景下,很多人第一反应都是:

“加个 Redis 分布式锁就好了。”

那么是否所有场景下 就简单的加一个Redis分布式锁就能解决问题了呢?
本篇文章将给你介绍
1、面试中 面试官问题 Redis 是否是强一致的 你如何回答?
2、实际业务中 (主从 集群Redis)是否分布式锁会存在问题?
3、带你用实际的业务代码去使用乐观锁去兜底。

二、Redis不是强一致性的

先给出场景:

  • Redis:主从 + 哨兵
  • 服务:多实例部署
  • 场景:一段“理论上只能执行一次”的逻辑

发生了什么?

  1. 实例 A 在 主节点 成功写入锁
  2. 这条锁数据 还没同步到从节点
  3. 主节点突然宕机
  4. 从节点被提升为新主
  5. 实例 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节点出问题 那么当前的锁就会丢失

所以不管如何都要有一个兜底机制:数据库的乐观锁