银行转账超发、电商库存超卖、 票务系统重复下单——这些高并发下的经典数据一致性噩梦都指向同一个根源并发写入时没有可靠的冲突检测。 乐观锁以”不加锁、提交时验证”的思路在读多写少的场景下优雅解决这一问题。本文从原理出发重点剖析 MySQL 和 PostgreSQL 的乐观锁实现兼顾其他主流数据库并提供 Spring Boot JPA/MyBatis-Plus 的完整落地方案。适合人群 后端开发者、了解基础 SQL 和 Spring Boot 的初 中级工程师关于本文档本文围绕”高并发下如何保证数据更新不冲突”展开从并发写入的痛点出发逐步深入乐观锁的实现原理和各数据库差异重点结合 Spring Boot 给出可直接复用的代码。✅ 乐观锁 vs 悲观锁的核心区别与选型依据✅ MySQL 版本号机制的 SQL 实现原理与陷阱✅ PostgreSQL MVCC 与乐观锁的深度结合✅ Oracle、MongoDB、Redis 乐观锁简明对比✅ Spring Boot JPA (Version) 完整实战代码✅ MyBatis-Plus 乐观锁插件配置与实战✅ 冲突异常处理、重试机制与最佳实践1. 并发写入的噩梦为什么需要乐观锁1.1 丢失更新真实发生的数据灾难想象一个电商库存场景商品 A 的库存为 100 件同时有两个下单请求到达后台。请求1线程ASELECT stock FROM products WHERE id1; → 读到 stock100 请求2线程BSELECT stock FROM products WHERE id1; → 读到 stock100 请求1线程AUPDATE products SET stock99 WHERE id1; → 更新成功 请求2线程BUPDATE products SET stock99 WHERE id1; → 更新成功覆盖了线程A实际卖出2 件库存却只减少了1 件——这就是典型的”丢失更新”Lost Update问题也是超卖的根源。1.2 悲观锁的代价阻塞换一致性最直觉的解法是悲观锁SELECT ... FOR UPDATE读数据时直接加排他锁其他事务必须等待。-- 悲观锁写法MySQL/PostgreSQL 通用 BEGIN; SELECT stock FROM products WHERE id1 FOR UPDATE; -- 锁住这一行 -- 业务逻辑... UPDATE products SET stock stock - 1 WHERE id1; COMMIT;悲观锁能解决问题但代价明显问题具体表现影响阻塞等待高并发时大量请求排队吞吐量断崖式下降死锁风险多表/多行操作时易死锁系统异常 回滚开销长事务危害锁持有时间长 → 锁升级级联阻塞雪崩连接耗尽等待中的连接占用资源数据库连接池满阿里巴巴 Java 开发手册规定如果每次访问冲突概率小于 20%推荐使用乐观锁否则使用悲观锁且乐观锁的重试次数不得小于 3 次。1.3 乐观锁的核心思想验证而非阻塞乐观锁不在读取时加锁而是在提交更新时检查数据是否被他人修改。就像超市结账你把商品放入购物车时不锁库存只在付款时确认库存是否还在。2. 乐观锁的两种核心机制2.1 版本号Version机制最推荐的方式在数据表中新增一个整数类型的version字段初始值为 0 或 1。每次更新数据时将version值 1并在WHERE条件中加入版本号比对。核心 SQL 模板-- 读取数据同时获取版本号 SELECT id, name, stock, version FROM products WHERE id 1; -- 假设读到stock100, version5 -- 更新时携带版本号只有版本匹配才能更新 UPDATE products SET stock stock - 1, version version 1 -- 版本号自增 WHERE id 1 AND version 5; -- 携带读取时的版本号 -- 检查 UPDATE 影响的行数 -- rows_affected 1 → 成功无冲突 -- rows_affected 0 → 失败已被他人修改为什么这样能防止丢失更新时刻线程A线程BversionT1读到 version5读到 version55T2UPDATE … WHERE version5 → 成功-6T3-UPDATE … WHERE version5 → 失败0行受影响6线程 B 的更新因为版本号已从 5 变成 6 而无法匹配数据不会被覆盖。2.2 时间戳Timestamp机制用updated_at时间戳替代整数 version原理相同但存在精度风险。-- 时间戳乐观锁 UPDATE orders SET status 2, updated_at NOW() WHERE id 1001 AND updated_at 2026-06-30 10:00:00.123; -- 毫秒级精度时间戳精度在高并发场景下可能产生问题。如果两个事务在同一毫秒内完成读取时间戳相同乐观锁将失效。推荐优先使用整数 version 字段时间戳仅作为辅助审计字段使用。2.3 CAS 原始值比较无额外字段某些简单场景下直接比对”更新前的业务字段值”也能实现乐观锁效果无需额外字段。-- 无 version 字段的 CAS 写法扣减库存 UPDATE products SET stock stock - 1 WHERE id 1 AND stock 100; -- 直接比对读取时的原始库存值方式额外字段精度推荐度适用场景整数 version需要高⭐⭐⭐⭐⭐所有场景时间戳不需要复用审计字段中毫秒⭐⭐⭐并发不极高的场景CAS 原值比较不需要取决于字段⭐⭐字段类型简单、单字段更新3. MySQL 乐观锁原理与实践3.1 MySQL 乐观锁的底层原理MySQL 本身不提供内置的乐观锁机制乐观锁完全是应用层实现。MySQL 的 InnoDB 引擎在执行UPDATE语句时会在行级别加一个短暂的写锁X 锁用于完成这次更新然后立即释放。真正的”版本比对”逻辑由WHERE version ?条件完成。MySQL 的UPDATE执行后应用层通过JDBC的executeUpdate()返回值affected rows来判断是否成功。返回 1 代表成功返回 0 代表版本冲突。3.2 MySQL 建表与基础 SQL 实现-- 建表添加 version 字段 CREATE TABLE products ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, stock INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, -- 乐观锁版本号 updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_id (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 初始数据 INSERT INTO products (name, stock, version) VALUES (商品A, 100, 0); -- 步骤1读取数据含 version SELECT id, name, stock, version FROM products WHERE id 1; -- 结果id1, name商品A, stock100, version0 -- 步骤2更新携带版本号失败时 rows_affected0 UPDATE products SET stock stock - 1, version version 1 WHERE id 1 AND version 0; -- 读取时拿到的版本号 -- 步骤3判断是否成功Java JDBC / MyBatis -- int rows jdbcTemplate.update(...); -- if (rows 0) { throw new OptimisticLockException(数据已被修改请重试); }3.3 MySQL 乐观锁注意事项常见陷阱一version 字段没有索引若 WHERE 条件中只有version而没有主键/唯一索引可能触发 全表扫描。务必确保WHERE id ? AND version ?中id是主键或有索引。常见陷阱二在循环重试中忘记重新查询乐观锁失败后必须重新查询最新数据含新 version再发起更新不能用旧数据重试。// ❌ 错误用旧的 version 重试 while (rows 0) { rows update(entity.getVersion()); // version 永远是旧值死循环 } // ✅ 正确失败后重新查询 int maxRetry 3; for (int i 0; i maxRetry; i) { Product latest productRepo.findById(id); // 重新查询最新数据 int rows productMapper.updateWithVersion(latest.getVersion(), ...); if (rows 0) break; if (i maxRetry - 1) throw new BusinessException(操作失败请稍后重试); }4. PostgreSQL 乐观锁MVCC 加持的更强选项4.1 PostgreSQL 的 MVCC 与乐观锁天然契合PostgreSQL 的并发控制基于多版本并发控制MVCCMulti-Version Concurrency Control。每一行数据在 PostgreSQL 内部都有系统隐藏列xmin插入/更新该行的事务 ID和xmax删除该行的事务 ID。这意味着 PostgreSQL 本身就在行级别维护了版本信息这是其与 MySQL 最显著的底层差异。4.2 方法一与 MySQL 相同的 version 字段方案PostgreSQL 完全支持与 MySQL 相同的 version 字段方案SQL 语法几乎一致-- 建表PostgreSQL 语法 CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, stock INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL DEFAULT 0, -- 乐观锁版本号 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 创建商品 INSERT INTO products (name, stock, version) VALUES (商品A, 100, 0); -- 乐观锁更新与 MySQL 相同逻辑 UPDATE products SET stock stock - 1, version version 1, updated_at NOW() WHERE id 1 AND version 0; -- 携带读取时的版本号 -- 检查 RETURNING 或 rowcount 来判断是否成功PostgreSQL 支持RETURNING子句可以更优雅地判断更新结果-- PostgreSQL 专属写法RETURNING 确认更新结果 UPDATE products SET stock stock - 1, version version 1 WHERE id 1 AND version 0 RETURNING id, stock, version; -- 如果没有返回行说明乐观锁冲突4.3 方法二利用 PostgreSQL 内置 xmin 系统列PostgreSQL 独有的xmin系统列天然记录了最后修改该行的事务 ID可以直接作为乐观锁的版本依据无需额外 version 字段。-- 读取数据时同时获取 xmin强制转换为文本便于传输 SELECT id, name, stock, xmin::TEXT AS row_version FROM products WHERE id 1; -- 结果id1, stock100, row_version12345 -- 更新时通过 xmin 比对注意 xmin 不能直接出现在 UPDATE 的 SET 中 UPDATE products SET stock stock - 1 WHERE id 1 AND xmin 12345::xid; -- 与读取时的 xmin 对比 -- 如果 xmin 已变化其他事务更新过该条件不满足rows_affected0xmin 方案的优点无需额外字段适合改造旧表缺点xmin 是 32 位事务 ID存在回绕问题超过 20 亿次事务后可能出现 ID 复用 生产环境需谨慎评估大多数场景下推荐使用显式 version 字段。4.4 PostgreSQL 的 Serializable 隔离级别自动冲突检测PostgreSQL 的SERIALIZABLE隔离级别通过谓词锁Predicate Locking自动检测读写冲突无需手动维护 version 字段是最彻底的乐观并发控制OCC实现。-- 使用 SERIALIZABLE 隔离级别 BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT stock FROM products WHERE id 1; -- 业务处理... UPDATE products SET stock stock - 1 WHERE id 1; COMMIT; -- 如果并发事务发生了读写冲突PostgreSQL 自动抛出 -- ERROR: could not serialize access due to concurrent update方案额外字段适用场景性能version 字段需要所有场景推荐首选高xmin 系统列不需要旧表改造低频更新高SERIALIZABLE不需要复杂业务逻辑强一致要求中冲突率高时下降明显5. 其他数据库的乐观锁实现简介5.1 OracleORA_ROWSCN 与 version 字段Oracle 提供了ORA_ROWSCN伪列System Change Number记录最后修改行的 SCN类似 PostgreSQL 的xmin。实践中通常仍使用 version 字段方案逻辑与 MySQL 完全相同。-- Oracle通过 ORA_ROWSCN 实现乐观锁 SELECT id, name, stock, ORA_ROWSCN AS row_scn FROM products WHERE id 1; -- 更新时比对 SCN UPDATE products SET stock stock - 1 WHERE id 1 AND ORA_ROWSCN :row_scn;5.2 MongoDBfindOneAndUpdate 的原子操作MongoDB 天然支持通过findOneAndUpdate version 字段的乐观锁。由于 MongoDB 的单文档操作是原子的这种方式非常高效。// MongoDB 乐观锁通过 version 字段 db.products.findOneAndUpdate( { _id: ObjectId(...), version: 5 }, // 查询条件包含版本号 { $inc: { stock: -1, version: 1 } // 扣减库存同时版本1 }, { returnDocument: after } ); // 如果返回 null说明版本已变更新失败5.3 RedisWATCH MULTI/EXEC 实现乐观锁Redis 通过WATCH命令监视一个或多个 key如果在执行EXEC之前被监视的 key 发生了变化整个事务将被取消返回 nil以此实现乐观锁。# Redis 乐观锁示例扣减库存 WATCH product:1:stock # 监视库存 key stock GET product:1:stock # 读取当前值 MULTI # 开启事务 DECRBY product:1:stock 1 # 扣减 EXEC # 执行如果 stock key 在 WATCH 后被修改返回 nil失败5.4 各数据库乐观锁横向对比数据库实现方式内置支持额外字段推荐度MySQL应用层 version 字段❌ 纯应用层需要⭐⭐⭐⭐⭐PostgreSQLversion 字段 / xmin / SERIALIZABLE✅ xmin SERIALIZABLE可选⭐⭐⭐⭐⭐Oracleversion 字段 / ORA_ROWSCN✅ ORA_ROWSCN可选⭐⭐⭐⭐MongoDBversion 字段 原子 findOneAndUpdate❌ 应用层需要⭐⭐⭐⭐RedisWATCH MULTI/EXEC✅ WATCH 命令不需要⭐⭐⭐SQL Serverrowversion / timestamp 列✅ rowversion 列需要⭐⭐⭐⭐6. Spring Boot 集成JPA Version 实战6.1 JPA Version 注解原理Spring Data JPA 通过Version注解提供开箱即用的乐观锁支持。Hibernate 在执行save()时会自动将 version 字段加入WHERE条件并在提交成功后自增 version。如果更新行数为 0则抛出OptimisticLockExceptionSpring 将其包装为ObjectOptimisticLockingFailureException。完整代码示例库存管理系统MySQL Spring Boot 3.x项目依赖pom.xml!-- Spring Boot 3.x 依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- PostgreSQL 可替换为-- !-- groupIdorg.postgresql/groupId -- !-- artifactIdpostgresql/artifactId --实体类Entitypackage com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; import java.time.LocalDateTime; Data Entity Table(name products) public class Product { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String name; Column(nullable false) private Integer stock; /** * 乐观锁版本号字段 * JPA 会自动在 UPDATE 的 WHERE 中加入版本比对并在成功后自动 1 * 支持类型int, Integer, long, Long, Timestamp */ Version Column(nullable false) private Integer version; Column(name updated_at) private LocalDateTime updatedAt; PrePersist PreUpdate public void onUpdate() { this.updatedAt LocalDateTime.now(); } }Repositorypackage com.example.demo.repository; import com.example.demo.entity.Product; import org.springframework.data.jpa.repository.JpaRepository; public interface ProductRepository extends JpaRepositoryProduct, Long { }Service业务逻辑 异常处理package com.example.demo.service; import com.example.demo.entity.Product; import com.example.demo.repository.ProductRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Slf4j Service RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; /** * 扣减库存JPA Version 乐观锁 * Retryable检测到乐观锁冲突后最多重试 3 次指数退避 */ Transactional Retryable( retryFor ObjectOptimisticLockingFailureException.class, maxAttempts 3, backoff Backoff(delay 100, multiplier 2) // 100ms, 200ms, 400ms ) public void decreaseStock(Long productId, int quantity) { // 1. 读取实体含 version 字段 Product product productRepository.findById(productId) .orElseThrow(() - new RuntimeException(商品不存在: productId)); // 2. 校验库存 if (product.getStock() quantity) { throw new RuntimeException(库存不足当前库存: product.getStock()); } // 3. 修改库存version 由 JPA 自动管理无需手动修改 product.setStock(product.getStock() - quantity); // 4. 保存时 Hibernate 生成 // UPDATE products SET stock?, version? WHERE id? AND version? // 若 version 不匹配抛出 ObjectOptimisticLockingFailureException productRepository.save(product); log.info(库存扣减成功productId{}, quantity{}, newStock{}, version{}, productId, quantity, product.getStock(), product.getVersion()); } }使用 Spring Retry 的Retryable注解需要在启动类或配置类上添加EnableRetry并引入spring-retry依赖。这是实现乐观锁自动重试的最优雅方式。添加 Spring Retry 依赖dependency groupIdorg.springframework.retry/groupId artifactIdspring-retry/artifactId /dependency dependency groupIdorg.springframework/groupId artifactIdspring-aspects/artifactId /dependency启动类SpringBootApplication EnableRetry // 启用 Spring Retry public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }6.2 Hibernate 生成的实际 SQL开启 SQL 日志后spring.jpa.show-sqltrue可以看到 Hibernate 自动生成的乐观锁 SQL-- 第一次更新version0成功 UPDATE products SET stock99, version1, updated_at... WHERE id1 AND version0; -- affected rows: 1 ✅ -- 并发时第二个请求version 已变为 1失败 UPDATE products SET stock99, version1, updated_at... WHERE id1 AND version0; -- affected rows: 0 → 抛出 ObjectOptimisticLockingFailureException6.3 全局异常处理package com.example.demo.exception; import org.springframework.http.HttpStatus; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.Map; RestControllerAdvice public class GlobalExceptionHandler { /** * 乐观锁冲突异常处理重试次数耗尽后的兜底响应 */ ExceptionHandler(ObjectOptimisticLockingFailureException.class) ResponseStatus(HttpStatus.CONFLICT) public MapString, Object handleOptimisticLock(ObjectOptimisticLockingFailureException e) { return Map.of( code, 409, message, 操作繁忙请稍后重试, detail, 数据版本冲突 e.getIdentifier() ); } }7. Spring Boot 集成MyBatis-Plus Version 实战7.1 MyBatis-Plus 乐观锁插件配置MyBatis-Plus 通过OptimisticLockerInnerInterceptor插件实现乐观锁配置简洁对业务代码无侵入。package com.example.demo.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 注意插件顺序官方推荐多租户 → 分页 → 乐观锁 → 防全表更新删除 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }7.2 实体类配置package com.example.demo.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(products) public class Product { TableId(type IdType.AUTO) private Long id; private String name; private Integer stock; /** * MyBatis-Plus 乐观锁注解 * 支持类型int, Integer, long, Long, Date, Timestamp, LocalDateTime * 注意仅支持 updateById(entity) 和 update(entity, wrapper) 方法触发乐观锁 */ Version private Integer version; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; }7.3 Mapper 与 Service// Mapper Mapper public interface ProductMapper extends BaseMapperProduct { } package com.example.demo.service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.entity.Product; import com.example.demo.mapper.ProductMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; Slf4j Service public class ProductService extends ServiceImplProductMapper, Product { /** * 扣减库存MyBatis-Plus 乐观锁 * OptimisticLockerInnerInterceptor 会自动在 SQL 中加入 version 比对 */ Transactional public boolean decreaseStock(Long productId, int quantity) { // 1. 必须先查询获取 version Product product getById(productId); if (product null || product.getStock() quantity) { return false; } // 2. 修改数据 product.setStock(product.getStock() - quantity); // 3. 调用 updateById // MP 自动生成UPDATE products SET stock?,version? WHERE id? AND version? boolean success updateById(product); if (!success) { log.warn(乐观锁冲突productId{}, 当前version{}, productId, product.getVersion()); } return success; } }7.4 MyBatis-Plus 自动生成的 SQL-- updateById(product) 实际执行的 SQL自动添加 AND version旧值 UPDATE products SET stock 99, version 1, updated_at 2026-06-30 10:00:00 WHERE id 1 AND version 0; -- ← MP 自动注入的版本比对条件MyBatis-Plus 乐观锁的重要限制只有updateById(entity)和update(entity, wrapper)两个方法会触发乐观锁在update(entity, wrapper)方法中wrapper不能复用每次需新建updateBatchById()批量更新不触发乐观锁8. JPA vs MyBatis-Plus 乐观锁选型对比8.1 框架选型对比对比维度Spring Data JPA VersionMyBatis-Plus Version配置复杂度⭐⭐仅加注解⭐⭐注解 插件注册代码侵入性极低注解即可极低注解即可自动重试需配合 Retryable需手动处理返回值冲突识别抛出异常强感知返回 false弱感知自定义 SQL较难结合乐观锁支持自定义 Mapper 需手动处理适合场景实体操作为主面向对象风格复杂 SQL灵活查询场景8.2 最佳实践AOP 自定义注解实现通用重试// 自定义注解 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface OptimisticRetry { int maxAttempts() default 3; } // AOP 切面 Aspect Component Slf4j public class OptimisticRetryAspect { Around(annotation(optimisticRetry)) public Object retry(ProceedingJoinPoint pjp, OptimisticRetry optimisticRetry) throws Throwable { int maxAttempts optimisticRetry.maxAttempts(); Exception lastException null; for (int attempt 1; attempt maxAttempts; attempt) { try { return pjp.proceed(); } catch (ObjectOptimisticLockingFailureException e) { lastException e; log.warn(乐观锁冲突第 {}/{} 次重试, attempt, maxAttempts); if (attempt maxAttempts) { Thread.sleep(50L * attempt); // 简单退避 } } } throw new BusinessException(操作失败请稍后重试, lastException); } } // 使用只需加注解 OptimisticRetry(maxAttempts 3) Transactional public void placeOrder(Long productId, int quantity) { // 业务代码不需要感知乐观锁细节 productService.decreaseStock(productId, quantity); }9. 最佳实践正确使用乐观锁的 7 条原则9.1 冲突率判断与选型场景特征建议方案原因冲突率 20%读多写少乐观锁无锁开销高吞吐冲突率 20%写密集悲观锁避免大量重试浪费写入极度密集秒杀悲观锁 队列 限流乐观锁重试会放大 DB 压力分布式系统分布式锁Redis/Zookeeper单节点乐观锁无法跨进程9.2 重试策略设计// ✅ 推荐指数退避重试避免惊群效应 Retryable( retryFor ObjectOptimisticLockingFailureException.class, maxAttempts 3, backoff Backoff(delay 100, multiplier 2, random true) // 加随机因子 ) // ❌ 错误固定间隔且间隔为 0 Retryable(maxAttempts 10, backoff Backoff(delay 0)) // 10 次无间隔重试瞬间压垮 DB9.3 避免的常见错误错误后果正确做法重试时不重新查询死循环或永久失败每次重试前必须重新 findById对批量更新用乐观锁性能极差批量更新用悲观锁或分批处理version 字段允许为 null乐观锁失效设置 NOT NULL DEFAULT 0跨微服务使用乐观锁无法防止分布式冲突改用分布式锁重试次数过多放大 DB 压力最多 3-5 次超出返回友好提示乐观锁不适用于以下场景高冲突率写入场景冲突 20%跨微服务/跨数据库的数据一致性需要强事务保证的金融清算批量数据更新百万行级别10. 总结核心概念一句话解释乐观锁读不加锁提交时验证版本号是否被修改version 字段数据库行中的整数字段每次更新自动 1CASCompare And Swap比较并交换乐观锁的底层思想OptimisticLockExceptionJPA 在版本冲突时抛出的异常需捕获并重试xminPostgreSQLPG 内置行版本标识可替代 version 字段RetryableSpring Retry 注解自动重试乐观锁冲突冲突率 20%阿里巴巴推荐的乐观锁/悲观锁切换阈值学习路径建议2026 年先手写 MySQL version 字段乐观锁的 SQL感受”影响行数0”的失败逻辑用 Spring Boot JPA Version搭一个并发扣减库存的 Demo模拟冲突对比 MyBatis-Plus 的OptimisticLockerInnerInterceptor理解两者的差异实现 AOP Retryable的通用重试机制让乐观锁对业务透明在生产中监控乐观锁冲突率超过 20% 及时切换方案