游乐游手机版
首页/数据库/文章详情

乐观锁深度解析:MySQL与PostgreSQL实战及Spring Boot集成

时间:2026-07-07 07:05
乐观锁通过版本号或时间戳在更新时验证数据是否被修改,避免高并发下丢失更新。MySQL需应用层实现版本字段;PostgreSQL可利用MVCC或xmin系统列。SpringBoot可通过JPA@Version或MyBatis-Plus插件集成,并需处理冲突重试,确保数据一致性。

关于本文档

这篇文章聊的是一个老生常谈但相当棘手的话题:高并发场景下,如何保证数据更新不冲突。从并发写入的那些坑开始,逐步深入到乐观锁的底层原理,以及不同数据库之间的实现差异,最后会给出在 Spring Boot 里可以直接拿来用的代码。

  • 乐观锁 vs 悲观锁的核心区别与选型依据
  • MySQL 版本号机制的 SQL 实现原理与陷阱
  • PostgreSQL MVCC 与乐观锁的深度结合
  • Oracle、MongoDB、Redis 乐观锁简明对比
  • Spring Boot JPA (@Version) 完整实战代码
  • MyBatis-Plus 乐观锁插件配置与实战
  • 冲突异常处理、重试机制与最佳实践

1. 并发写入的噩梦:为什么需要乐观锁

1.1 丢失更新:真实发生的数据灾难

先来看一个电商库存的典型场景。假设商品 A 的库存是 100 件,这时候有两个下单请求几乎同时到达后台。

请求1(线程A):SELECT stock FROM products WHERE id=1;  → 读到 stock=100
请求2(线程B):SELECT stock FROM products WHERE id=1;  → 读到 stock=100
请求1(线程A):UPDATE products SET stock=99 WHERE id=1; → 更新成功
请求2(线程B):UPDATE products SET stock=99 WHERE id=1; → 更新成功(覆盖了线程A!)

注意看,实际卖出了 2 件,但库存只减少了 1 件。这就是典型的"丢失更新"(Lost Update)问题,也是超卖现象的根源所在。

1.2 悲观锁的代价:阻塞换一致性

最直觉的解法当然是悲观锁(SELECT ... FOR UPDATE):读数据的时候直接加上排他锁,其他事务必须等着。

-- 悲观锁写法(MySQL/PostgreSQL 通用)
BEGIN;
SELECT stock FROM products WHERE id=1 FOR UPDATE;  -- 锁住这一行
-- 业务逻辑...
UPDATE products SET stock = stock - 1 WHERE id=1;
COMMIT;

悲观锁确实能解决问题,但代价也很明显:

问题具体表现影响
阻塞等待高并发时大量请求排队吞吐量断崖式下降
死锁风险多表/多行操作时易死锁系统异常 + 回滚开销
长事务危害锁持有时间长 → 锁升级级联阻塞,雪崩
连接耗尽等待中的连接占用资源数据库连接池满

1.3 乐观锁的核心思想:验证而非阻塞

乐观锁的思路刚好反过来——它不在读取时加锁,而是在提交更新的时候检查数据是否已经被别人改过。打个比方,就像超市结账:你把商品放进购物车的时候并不会锁住库存,只是在付款的那一刻才去确认库存还在不在。

flowchart TB
    subgraph 悲观锁[" 悲观锁(锁定优先)"]
        A1[读数据] --> A2[加锁] --> A3[写数据] --> A4[释放锁]
    end
    subgraph 乐观锁[" 乐观锁(验证优先)"]
        B1[读数据+版本号] --> B2[写数据] --> B3{版本号匹配?}
        B3 -->|是| B4[提交成功 version+1]
        B3 -->|否| B5[失败 重试/报错]
    end
    style 悲观锁 fill:#ffebee
    style 乐观锁 fill:#e8f5e9

2. 乐观锁的两种核心机制

2.1 版本号(Version)机制:最推荐的方式

在数据表里新增一个整数类型的 version 字段,初始值设为 0 或 1。每次更新数据时,version 值自动 +1,并且在 WHERE 条件里加上版本号的比对。

核心 SQL 模板:

-- 读取数据,同时获取版本号
SELECT id, name, stock, version FROM products WHERE id = 1;
-- 假设读到:stock=100, version=5-- 更新时携带版本号,只有版本匹配才能更新
UPDATE products
SET stock = stock - 1,
    version = version + 1          -- 版本号自增
WHERE id = 1
  AND version = 5;                 -- 携带读取时的版本号-- 检查 UPDATE 影响的行数:
--   rows_affected = 1 → 成功(无冲突)
--   rows_affected = 0 → 失败(已被他人修改)

为什么这样能防止丢失更新?

时刻线程A线程Bversion
T1读到 version=5读到 version=55
T2UPDATE ... WHERE version=5 → 成功-6
T3-UPDATE ... WHERE version=5 → 失败(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';  -- 毫秒级精度

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 本身并没有内置的乐观锁机制,所谓的乐观锁,完全是应用层自己实现的。InnoDB 引擎在执行 UPDATE 语句时,会在行级别加一个短暂的写锁(X 锁)来完成这次更新,然后立刻释放。真正的"版本比对"逻辑,是由 WHERE version = ? 这个条件来完成的。

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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 初始数据
INSERT INTO products (name, stock, version) VALUES ('商品A', 100, 0);
-- 步骤1:读取数据(含 version)
SELECT id, name, stock, version FROM products WHERE id = 1;
-- 结果:id=1, name='商品A', stock=100, version=0-- 步骤2:更新(携带版本号,失败时 rows_affected=0)
UPDATE products
SET   stock   = stock - 1,
      version = version + 1
WHERE id      = 1
  AND version = 0;       -- 读取时拿到的版本号-- 步骤3:判断是否成功(Ja va JDBC / MyBatis)
-- int rows = jdbcTemplate.update(...);
-- if (rows == 0) { throw new OptimisticLockException("数据已被修改,请重试"); }

3.3 MySQL 乐观锁注意事项

//  错误:用旧的 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 的并发控制基于多版本并发控制(MVCC,Multi-Version Concurrency Control)。每一行数据在内部都有系统隐藏列 xmin(插入/更新该行的事务 ID)和 xmax(删除该行的事务 ID)。

这意味着 PostgreSQL 本身就在行级别维护了版本信息,这也是它和 MySQL 一个很显著的底层差异。

flowchart TB
    subgraph PG_MVCC["PostgreSQL MVCC 内部结构"]
        R1["行版本1: xmin=100, xmax=200, stock=100"]
        R2["行版本2: xmin=200, xmax=0, stock=99  ← 当前可见"]
    end
    subgraph App["应用层乐观锁"]
        A["读取 + 手动 version 字段 / xmin"]
        B["UPDATE WHERE version = 旧值"]
    end
    PG_MVCC --> App
    style R2 fill:#e8f5e9

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;
-- 结果:id=1, stock=100, row_version='12345'-- 更新时通过 xmin 比对(注意 xmin 不能直接出现在 UPDATE 的 SET 中)
UPDATE products
SET   stock = stock - 1
WHERE id    = 1
  AND xmin  = '12345'::xid;   -- 与读取时的 xmin 对比
-- 如果 xmin 已变化(其他事务更新过),该条件不满足,rows_affected=0

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 Oracle:ORA_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 MongoDB:findOneAndUpdate 的原子操作

MongoDB 天然支持通过 findOneAndUpdate 配合 version 字段实现乐观锁。由于单文档操作是原子的,这种方式非常高效。

// MongoDB 乐观锁:通过 version 字段
db.products.findOneAndUpdate(
  { _id: ObjectId("..."), version: 5 },    // 查询条件包含版本号
  {
    $inc: { stock: -1, version: 1 }        // 扣减库存同时版本+1
  },
  { returnDocument: "after" }
);
// 如果返回 null,说明版本已变,更新失败

5.3 Redis:WATCH + MULTI/EXEC 实现乐观锁

Redis 通过 WATCH 命令监视一个或多个 key。如果在执行 EXEC 之前,被监视的 key 发生了变化,整个事务就会被取消(返回 nil),相当于是 Redis 版的乐观锁。

# 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 在执行 sa ve() 时,会自动把 version 字段塞进 WHERE 条件,并在提交成功后自增 version。如果更新行数为 0,就抛出 OptimisticLockException,Spring 会把它包装成 ObjectOptimisticLockingFailureException

完整代码示例:库存管理系统(MySQL + Spring Boot 3.x)

项目依赖(pom.xml)


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
    <groupId>com.mysqlgroupId>
    <artifactId>mysql-connector-jartifactId>
    <scope>runtimescope>
dependency>



实体类(Entity)

package com.example.demo.entity;import jakarta.persistence.*;
import lombok.Data;
import ja va.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();
    }
}

Repository

package com.example.demo.repository;import com.example.demo.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;public interface ProductRepository extends JpaRepository {
}

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.sa ve(product);        log.info("库存扣减成功:productId={}, quantity={}, newStock={}, version={}",
            productId, quantity, product.getStock(), product.getVersion());
    }
}

添加 Spring Retry 依赖

<dependency>
    <groupId>org.springframework.retrygroupId>
    <artifactId>spring-retryartifactId>
dependency>
<dependency>
    <groupId>org.springframeworkgroupId>
    <artifactId>spring-aspectsartifactId>
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-sql=true),可以看到 Hibernate 自动生成的乐观锁 SQL:

-- 第一次更新(version=0,成功)
UPDATE products SET stock=99, version=1, updated_at='...' WHERE id=1 AND version=0;
-- affected rows: 1 -- 并发时第二个请求(version 已变为 1,失败)
UPDATE products SET stock=99, version=1, updated_at='...' WHERE id=1 AND version=0;
-- affected rows: 0 → 抛出 ObjectOptimisticLockingFailureException

6.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 ja va.util.Map;@RestControllerAdvice
public class GlobalExceptionHandler {    /**
     * 乐观锁冲突异常处理:重试次数耗尽后的兜底响应
     */
    @ExceptionHandler(ObjectOptimisticLockingFailureException.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public Map 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 ja va.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 BaseMapper {
}
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 ServiceImpl {    /**
     * 扣减库存(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 自动注入的版本比对条件

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 次无间隔重试,瞬间压垮 DB

9.3 避免的常见错误

错误后果正确做法
重试时不重新查询死循环或永久失败每次重试前必须重新 findById
对批量更新用乐观锁性能极差批量更新用悲观锁或分批处理
version 字段允许为 null乐观锁失效设置 NOT NULL DEFAULT 0
跨微服务使用乐观锁无法防止分布式冲突改用分布式锁
重试次数过多放大 DB 压力最多 3-5 次,超出返回友好提示

10. 总结

核心概念一句话解释
乐观锁读不加锁,提交时验证版本号是否被修改
version 字段数据库行中的整数字段,每次更新自动 +1
CASCompare And Swap,比较并交换,乐观锁的底层思想
OptimisticLockExceptionJPA 在版本冲突时抛出的异常,需捕获并重试
xmin(PostgreSQL)PG 内置行版本标识,可替代 version 字段
@RetryableSpring Retry 注解,自动重试乐观锁冲突
冲突率 20%阿里巴巴推荐的乐观锁/悲观锁切换阈值

来源:https://juejin.cn/post/7657541772180635711
上一篇MySQL JOIN解析:朴实无华却实用至上 下一篇KSQL字段显示不全?展开模式与输出控制用法
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
用Qwen大模型为MySQL查询推荐最佳可视化图表
数据库 · 2026-07-07

用Qwen大模型为MySQL查询推荐最佳可视化图表

如何用Qwen大模型为MySQL查询自动推荐最佳可视化图表 你是否希望从MySQL查出的销售数据自动生成柱状图,而不是对着满屏数字发呆?刚写完一条SELECT语句,却不确定该使用折线图还是热力图来展示时间趋势?或者你把查询结果复制进Excel后才想起,其实散点图更能说明问题。这些场景是不是很熟悉?

MongoDB 4.0事务处理机制底层原理详解
数据库 · 2026-07-07

MongoDB 4.0事务处理机制底层原理详解

MongoDB4 0多文档事务深度复用WiredTiger引擎原生多行事务能力,基于快照隔离和MVCC机制。事务启动获取clusterTime,读操作基于固定快照,写冲突在提交时检测。oplog异步刷盘可能影响持久性,生产环境需启用journal并控制事务超时。

Qwen大模型助力MySQL敏感数据脱敏与隐私保护
数据库 · 2026-07-07

Qwen大模型助力MySQL敏感数据脱敏与隐私保护

借助Qwen大模型一键生成合规的MySQL脱敏SQL语句 先看一个真实业务场景:你需要在MySQL中对姓名、手机号、身份证号这类敏感字段进行合规脱敏,且脱敏逻辑要具备可复用性、可审计性、可回溯能力。此时直接打开Qwen的Web界面或调用API,输入一条清晰指令就能搞定——例如:“请为MySQL表us

数据库里最反直觉的陷阱:NULL不等于空,90%新手踩过坑
数据库 · 2026-07-07

数据库里最反直觉的陷阱:NULL不等于空,90%新手踩过坑

NULL是数据库中表示“未知”的特殊标记,而非空值或0。它引入三值逻辑,导致用=NULL查不出数据、COUNT(column)忽略NULL、运算结果全为NULL、NOTIN遇NULL返回空、排序位置因数据库而异。正确处理需用ISNULL判断、COALESCE赋默认值、NOTEXISTS替代NOTIN,建表时尽量设置NOTNULL。

Qwen大模型生成MySQL性能优化量化对比报告测评
数据库 · 2026-07-07

Qwen大模型生成MySQL性能优化量化对比报告测评

Qwen大模型能够基于两份CSV文件,自动生成一份包含QPS、延迟等8项核心指标的MySQL优化量化对比报告。您只需导出规范的CSV数据,使用特定提示词触发解析,再将结果转为HTML或PDF格式即可交付。此外,通过三步验证流程,可确保所有数据真实可信,满足技术评审要求。需要一份能直接用于技术评审或D