MyBatis-Plus 提供了强大的 乐观锁插件(OptimisticLockerInterceptor),用于解决多线程或高并发环境下对同一数据的并发修改问题。相比悲观锁(数据库行锁),乐观锁性能更高、开销更小,是现代高并发系统的首选方案。
一、核心概念
1.1 什么是乐观锁?
- 乐观锁(Optimistic Locking):假设数据一般不会发生冲突,只在提交更新时检查是否被其他事务修改过。
- 实现方式:通常使用一个版本号字段(
version
)或时间戳字段,在更新时判断版本是否变化。 - SQL 示例:
如果返回影响行数为 0,说明数据已被其他事务修改,当前更新失败。UPDATE user SET name = '新名字', version = version + 1 WHERE id = 1 AND version = 10;
1.2 MyBatis-Plus 乐观锁机制
- 基于
@Version
注解标记版本字段。 - 配合
OptimisticLockerInnerInterceptor
(新版)自动在UPDATE
语句中添加version = 当前值
条件,并自动 +1。 - 无需手动编写 SQL,完全透明集成。
1.3 适用场景
✅ 适合读多写少的场景
✅ 高并发系统(如秒杀、库存更新)
✅ 数据冲突概率较低的业务
❌ 不适合频繁并发修改的极端场景(此时应考虑分布式锁或队列)
二、操作步骤(非常详细)
步骤 1:添加依赖(Maven)
确保已引入 MyBatis-Plus 核心依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
✅ 推荐使用 3.4.0 及以上版本,使用
InnerInterceptor
新架构。
步骤 2:数据库表添加 version
字段
在需要加锁的表中添加 version
字段:
ALTER TABLE `user`
ADD COLUMN `version` INT NOT NULL DEFAULT 1 COMMENT '乐观锁版本号';
🔍 字段说明:
- 类型:
INT
或BIGINT
- 默认值:
1
- 非空:
NOT NULL
- 注释:便于理解
步骤 3:实体类添加 @Version
注解
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String email;
/**
* 乐观锁版本号字段
* 必须使用 @Version 注解
*/
@Version
@TableField("version")
private Integer version;
/**
* 其他字段...
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
⚠️ 注意:
- 必须使用
@Version
注解。- 字段类型支持:
int
、Integer
、long
、Long
、short
、Short
、Timestamp
。- 初始值建议为
1
,避免0
导致条件判断问题。
步骤 4:配置乐观锁插件(MyBatis-Plus 3.4.0+)
在 Spring Boot 配置类中注册 MybatisPlusInterceptor
并添加 OptimisticLockerInnerInterceptor
:
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;
/**
* MyBatis-Plus 配置类
*/
@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;
}
}
✅ 关键点:
- 使用
MybatisPlusInterceptor
替代旧版OptimisticLockerInterceptor
。OptimisticLockerInnerInterceptor
是 MyBatis-Plus 3.4.0+ 推荐方式。- 插件会自动拦截
update
操作,无需手动调用。
步骤 5:使用 Mapper 进行更新操作(自动生效)
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 更新用户信息(自动启用乐观锁)
*/
public boolean updateUser(User user) {
// 假设前端传来了 id 和 name,version 也必须传入
int result = userMapper.updateById(user);
return result > 0; // 返回是否更新成功
}
/**
* 模拟并发更新场景
*/
public void concurrentUpdate() {
// 用户 A 查询数据
User userA = userMapper.selectById(1L);
System.out.println("User A Version: " + userA.getVersion()); // 1
// 用户 B 查询数据
User userB = userMapper.selectById(1L);
System.out.println("User B Version: " + userB.getVersion()); // 1
// 用户 A 先更新
userA.setName("A 更新");
userMapper.updateById(userA); // UPDATE ... WHERE id=1 AND version=1
System.out.println("A 更新后 Version: " + userA.getVersion()); // 2
// 用户 B 后更新(此时 version 已变为 2,但 userB.version 仍是 1)
userB.setName("B 更新");
int result = userMapper.updateById(userB); // WHERE id=1 AND version=1 → 无匹配
System.out.println("B 更新结果: " + result); // 0(失败)
if (result == 0) {
throw new RuntimeException("数据已被其他用户修改,请刷新重试!");
}
}
}
✅ 输出示例:
User A Version: 1
User B Version: 1
A 更新后 Version: 2
B 更新结果: 0
三、常见错误与解决方案
错误 | 原因 | 解决方案 |
---|---|---|
version 字段未参与 WHERE 条件 |
未正确配置插件 | 检查 MybatisPlusInterceptor 是否添加 OptimisticLockerInnerInterceptor |
更新失败但无提示 | 未处理返回值 | 检查 updateById 返回值是否 > 0 |
@Version 字段为 null |
实体未查出或未传入 | 查询后必须将 version 字段传回前端,更新时带回 |
插件不生效 | 使用了旧版配置 | 确保使用 InnerInterceptor 架构(3.4.0+) |
批量更新不支持 | 乐观锁不支持 updateBatchById |
需逐条更新或自定义 SQL |
四、注意事项
@Version
字段必须非空:数据库和 Java 对象都应保证有值。- 更新时必须传入
version
:前端需将version
作为参数传回。 - 不支持
wrapper
更新中的set
操作:如update().set("version", 999)
会被忽略。 null
值不参与乐观锁:如果version
为null
,插件不会添加WHERE
条件。- 避免在
insert
时设置version
:建议由数据库默认值或自动填充处理。
五、使用技巧
5.1 结合自动填充设置初始版本
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "version", Integer.class, 1);
}
@Override
public void updateFill(MetaObject metaObject) {
// updateTime 等
}
}
5.2 自定义异常处理
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<String> handleOptimisticLockFailure(DataIntegrityViolationException ex) {
return ResponseEntity.badRequest().body("数据已被修改,请刷新页面后重试。");
}
⚠️ 注意:MyBatis-Plus 不会抛出特定异常,需通过返回值判断。
5.3 支持 Timestamp
类型版本
@Version
private LocalDateTime versionTime;
适用于以时间戳作为版本依据的场景。
5.4 手动重试机制(高级)
public boolean updateWithRetry(User user, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
User dbUser = userMapper.selectById(user.getId());
user.setVersion(dbUser.getVersion()); // 使用最新版本
int result = userMapper.updateById(user);
if (result > 0) {
return true;
}
// 可加入延迟
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
return false;
}
六、最佳实践与性能优化
6.1 最佳实践
✅ 统一基类封装 version
字段
@Data
public class BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
@Version
@TableField("version")
private Integer version;
}
// 实体继承
public class User extends BaseEntity {
private String name;
}
✅ 前端展示版本信息(可选)
- 显示“最后修改时间”或“版本号”,提升用户体验。
✅ 日志记录冲突事件
if (result == 0) {
log.warn("乐观锁冲突:用户 {} 尝试更新已被修改的数据,ID={}", currentUser, user.getId());
}
✅ 结合业务重试或合并
- 对于非关键冲突,可提示用户“数据已变更”,让用户决定是否覆盖。
6.2 性能优化
- 无额外查询开销:乐观锁只在
UPDATE
时增加一个WHERE
条件,性能几乎无损。 - 减少数据库锁等待:相比悲观锁,不会阻塞其他读操作。
- 适合分布式环境:无需跨节点协调锁。
- 批量操作优化:避免对同一记录频繁更新,可合并操作或使用队列。
七、总结
MyBatis-Plus 乐观锁插件是高并发系统的利器:
功能 | 实现方式 |
---|---|
启用乐观锁 | @Version 注解 + OptimisticLockerInnerInterceptor |
版本控制 | 数据库 version 字段 + 自动填充初始值 |
冲突处理 | 检查 update 返回值,提示用户重试 |
🔧 推荐流程:
- 数据库添加
version
字段 - 实体类添加
@Version
注解 - 配置
MybatisPlusInterceptor
添加乐观锁插件 - 更新时传入
version
,检查返回值 - 处理失败情况(提示、重试、日志)