一、核心概念
什么是 @Version
?
@Version
是 MyBatis-Plus 提供的乐观锁注解,用于标识实体类中的版本字段。在执行 UPDATE 操作时,MyBatis-Plus 会自动将版本号 +1,并在 SQL 中添加 WHERE version = 原值
条件,确保数据未被其他事务修改过。
乐观锁 vs 悲观锁
类型 | 原理 | 适用场景 |
---|---|---|
乐观锁 | 假设不会发生冲突,更新时检查版本号 | 读多写少、并发不高 |
悲观锁 | 假设会发生冲突,直接加锁(如 SELECT ... FOR UPDATE ) |
写多、高并发、强一致性 |
@Version
工作原理
-- 假设当前 version = 1
UPDATE user SET name = 'newName', version = 2 WHERE id = 1 AND version = 1;
- 如果返回
affectedRows == 1
→ 更新成功 - 如果返回
affectedRows == 0
→ 版本不匹配,更新失败(数据已被修改)
二、操作步骤(超详细)
第一步:数据库表添加版本字段
ALTER TABLE `user` ADD COLUMN `version` INT DEFAULT 1 COMMENT '版本号';
-- 或建表时包含
-- `version` INT DEFAULT 1 COMMENT '版本号'
✅ 建议:初始值设为
1
,便于识别是否被更新过。
第二步:实体类添加 @Version
注解
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class UserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
// 版本字段
@Version
@TableField("version")
private Integer version;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
⚠️ 注意:
- 字段类型必须是
Integer
或Long
- 必须配合
@TableField
使用(指定数据库字段名)- 通常与
MetaObjectHandler
配合自动填充updateTime
第三步:配置 MyBatis-Plus 乐观锁插件
在 Spring Boot 配置类中注册 MybatisPlusInterceptor
:
import com.baomidou.mybatisplus.annotation.DbType;
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 {
/**
* 配置 MyBatis-Plus 插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 可同时添加分页插件
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
✅ 从 MP 3.4.0 起,推荐使用
MybatisPlusInterceptor
,替代旧的OptimisticLockerInterceptor
。
第四步:编写业务代码测试乐观锁
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
/**
* 更新用户信息(演示乐观锁)
*/
public boolean updateUser(UserDO user) {
// 假设前端传来的 user 对象包含当前 version
int result = userMapper.updateById(user);
return result > 0; // 返回是否更新成功
}
/**
* 模拟并发更新场景
*/
public void concurrentUpdateDemo() {
// 用户 A 查询数据
UserDO userA = userMapper.selectById(1L);
System.out.println("用户A查询: " + userA.getVersion()); // version=1
// 用户 B 查询数据
UserDO userB = userMapper.selectById(1L);
System.out.println("用户B查询: " + userB.getVersion()); // version=1
// 用户 A 先更新
userA.setName("A修改了");
userMapper.updateById(userA); // version=1 → version=2
System.out.println("A更新后: " + userA.getVersion()); // version=2
// 用户 B 后更新(使用的是旧版本数据)
userB.setName("B修改了");
int result = userMapper.updateById(userB); // WHERE version=1 → 失败
System.out.println("B更新结果: " + result); // result=0
if (result == 0) {
throw new RuntimeException("数据已被其他用户修改,请刷新后重试!");
}
}
}
第五步:处理更新失败(推荐做法)
public boolean updateUserWithRetry(UserDO user, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
int result = userMapper.updateById(user);
if (result > 0) {
return true; // 更新成功
}
// 更新失败,重新查询最新数据
UserDO latest = userMapper.selectById(user.getId());
if (latest == null) {
throw new RuntimeException("数据不存在");
}
// 更新业务字段,保留最新版本号
user.setVersion(latest.getVersion());
// 可选:合并业务变更
user.setName(latest.getName()); // 示例:以最新名为准
} catch (Exception e) {
log.error("更新失败,重试 {}/{}", i + 1, maxRetries, e);
if (i == maxRetries - 1) throw e;
try {
Thread.sleep(100 * (i + 1)); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
return false;
}
三、常见错误与解决方案
错误现象 | 原因 | 解决方案 |
---|---|---|
@Version 无效 |
未配置 OptimisticLockerInnerInterceptor |
添加插件配置 |
更新后 version 未 +1 | 字段未加 @Version 或类型错误 |
确保注解正确且类型为 Integer/Long |
UPDATE 语句无 version 条件 |
插件未生效 | 检查 MybatisPlusInterceptor 是否注册 |
version 字段为 null |
数据库默认值未设置 | 设置默认值 DEFAULT 1 |
批量更新不支持 | updateBatchById 不支持乐观锁 |
需手动逐条更新或自定义 SQL |
SELECT FOR UPDATE 冲突 |
混用悲观锁和乐观锁 | 统一使用一种锁机制 |
四、注意事项
- ✅ 必须配置插件:
OptimisticLockerInnerInterceptor
是@Version
生效的前提。 - ✅ 字段类型:只能是
Integer
或Long
,不能是int
/long
(避免 null 问题)。 - ✅ 初始值:建议设为
1
,避免0
导致的意外行为。 - ✅ 并发更新:
updateBatchById
方法不支持乐观锁,需逐条处理。 - ✅ null 值处理:如果 version 为 null,更新会失败,确保查询时能取到值。
- ✅ 逻辑删除:乐观锁与逻辑删除可同时使用。
- ✅ 分布式场景:乐观锁适用于单数据库,跨服务需结合分布式锁或事件溯源。
五、使用技巧
1. 自定义版本生成策略
// 默认是 +1,可自定义
@Version
@TableField("version")
private Integer version;
// 在 update 前手动设置 version = oldVersion + 1
// 但通常不建议,让 MP 自动处理
2. 结合 MetaObjectHandler
初始化版本
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "version", Integer.class, 1);
}
3. 版本号用于缓存更新
// 更新成功后,通过 version 变化通知缓存失效
redisTemplate.delete("user:" + user.getId());
4. 前端提示“请刷新页面”
当更新失败时,返回特定错误码,前端提示用户刷新数据:
{
"code": 409,
"msg": "数据版本冲突,请刷新后重试"
}
六、最佳实践与性能优化
✅ 最佳实践
实践 | 说明 |
---|---|
✅ 所有可变表都加 version 字段 |
统一数据安全标准 |
✅ 配合 updateTime 使用 |
记录最后修改时间 |
✅ 前端展示版本信息 | 调试时显示 version 值 |
✅ 重试机制 | 重要操作添加有限重试 |
✅ 日志记录 | 记录乐观锁失败事件,便于分析 |
✅ 不用于高并发强一致场景 | 如库存扣减,建议用悲观锁或分布式锁 |
⚡ 性能优化建议
- ✅ 避免频繁更新:减少不必要的 UPDATE 操作
- ✅ 索引优化:
version
字段通常已在主键索引中,无需额外索引 - ✅ 批量处理:非并发场景可批量更新(不启用乐观锁)
- ✅ 缓存结合:更新后及时清理缓存,避免脏读
七、高级用法:自定义乐观锁逻辑
场景:使用时间戳作为版本号
// 不推荐,但可行
@Version
@TableField("update_time")
private LocalDateTime updateTime;
// 需要手动在 update 前 setUpdateTime(oldTime)
// 且精度问题可能导致冲突
⚠️ 建议:仍使用
INT
类型的version
字段,更简单可靠。
总结
@Version
注解是 MyBatis-Plus 实现乐观锁的核心,通过简单的注解和插件配置,即可防止并发修改导致的数据覆盖。
🚀 四步启用:
- 数据库添加
version
字段(INT,默认 1)- 实体类字段加
@Version
- 配置
OptimisticLockerInnerInterceptor
- 业务代码捕获更新失败并处理(提示刷新)
掌握 @Version
,让你的系统在并发环境下更加安全、稳定。适用于用户信息、配置管理、订单状态等读多写少的场景。