本文目录导读:

- 数据库表结构
- Maven依赖
- 配置文件 (application.yml)
- 实体类
- 配置乐观锁插件
- 自定义MetaObjectHandler(自动填充)
- Mapper接口
- Service层
- Service实现类
- 测试类
- 启动类
- SQL日志输出示例
- 关键点说明
我来为你提供一个完整的MyBatis-Plus乐观锁案例。
数据库表结构
CREATE TABLE `user` ( `id` bigint(20) NOT NULL COMMENT '主键ID', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `age` int(11) DEFAULT NULL COMMENT '年龄', `email` varchar(50) DEFAULT NULL COMMENT '邮箱', `version` int(11) DEFAULT '0' COMMENT '版本号', `create_time` datetime DEFAULT NULL COMMENT '创建时间', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 插入测试数据 INSERT INTO `user` (`id`, `name`, `age`, `email`, `version`) VALUES (1, '张三', 20, 'zhangsan@example.com', 0), (2, '李四', 25, 'lisi@example.com', 0), (3, '王五', 30, 'wangwu@example.com', 0);
Maven依赖
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件 (application.yml)
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
实体类
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer age;
private String email;
/**
* 版本号字段
* 实现乐观锁的关键注解
*/
@Version
private Integer version;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
配置乐观锁插件
package com.example.demo.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.example.demo.mapper")
public class MybatisPlusConfig {
/**
* MyBatis-Plus插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
paginationInterceptor.setMaxLimit(500L);
paginationInterceptor.setOverflow(false);
interceptor.addInnerInterceptor(paginationInterceptor);
return interceptor;
}
}
自定义MetaObjectHandler(自动填充)
package com.example.demo.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 初始版本号为0
this.strictInsertFill(metaObject, "version", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
Mapper接口
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承BaseMapper即可获得基础CRUD方法
}
Service层
package com.example.demo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.entity.User;
public interface UserService extends IService<User> {
/**
* 模拟并发更新场景
*/
boolean transferWithOptimisticLock(Long userId, int incrementAge);
/**
* 使用UpdateWrapper进行乐观锁更新
*/
boolean updateByOptimisticLock(User user);
}
Service实现类
package com.example.demo.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 模拟并发更新场景
* 两个线程同时修改同一条数据
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean transferWithOptimisticLock(Long userId, int incrementAge) {
// 1. 查询用户(此时version=0)
User user = this.getById(userId);
System.out.println("读取用户信息: " + user);
// 2. 模拟数据处理耗时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 3. 修改数据(update时会自动带上version条件)
user.setAge(user.getAge() + incrementAge);
boolean result = this.updateById(user);
if (result) {
System.out.println("更新成功,新版本号: " + user.getVersion());
} else {
System.out.println("更新失败,版本号冲突");
}
return result;
}
/**
* 使用UpdateWrapper进行乐观锁更新
*/
@Override
public boolean updateByOptimisticLock(User user) {
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", user.getId())
.eq("version", user.getVersion()); // 手动加version条件
// 更新字段
updateWrapper.set("age", user.getAge())
.set("email", user.getEmail());
return this.update(updateWrapper);
}
}
测试类
package com.example.demo;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest
class OptimisticLockTest {
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
/**
* 测试基本乐观锁更新
*/
@Test
void testOptimisticLock() {
// 1. 查询用户
User user = userMapper.selectById(1L);
System.out.println("原始数据: " + user);
// 2. 修改数据
user.setAge(user.getAge() + 1);
userMapper.updateById(user);
// 3. 查询验证
User updatedUser = userMapper.selectById(1L);
System.out.println("更新后数据: " + updatedUser);
// 断言版本号增加
assert updatedUser.getVersion() == user.getVersion();
}
/**
* 测试并发更新(模拟多线程竞争)
*/
@Test
void testConcurrentUpdate() throws InterruptedException {
// 重置用户数据
User user = userMapper.selectById(2L);
user.setAge(25);
user.setVersion(0);
userMapper.updateById(user);
// 创建并发任务
CountDownLatch latch = new CountDownLatch(5);
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
final int threadNum = i;
executor.execute(() -> {
try {
System.out.println("线程 " + threadNum + " 开始更新");
boolean success = userService.transferWithOptimisticLock(2L, 1);
System.out.println("线程 " + threadNum + " 更新结果: " + success);
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// 查询最终结果
User finalUser = userMapper.selectById(2L);
System.out.println("最终结果: " + finalUser);
// 最终age应该只增加1(只有第一个线程成功)
assert finalUser.getAge() == 26;
}
/**
* 测试版本号冲突
*/
@Test
void testVersionConflict() {
// 模拟两个线程同时读取
User user1 = userMapper.selectById(3L);
User user2 = userMapper.selectById(3L);
System.out.println("用户1读取: " + user1);
System.out.println("用户2读取: " + user2);
// 用户1先更新
user1.setAge(user1.getAge() + 10);
boolean update1 = userMapper.updateById(user1);
System.out.println("用户1更新成功: " + update1);
// 用户2后更新(此时version已经改变)
user2.setAge(user2.getAge() + 20);
boolean update2 = userMapper.updateById(user2);
System.out.println("用户2更新成功: " + update2);
// 用户2的更新应该失败
assert update1 && !update2;
}
/**
* 测试批量更新
*/
@Test
void testBatchUpdate() {
userMapper.selectList(null).forEach(user -> {
user.setEmail(user.getEmail());
userMapper.updateById(user);
});
System.out.println("批量更新完成");
}
/**
* 测试带条件的乐观锁更新
*/
@Test
void testConditionalOptimisticLock() {
User user = userMapper.selectById(1L);
user.setEmail("newemail@example.com");
boolean result = userService.updateByOptimisticLock(user);
System.out.println("条件更新结果: " + result);
// 如果成功,查询并打印
if (result) {
User updated = userMapper.selectById(1L);
System.out.println("更新后的数据: " + updated);
}
}
}
启动类
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
SQL日志输出示例
==> Preparing: SELECT id,name,age,email,version,create_time,update_time FROM user WHERE id=? AND version=?
==> Parameters: 1(Integer), 0(Integer)
<== Columns: id, name, age, email, version, create_time, update_time
<== Row: 1, 张三, 20, zhangsan@example.com, 0, 2024-01-01, 2024-01-01
==> Preparing: UPDATE user SET name=?, age=?, email=?, version=?, update_time=? WHERE id=? AND version=?
==> Parameters: 张三(String), 21(Integer), zhangsan@example.com(String), 1(Integer), 2024-01-01, 1(Long), 0(Integer)
关键点说明
乐观锁实现原理
- 查询时获取
version字段 - 更新时执行:
UPDATE ... WHERE id = ? AND version = ? - 更新成功后自动将
version + 1
注意事项
@Version注解:必须加在版本字段上- 更新实体:使用
updateById时会自动处理version - 更新Wrapper:使用
update(UpdateWrapper)时需手动处理version - 自动填充:插入时可设置初始version值为0
使用场景
- 商品库存更新
- 订单状态流转
- 用户信息修改
- 优惠券使用等需要防止并发冲突的业务
这个案例完整展示了MyBatis-Plus乐观锁的配置和使用方法,包含并发场景测试,可以实际运行验证效果。