找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1567

积分

0

好友

203

主题
发表于 2026-2-15 06:43:17 | 查看: 27| 回复: 0

在多线程、多进程以及分布式环境中,并发访问共享资源是常态。如果控制不当,就会出现数据不一致、更新丢失、超卖、重复执行等严重问题。而锁机制,正是解决这些并发控制难题的核心手段。

本文将围绕 “原理 → 场景 → 代码 → 线上坑 → 架构策略” 这条主线,为你系统梳理悲观锁、乐观锁与分布式锁的正确使用姿势。

一、为什么一定要理解“锁”?

可以一句话总结:

锁的本质:用性能换取一致性。

  • 不加锁:系统并发高,吞吐量可能不错,但数据正确性无法保证。
  • 加锁:数据强一致性得以保障,但往往会牺牲部分性能,导致吞吐下降、延迟上升。

因此,真正的工程能力体现于:在业务可接受的一致性要求与系统性能之间,找到那个精妙的平衡点。


① 悲观锁(Pessimistic Locking)

1️⃣ 核心思想

先上锁,再操作,默认一定会发生冲突。
在访问或修改共享数据之前,就预先获取锁,从而阻塞其他并发线程或事务。

2️⃣ 典型实现

✔ 数据库层面(以MySQL为例)

通过 SELECT ... FOR UPDATE 实现行级锁(需要在事务中)。

START TRANSACTION;

SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;

UPDATE product
SET stock = stock - 1
WHERE id = 1;

COMMIT;

👉 FOR UPDATE 会使该行被锁定,其他尝试读取或修改此行的事务将被阻塞,直到当前事务提交或回滚。

✔ Java 代码层面

使用 ReentrantLocksynchronized 关键字。

private final Lock lock = new ReentrantLock();

public void decrease() {
    lock.lock();
    try {
        if (stock > 0) {
            stock--;
        }
    } finally {
        lock.unlock();
    }
}

3️⃣ 适用场景

  • 写多读少的业务
  • 冲突概率很高的并发更新
  • 强一致性要求极高的场景

典型业务:库存扣减、金融账户转账、余额更新。

4️⃣ 优缺点

优点 缺点
强一致性保障 阻塞等待,性能开销大
实现简单直观 容易引发死锁(如锁顺序不当)
安全可靠 长事务持有锁会严重影响系统吞吐

⚠️ 生产级大坑:FOR UPDATE 变“锁全表”

错误示例

SELECT * FROM product
WHERE stock > 0
FOR UPDATE;

如果 stock 字段没有建立索引,在默认的 REPEATABLE-READ 隔离级别下,InnoDB 会使用 Next-Key Lock 机制,可能锁住整张表或大片索引区间,导致其他所有并发操作被阻塞。

✔ 正确姿势

SELECT stock FROM product
WHERE id = 1
FOR UPDATE;

👉 核心要点:FOR UPDATE 的条件字段必须命中索引!

🎯 实战技巧

  • 缩短事务时间:锁内只做必要的核心操作。
  • 避免锁内远程调用:不要在锁保护的代码块中进行 RPC 或耗时 IO 操作。
  • 使用 tryLock():尝试获取锁,设置超时时间,避免无限期等待导致死锁。

② 乐观锁(Optimistic Locking)

1️⃣ 核心思想

不加锁,先操作,提交时再做冲突检测。
假设并发冲突的概率很低,只在数据提交更新时检查版本号是否被修改过。

2️⃣ 典型实现

✔ 数据库 Version 字段

为数据表增加一个 version 字段,每次更新时作为条件。

UPDATE product
SET stock = stock - 1,
    version = version + 1
WHERE id = 1
  AND version = 10;

👉 执行后,通过检查 SQL 的“影响行数”是否为 0 来判断是否发生了冲突(即其他人已修改过数据)。

✔ Java CAS (Compare And Swap)

利用 JUC 包下的原子类实现无锁更新。

AtomicInteger count = new AtomicInteger(0);

public void increment() {
    int oldVal, newVal;
    do {
        oldVal = count.get();
        newVal = oldVal + 1;
    } while (!count.compareAndSet(oldVal, newVal));
}

3️⃣ 适用场景

  • 读多写少的业务
  • 冲突概率较低的并发场景
  • 追求极致性能,希望减少锁开销

典型业务:文章阅读量统计、点赞数更新、系统配置修改。

4️⃣ 优缺点

优点 缺点
无阻塞,吞吐量高 高并发冲突下,失败重试频繁,性能反而差
不会产生死锁 需要额外维护版本号字段或时间戳
实现相对轻量 CAS 操作存在 “ABA”问题

🔥 线上事故案例:乐观锁“重试风暴”

  • 背景:一个秒杀系统使用数据库乐观锁来扣减库存。
  • 并发:瞬时 QPS 高达 5万。
  • 结果
    • 更新失败率超过 95%。
    • 大量请求进入无限重试循环。
    • 应用服务器 CPU 被打满,响应时间 RT 飙升,最终导致服务雪崩

根本原因乐观锁适用于低冲突场景,而秒杀是典型的高冲突写热点场景,强行使用必然引发“重试风暴”。

✔ 正确优化策略

  • ✅ Redis 预扣库存:将库存热点数据前置到缓存,在缓存层进行预减。
  • ✅ 消息队列削峰:使用 Apache Kafka 或 RabbitMQ 将瞬时高并发请求序列化、异步化处理。
  • ✅ 有限重试 + 退避:为乐观锁更新设置合理的重试次数和退避时间。
int retry = 3;
while (retry-- > 0) {
    if (update()) break;
    Thread.sleep(50); // 退避等待
}

🎯 实战技巧

  • version 字段使用 intbigint 类型。
  • 预估冲突率,冲突高时必须限制重试次数
  • 解决 CAS 的 ABA 问题,可以考虑使用 AtomicStampedReference

③ 分布式锁(Distributed Lock)

1️⃣ 为什么需要分布式锁?

单机锁(如 synchronizedReentrantLock)只能控制单个 JVM 进程内的线程互斥。

  • 🚫 多服务实例部署时无效。
  • 🚫 集群定时任务可能被重复执行。
  • 🚫 多个服务需要互斥访问共享的分布式资源(如一个公共文件)。

2️⃣ 常见实现方式对比

方案 原理简述
DB 唯一索引 向特定表中插入一条代表锁的记录(基于唯一索引),插入成功即获锁。
Redis 利用 SET key value NX PX timeout 命令,实现基于缓存的锁。
ZooKeeper 利用临时顺序节点的特性,客户端通过创建节点和监听来竞争锁。

3️⃣ Redis 分布式锁的正确姿势

基础加锁命令:

SET lock_key unique_client_id NX PX 30000
  • NX:仅当 key 不存在时才设置,实现互斥性。
  • PX 30000:设置锁的自动过期时间为 30 秒,防止持有者崩溃导致锁永远无法释放。
  • unique_client_id:必须使用唯一值(如 UUID),作为锁的持有者标识,用于安全释放。

⚠️ 致命大坑 1:误删他人持有的锁

错误操作

DEL lock_key

风险:如果当前线程持有的锁已经过期自动释放,且被另一个线程获取。此时执行 DEL,就会删除别人刚加上的锁,导致锁完全失效。

✔ 正确解法:使用 Lua 脚本保证原子性
释放锁时,必须先验证 value 是否仍为自己当初设置的值,再执行删除。

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

⚠️ 致命大坑 2:业务未完成,锁已过期

风险:锁的过期时间 TTL 设置过短,业务逻辑还未执行完锁就释放了,导致其他线程也能进入临界区,造成并发问题。

✔ 解决方案

  1. 合理评估 TTL:设置的锁过期时间必须远大于业务逻辑的平均执行时间。
  2. 使用 WatchDog 自动续期:这是 Redisson 等客户端库提供的核心功能,后台线程会定期检查并延长持有锁的过期时间。

4️⃣ Redisson 客户端示例

Redisson 封装了完善的分布式锁实现,解决了上述大部分难题。

RLock lock = redissonClient.getLock("order:" + orderId);
// 尝试获取锁,最多等待5秒,锁持有超时时间为30秒
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
    try {
        // 你的核心业务逻辑在这里执行
        // Redisson 的 WatchDog 会自动为你续期
    } finally {
        lock.unlock(); // 解锁时会自动校验持有者
    }
}

👉 优势:自动续期、可重入、防误删,开箱即用。

🎯 分布式锁最佳实践

  • 锁粒度要细:以业务ID(如order:123)为键,避免大范围锁。
  • 必须设置过期时间:防止死锁。
  • 锁 Value 必须全局唯一:用于标识锁的持有者。
  • 删除前必须校验归属:使用 Lua 脚本保证“获取-校验-删除”的原子性。

④ 三种锁对比总结

维度 悲观锁 乐观锁 分布式锁
核心思想 先锁后操作,防患于未然 先操作后校验,假设冲突少 跨进程/节点的互斥协调
性能特点 较低(有阻塞开销) 很高(无阻塞) 中等(有网络开销)
冲突处理 阻塞等待 重试或失败 竞争获取
主要范围 单机 / 单数据库事务内 单机 / 单数据库 跨服务、跨节点的分布式环境

⑤ 高级架构策略:锁的组合拳

🎯 生产级“双保险”设计

对于极其重要的核心业务流程(如支付回调、订单状态机流转),可以采用组合策略:

1. 分布式锁(Redis/ZK) -> 2. 数据库乐观锁(Version)
  • 第一层:分布式锁保证跨服务实例的全局互斥。
  • 第二层:数据库乐观锁保证即使在极端情况下(如锁超时、网络分区导致脑裂),数据库层面的更新也能保持最终正确性(幂等)。
  • 适用场景:支付处理、订单状态流转、全局幂等性控制。

🎯 秒杀等高并发场景推荐方案

面对瞬时超高并发的写热点,应尽量避免直接竞争数据库锁。

1. Redis 预扣减库存 -> 2. 消息队列削峰 -> 3. 数据库异步最终扣减

这个方案的核心思想是将同步的锁竞争,转化为异步的、串行化的消息处理,从而保护数据库,提升整体吞吐。


⑥ 认知升级:真正的高并发优化方向

一个常见的误区是认为“只要选对了锁,系统性能就能提升”。
实际上,锁本身不会提升性能,它只是一种控制并发、保证正确性的成本开销。

真正的高并发优化方向应该是:

  • 减少锁竞争:通过数据分片(Sharding)、资源隔离等手段。
  • 去共享化设计:使用 ThreadLocal、副本等避免直接共享。
  • 异步化与非阻塞:利用回调、Future、反应式编程模型。
  • 无锁数据结构:如 Disruptor 的 RingBuffer,在特定场景下性能极高。

总结

回顾一下三种锁的核心选择逻辑:

  • 悲观锁:适用于冲突概率高、强一致性要求的场景。
  • 乐观锁:适用于冲突概率低、追求高性能的场景。
  • 分布式锁跨多个服务或进程实现互斥访问的必备工具。

最终的工程原则是:没有“最好”的锁,只有“最适合”当前业务场景的锁。

在实践中,我们常常看到这些组合策略被广泛应用:

  • 乐观锁 + 有限次数重试
  • 分布式锁 + 数据库乐观锁(双保险)
  • Redis 缓存 + 消息队列 + 异步化处理

希望这份从原理到实战的指南,能帮助你在 JavaMySQLRedis 等技术栈中更游刃有余地处理并发问题。如果你有更多的实战经验或疑问,欢迎在 云栈社区 与大家交流探讨。




上一篇:2026届计算机毕业生就业指南:AI算法工程师、公务员与央国企如何选择?
下一篇:港大开源Nanobot:用1%代码量复刻Clawdbot的AI助手,附安装部署教程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 09:01 , Processed in 0.697878 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表