MyBatis-Plus 3.5.8版本逻辑删除与乐观锁冲突问题解析
引言
在日常的企业级应用开发中,数据的安全性和一致性是至关重要的。MyBatis-Plus作为MyBatis的增强工具包,提供了逻辑删除(Logic Delete)和乐观锁(Optimistic Locking)两大核心功能来保障数据操作的完整性和并发安全性。然而,在3.5.8版本中,这两个功能的结合使用却出现了一个隐蔽但严重的冲突问题。
你是否遇到过这样的场景:在并发环境下执行逻辑删除操作时,明明数据存在却删除失败?或者乐观锁版本号更新异常?本文将深入剖析MyBatis-Plus 3.5.8版本中逻辑删除与乐观锁的冲突问题,并提供完整的解决方案。
问题现象与背景
典型问题场景
// 实体类定义
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableLogic // 逻辑删除注解
private Integer deleted;
@Version // 乐观锁注解
private Integer version;
}
// 业务代码 - 并发删除操作
@Transactional
public void concurrentDelete(Long userId) {
User user = userMapper.selectById(userId);
if (user != null) {
userMapper.deleteById(userId); // 这里可能出现问题
}
}
在上述场景中,当多个线程同时执行删除操作时,可能会出现以下异常情况:
- 删除操作返回影响行数为0,但数据实际存在
- 乐观锁版本号未按预期更新
- 逻辑删除状态设置异常
技术背景
为了更好地理解问题,我们先来回顾一下MyBatis-Plus中这两个功能的实现机制:
逻辑删除实现原理
乐观锁实现原理
问题根因分析
冲突机制剖析
在MyBatis-Plus 3.5.8版本中,逻辑删除和乐观锁的冲突主要发生在SQL拦截器的执行顺序上。让我们通过一个具体的代码示例来分析:
// 问题代码片段 - 旧版本实现
public class TableInfo {
// 逻辑删除SQL生成
public String getLogicDeleteSql(boolean startWithAnd, boolean isWhere) {
if (withLogicDelete) {
String logicDeleteSql = formatLogicDeleteSql(isWhere);
// ... 省略其他代码
}
return EMPTY;
}
// 乐观锁处理
public String getAllSqlSet(boolean ignoreLogicDelFiled, final String prefix) {
return fieldList.stream()
.filter(i -> {
if (ignoreLogicDelFiled) {
return !(isWithLogicDelete() && i.isLogicDelete());
}
return true;
}).map(i -> i.getSqlSet(newPrefix)).filter(Objects::nonNull)
.collect(joining(NEWLINE));
}
}
核心问题点
-
执行顺序冲突:当同时启用逻辑删除和乐观锁时,拦截器的执行顺序可能导致:
- 逻辑删除将DELETE操作转换为UPDATE操作
- 乐观锁在UPDATE操作中添加版本号条件
- 但版本号字段可能在逻辑删除处理时被错误过滤
-
字段过滤逻辑缺陷:在
getAllSqlSet方法中,逻辑删除字段被过滤掉,导致乐观锁版本号字段无法正常更新 -
条件拼接异常:逻辑删除条件和乐观锁条件在WHERE子句中的拼接顺序可能影响查询结果
问题复现
让我们通过一个具体的SQL生成示例来演示这个问题:
-- 期望生成的SQL(正确)
UPDATE user SET deleted = 1, version = version + 1 WHERE id = ? AND version = ?
-- 实际生成的SQL(错误)- 3.5.8版本
UPDATE user SET deleted = 1 WHERE id = ? AND version = ?
-- 注意:version字段的更新丢失了!
解决方案
MyBatis-Plus官方修复
在3.5.8版本中,官方已经识别并修复了这个问题。修复的核心在于:
修复后的关键代码
// 修复后的getAllSqlSet方法
public String getAllSqlSet(boolean ignoreLogicDelFiled, final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
.filter(i -> {
if (ignoreLogicDelFiled) {
// 修复:不再过滤逻辑删除字段,而是由具体业务逻辑处理
return !(isWithLogicDelete() && i.isLogicDelete() && i.isWithUpdateFill());
}
return true;
}).map(i -> i.getSqlSet(newPrefix)).filter(Objects::nonNull)
.collect(joining(NEWLINE));
}
// 新增的逻辑删除专用处理方法
protected String formatLogicDeleteWithVersionSql(boolean isWhere) {
if (withLogicDelete && withVersion) {
// 同时处理逻辑删除和版本号更新
String logicSql = formatLogicDeleteSql(isWhere);
String versionSql = versionFieldInfo.getColumn() + " = " +
versionFieldInfo.getColumn() + " + 1";
return versionSql + ", " + logicSql;
}
return formatLogicDeleteSql(isWhere);
}
拦截器执行顺序优化
升级指南
版本升级步骤
- 检查当前版本
<!-- pom.xml中查看当前版本 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.7</version> <!-- 如果是旧版本 -->
</dependency>
- 升级到3.5.8+版本
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.8</version> <!-- 或更高版本 -->
</dependency>
- 验证修复效果
// 测试代码验证修复
@Test
public void testLogicDeleteWithOptimisticLock() {
User user = new User();
user.setName("test");
userMapper.insert(user);
Long id = user.getId();
// 第一次删除应该成功
int result1 = userMapper.deleteById(id);
assertEquals(1, result1);
// 第二次删除应该失败(数据已逻辑删除)
int result2 = userMapper.deleteById(id);
assertEquals(0, result2);
// 验证版本号更新
User deletedUser = userMapper.selectById(id);
assertNotNull(deletedUser);
assertEquals(1, deletedUser.getVersion()); // 版本号应该+1
assertEquals(1, deletedUser.getDeleted()); // 逻辑删除标志应该为1
}
配置调整建议
正确的拦截器配置
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁拦截器
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 添加逻辑删除拦截器(注意顺序)
interceptor.addInnerInterceptor(new LogicDeleteInnerInterceptor());
// 其他拦截器...
return interceptor;
}
}
实体类注解配置
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
// 逻辑删除配置
@TableLogic(value = "0", delval = "1")
private Integer deleted;
// 乐观锁配置
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;
// 其他字段...
}
最佳实践
并发场景下的处理策略
重试机制实现
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
private static final int MAX_RETRY = 3;
/**
* 安全的逻辑删除方法(带重试机制)
*/
@Transactional
public boolean safeDelete(Long userId) {
int retryCount = 0;
while (retryCount < MAX_RETRY) {
try {
User user = userMapper.selectById(userId);
if (user == null) {
log.warn("用户不存在: {}", userId);
return false;
}
int affected = userMapper.deleteById(userId);
if (affected > 0) {
log.info("成功删除用户: {}", userId);
return true;
}
// 删除失败,可能是版本冲突
retryCount++;
log.warn("删除失败,重试第{}次", retryCount);
Thread.sleep(100 * retryCount); // 指数退避
} catch (Exception e) {
log.error("删除用户异常", e);
retryCount++;
}
}
log.error("删除用户失败,达到最大重试次数: {}", userId);
return false;
}
}
监控与日志配置
添加专门的监控点
# application.yml 配置
management:
endpoints:
web:
exposure:
include: health,info,metrics
metrics:
tags:
application: ${spring.application.name}
logging:
level:
com.baomidou.mybatisplus: DEBUG
com.example.mapper: DEBUG
自定义监控指标
@Component
public class MybatisPlusMetrics {
private final MeterRegistry meterRegistry;
private final AtomicLong logicDeleteCount = new AtomicLong(0);
private final AtomicLong optimisticLockConflict = new AtomicLong(0);
public MybatisPlusMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
initMetrics();
}
private void initMetrics() {
// 逻辑删除计数器
Gauge.builder("mybatisplus.logic.delete.count", logicDeleteCount, AtomicLong::get)
.description("逻辑删除操作计数")
.register(meterRegistry);
// 乐观锁冲突计数器
Gauge.builder("mybatisplus.optimistic.lock.conflict", optimisticLockConflict, AtomicLong::get)
.description("乐观锁冲突次数")
.register(meterRegistry);
}
public void incrementLogicDelete() {
logicDeleteCount.incrementAndGet();
}
public void incrementOptimisticLockConflict() {
optimisticLockConflict.incrementAndGet();
}
}
性能优化建议
数据库层面优化
-- 为逻辑删除和版本号字段添加索引
CREATE INDEX idx_deleted ON user(deleted);
CREATE INDEX idx_version ON user(version);
-- 复合索引优化查询性能
CREATE INDEX idx_deleted_version ON user(deleted, version);
应用层面优化
// 批量处理优化
@Transactional
public void batchLogicDelete(List<Long> userIds) {
if (CollectionUtils.isEmpty(userIds)) {
return;
}
// 使用批量操作减少数据库交互
userMapper.update(
new User().setDeleted(1),
Wrappers.<User>lambdaUpdate()
.set(User::getVersion, User::getVersion + 1)
.in(User::getId, userIds)
.eq(User::getDeleted, 0)
);
}
总结与展望
MyBatis-Plus 3.5.8版本中逻辑删除与乐观锁的冲突问题是一个典型的功能交互缺陷,通过本文的分析我们可以看到:
问题总结
- 根本原因:拦截器执行顺序和字段过滤逻辑缺陷导致版本号更新丢失
- 影响范围:同时使用
@TableLogic和@Version注解的实体类 - 解决方案:升级到3.5.8+版本并正确配置拦截器顺序
经验教训
- 功能测试完整性:新增功能时需要充分考虑与其他功能的交互影响
- 拦截器设计原则:明确各拦截器的职责边界和执行顺序
- 版本升级策略:生产环境升级前必须进行充分的兼容性测试
未来展望
随着MyBatis-Plus的持续发展,我们期待在以下方面的改进:
- 更智能的冲突检测:框架层面自动检测功能冲突并给出警告
- 更灵活的配置方式:支持更细粒度的功能交互控制
- 更完善的监控体系:内置更多的性能指标和健康检查项
通过本文的深入分析,相信读者已经对MyBatis-Plus中逻辑删除与乐观锁的冲突问题有了全面的理解。在实际项目开发中,合理使用这些功能并遵循最佳实践,将能够构建出更加健壮和可靠的数据访问层。
提醒:无论使用哪个版本的MyBatis-Plus,都建议在重要操作前后添加充分的日志记录和监控指标,以便及时发现和解决潜在的问题。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



