MyBatis-Plus 乐观锁

@Version 注解是 MyBatis-Plus 实现乐观锁的核心,它能帮你优雅地解决高并发下的数据更新冲突问题,比如商品超卖、余额扣减等,核心原理可以用一句话概括:基于“版本号”的冲突检测

🤔 什么是乐观锁?

乐观锁是一种并发控制策略,它假设并发操作发生冲突的概率较低,因此操作数据时不会加锁,而是在数据提交更新时进行冲突检测。若发现数据在此期间已被修改,则当前更新操作会失败。

🧠 核心原理:一个版本号的较量

  1. 读数据,记版本:当你查询一条记录时,MP会顺手查出它的版本号(version),比如版本号为 1
  2. 提交时,验版本:在更新这条数据时,MP生成的SQL会自动加上一个条件:WHERE ... AND version = 1
  3. 无冲突,加一版:如果这期间没人动过这条数据,数据库里的版本号仍然是 1,条件成立,更新成功。同时,MP会把版本号自增为 2
  4. 有冲突,即失败:如果你查询后,别人抢先一步更新了数据,版本号就变成了 2。这时你再执行更新,SQL中的 version = 1 条件就无法匹配,更新操作的效果为0行,你就能知道冲突发生了。

🛠️ 使用步骤:三步搞定

第一步:数据库准备

为需要并发控制的表添加一个版本号字段,比如 version

sql

1
ALTER TABLE your_table ADD COLUMN version INT DEFAULT 0;

第二步:实体类添加 @Version 注解

在实体类中,用 @Version 注解标记版本号字段。

java

1
2
3
4
5
6
7
8
9
10
11
12
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;

@Data
public class Product {
private Long id;
private String name;
private Integer price;

@Version
private Integer version; // 版本号字段
}

第三步:注册乐观锁插件

在你的 MyBatis-Plus 配置类中,添加 OptimisticLockerInnerInterceptor 插件。

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}

✨ 效果与实战

完成以上步骤后,当你执行 updateById 等方法时,MP 会自动进行版本控制。

自动生成的SQL

sql

1
2
3
4
-- 假设查询到的 version = 1
UPDATE product
SET price = 99, version = 2
WHERE id = 1 AND version = 1

业务代码示例

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Override
public boolean updatePrice(Long productId, Integer newPrice) {
// 1. 查询商品,version 会自动被查询出来
Product product = baseMapper.selectById(productId);
if (product == null) {
return false;
}

// 2. 修改字段
product.setPrice(newPrice);

// 3. 执行更新,MP 会自动处理 version 的校验和自增
return updateById(product);
}
}

🚦 业务层处理:更新冲突怎么办?

当并发发生时,updateById(product) 会返回 false 或影响行数为 0。你需要根据业务场景,在业务层主动处理冲突,通常有以下几种方式:

  • 提示重试:对于用户触发操作,可以返回“数据已被修改,请刷新后重试”。
  • 自动重试:对库存扣减等对用户无感的操作,可采用自动重试机制,并建议设置最大重试次数和指数退避策略以避免加剧冲突。
  • 合并更新:应用在用户个人资料修改等场景,可尝试合并两次更新,或提示用户确认覆盖。
  • 人工介入:在财务等敏感核心系统,可将冲突数据记录下来,交由人工审核处理。

💎 总结与注意

✅ 典型适用场景

  • 高并发下的库存扣减:秒杀、抢票系统。
  • 账户余额更新:同时发生充值、消费等。
  • 多项配置同时修改:避免最后提交覆盖此前内容。
  • 多终端/多用户编辑同一数据:编辑协同、后台管理。

⚠️ 几个重要提醒

  • 不要手动修改 **version**:由 MP 自增,手动修改将破坏并发控制逻辑。
  • 确保版本号有初始值:新增数据时,确保 version 字段有默认值(如 0 或 1)。
  • 不支持伪更新:更新时若字段都没变,MP 可能不追加 where version = ?,导致乐观锁失效。可通过 UpdateWrappersetSql 强制触发,如 setSql("version=version")
  • 注意插件配置:若同时使用分页等多种插件,确保已正确注册 OptimisticLockerInnerInterceptor
  • 支持的数据类型:版本号字段支持 intIntegerlongLongDateTimestampLocalDateTime。对于整数类型,版本号自增为 oldVersion + 1

🤔 什么时候不适合用?

  • 高冲突场景:数据激烈竞争会导致大量更新失败与重试开销。
  • 长事务:事务过长会显著增加冲突概率。
  • 需强事务一致性:依赖数据库事务隔离级别或对一致性有极高要求的场景。

MyBatis-Plus 的 @Version 注解实现乐观锁,为高并发下数据一致性问题提供了高效、低侵入的解决方案。它通过版本号机制以无锁方式应对并发,并需要你在业务层配合做好冲突处理。