MyBatis-Plus 3.5.8版本逻辑删除与乐观锁冲突问题解析

MyBatis-Plus 3.5.8版本逻辑删除与乐观锁冲突问题解析

【免费下载链接】mybatis-plus mybatis 增强工具包,简化 CRUD 操作。 文档 http://baomidou.com 低代码组件库 http://aizuda.com 【免费下载链接】mybatis-plus 项目地址: https://gitcode.com/baomidou/mybatis-plus

引言

在日常的企业级应用开发中,数据的安全性和一致性是至关重要的。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);  // 这里可能出现问题
    }
}

在上述场景中,当多个线程同时执行删除操作时,可能会出现以下异常情况:

  1. 删除操作返回影响行数为0,但数据实际存在
  2. 乐观锁版本号未按预期更新
  3. 逻辑删除状态设置异常

技术背景

为了更好地理解问题,我们先来回顾一下MyBatis-Plus中这两个功能的实现机制:

逻辑删除实现原理

mermaid

乐观锁实现原理

mermaid

问题根因分析

冲突机制剖析

在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));
    }
}

核心问题点

  1. 执行顺序冲突:当同时启用逻辑删除和乐观锁时,拦截器的执行顺序可能导致:

    • 逻辑删除将DELETE操作转换为UPDATE操作
    • 乐观锁在UPDATE操作中添加版本号条件
    • 但版本号字段可能在逻辑删除处理时被错误过滤
  2. 字段过滤逻辑缺陷:在getAllSqlSet方法中,逻辑删除字段被过滤掉,导致乐观锁版本号字段无法正常更新

  3. 条件拼接异常:逻辑删除条件和乐观锁条件在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);
}
拦截器执行顺序优化

mermaid

升级指南

版本升级步骤
  1. 检查当前版本
<!-- pom.xml中查看当前版本 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.7</version> <!-- 如果是旧版本 -->
</dependency>
  1. 升级到3.5.8+版本
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.8</version> <!-- 或更高版本 -->
</dependency>
  1. 验证修复效果
// 测试代码验证修复
@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版本中逻辑删除与乐观锁的冲突问题是一个典型的功能交互缺陷,通过本文的分析我们可以看到:

问题总结

  1. 根本原因:拦截器执行顺序和字段过滤逻辑缺陷导致版本号更新丢失
  2. 影响范围:同时使用@TableLogic@Version注解的实体类
  3. 解决方案:升级到3.5.8+版本并正确配置拦截器顺序

经验教训

  1. 功能测试完整性:新增功能时需要充分考虑与其他功能的交互影响
  2. 拦截器设计原则:明确各拦截器的职责边界和执行顺序
  3. 版本升级策略:生产环境升级前必须进行充分的兼容性测试

未来展望

随着MyBatis-Plus的持续发展,我们期待在以下方面的改进:

  1. 更智能的冲突检测:框架层面自动检测功能冲突并给出警告
  2. 更灵活的配置方式:支持更细粒度的功能交互控制
  3. 更完善的监控体系:内置更多的性能指标和健康检查项

通过本文的深入分析,相信读者已经对MyBatis-Plus中逻辑删除与乐观锁的冲突问题有了全面的理解。在实际项目开发中,合理使用这些功能并遵循最佳实践,将能够构建出更加健壮和可靠的数据访问层。

提醒:无论使用哪个版本的MyBatis-Plus,都建议在重要操作前后添加充分的日志记录和监控指标,以便及时发现和解决潜在的问题。

【免费下载链接】mybatis-plus mybatis 增强工具包,简化 CRUD 操作。 文档 http://baomidou.com 低代码组件库 http://aizuda.com 【免费下载链接】mybatis-plus 项目地址: https://gitcode.com/baomidou/mybatis-plus

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值