乐观锁怎么用它处理多线程问题的?
1 个回答
请优先考虑优先考虑你的人。吉尔伯特## 1)它是什么? **乐观锁(Optimistic Concurrency Control, OCC)**不加互斥锁阻塞线程,而是让线程先读数据并计算,**提交写入时检查版本是否被别人改过**。如果被改过就**失败并重试/放弃**。 核心是“**检测冲突**而不是预防冲突”。 适用:**读多写少、冲突概率低**的场景(热点不高)。不适用:高冲突时会出现“重试风暴”。 ## 2)怎么用? ### A. 内存并发(Java 层)——CAS/原子类/StampedLock #### 方案 A1:CAS 原子类(`Atomic*`) ```java AtomicInteger stock = new AtomicInteger(100); boolean decOnce(int n) { while (true) { int cur = stock.get(); if (cur < n) return false; // 不够则失败 if (stock.compareAndSet(cur, cur - n)) { // CAS 成功表示没人改过 return true; } // CAS 失败:有人改过,重试(可加退避) } } ``` 要点: * \*\*比较并交换(CAS)\*\*是硬件指令,失败重试即可。 * 高并发下加**指数退避**或限制最大重试次数,避免 CPU 自旋打满。 #### 方案 A2:解决 ABA —— `AtomicStampedReference` 当值可能 A→B→A 时,单纯比较值会误判“没人改过”(ABA 问题),用“值+版本戳”: ```java AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); boolean dec(int n) { while (true) { int[] stamp = new int[1]; int cur = ref.get(stamp); if (cur < n) return false; int newStamp = stamp[0] + 1; if (ref.compareAndSet(cur, cur - n, stamp[0], newStamp)) return true; } } ``` #### 方案 A3:读多写少下的“乐观读”——`StampedLock` ```java StampedLock lock = new StampedLock(); long stamp = lock.tryOptimisticRead(); int xLocal = x; // 无锁读取 if (!lock.validate(stamp)) { // 期间若被写,验证失败 stamp = lock.readLock(); // 再走读锁兜底 try { xLocal = x; } finally { lock.unlockRead(stamp); } } ``` 要点:**乐观读→验证→必要时回退到读锁**,减少阻塞。 ### B. 数据库并发(MySQL/Oracle 等)——版本号/条件更新 **标准两步**:先读出 `version`,更新时携带 `WHERE version=?`,受影响行数=1 视为成功,否则重试。 **表结构** ```sql ALTER TABLE product ADD COLUMN version INT NOT NULL DEFAULT 0; ``` **更新 SQL** ```sql -- 假设要扣减库存 1 UPDATE product SET stock = stock - 1, version = version + 1 WHERE id = #{id} AND stock >= 1 -- 业务约束 AND version = #{oldVersion}; ``` **Java 伪代码(MyBatis/JdbcTemplate)** ```java Product p = dao.selectForUpdateLikeNoLock(id); // 普通查询即可(不要 FOR UPDATE) int retry = 0, maxRetry = 5; while (retry++ < maxRetry) { int rows = dao.updateStockWithVersion(id, p.getVersion()); if (rows == 1) return true; // 成功 p = dao.selectById(id); // 别人改过,拿新版本再试 } return false; // 超过重试上限,交给上层处理/排队 ``` 要点: * **不需要悲观锁/行锁**;靠版本冲突检测避免“丢失更新”。 * 冲突时**重读最新数据**再尝试,或直接失败给调用方。 * JPA 可用 `@Version` 自动完成(抛 `OptimisticLockException` 后应用层重试)。 > 扩展:不用显式 `version` 字段时,也可用业务条件做**条件更新**:`WHERE amount = #{oldAmount}` 或 `WHERE updated_at = #{oldTs}`,效果类似但不如版本号稳妥。 ### C. 分布式场景(跨进程)——Redis/WATCH 或 Lua #### 方案 C1:Redis **WATCH/MULTI/EXEC**(CAS 语义) ```bash WATCH stock:sku1 # 监视键 val = GET stock:sku1 if val >= n: MULTI DECRBY stock:sku1 n # 事务块 EXEC # 若期间有人改过,EXEC 返回空,表示失败需重试 else: UNWATCH ``` 要点:轻量 CAS,适合低~中冲突;高冲突会频繁失败重试。 #### 方案 C2:Redis **Lua 脚本**(原子性+业务校验) ```lua -- KEYS[1]=stockKey, ARGV[1]=n local cur = tonumber(redis.call('GET', KEYS[1]) or '0') if cur >= tonumber(ARGV[1]) then redis.call('DECRBY', KEYS[1], ARGV[1]) return 1 else return 0 end ``` 要点:在 Redis 内一次完成校验与扣减,**原子且无重试风暴**;配合消息/DB 最终一致。 ## 3)什么时候选乐观锁? * **读多写少**、冲突概率低; * **不能接受长时间阻塞**(比如高并发读场景); * 数据可接受**失败重试或让用户重试**的交互(购物车、点赞、配额扣减)。 不适合:**热点写**(同一 key 高并发频繁冲突),会导致大量重试与 CPU 浪费。此时考虑: * **排队/串行化**:按 key 绑定单线程执行(Disruptor、队列、Key 分片执行器)。 * **悲观锁/分布式锁**:短临界区且冲突高时更省事。 * **批处理**:聚合写入降低冲突频率(库存/计数分片汇总)。 ## 4)常见坑 & 优化 * **重试风暴**:加**随机退避**(如 1ms、2ms、4ms 上限 50ms)和**重试上限**,并埋点 `retry_count`。 * **ABA 问题**:内存 CAS 用 **Stamped/版本戳**;DB 用 version 列天然避免。 * **饥饿与长尾**:热点 key 可做**分片计数**(分 10 份独立 CAS,读时汇总)。 * **一致性**:DB 更新+缓存更新时,用**延迟双删**或版本号校验;DB+MQ 用 **Outbox/事务消息** 保证最终一致。 * **幂等**:重试要**业务幂等**(请求 id 去重、唯一约束)。 * **监控**:统计**冲突率/重试次数/成功延迟**,超过阈值自动降级为队列/锁。 ### 一句话总结 **乐观锁=“提交时校验版本”**。 在 Java 内存用 **CAS/StampedLock**,在数据库用 **`version` 条件更新**,在分布式用 **Redis WATCH 或 Lua**。它适合低冲突场景,用“失败重试”代替“阻塞等待”;热点写时要换成排队/悲观锁/批处理并配合退避与幂等。发布于 2025-08-13 13:21 •IP属地 河北举报评论收藏
公众号进入网站体验更佳

