Mybatisplus中saveOrUpdate()方法并发问题

MybatisplussaveOrUpdate()方法底层执行逻辑:先查询数据是否存在(通过判断主键是否为null),如果数据不存在则做插入操作,否则做更新操作。根据这个执行过程来看,saveOrUpdate()方法会首先进行查询操作,再决定之后执行什么操作。那么在并发场景下使用该方法就用可能出现并发问题

saveOrUpdate()方法默认只通过主键判断存在性,不能用业务字段替代判断。所以要想使用该方法,数据库表必须建立主键。

一、潜在的并发问题

1. 竞态条件(Race Condition)

竞态条件(Race Condition)是多线程/多进程中的一个并发问题,当多个线程/进程并发访问和修改共享资源时,如果操作的执行顺序不加以控制,就会导致不确定的结果或错误,这就叫做竞态条件

  • 现象:多个线程同时使用saveOrUpdate(),若某个线程在检查记录是否存在(SELECT)后、执行插入或更新操作(INSERT/UPDATE)前,有另外的线程先修改了记录,那就有可能造成数据的不一致。

  • 示例场景:

    • 线程A执行saveOrUpdate(),查询到主键为16的记录不存在,准备执行插入(INSERT)操作时;

    • 线程B在此时先插入了主键为16的记录,那么就导致线程A的插入因主键冲突而失败;

    • 或者线程A插入成功,但是线程B判断为不存在,也执行插入操作,也会导致主键冲突而插入失败

2. 唯一键冲突

当表中存在唯一索引(如唯一字段)时,在高并发的场景下,多个线程同时尝试插入相同的数据时,数据库会抛出唯一键冲突异常(如MySQL的 Duplicate entry)。

3. 非原子性操作

saveOrUpdate()内部先进行查询,再进行插入/更新,这两个操作都不是原子操作,在高并发场景下,无法保证操作的原子性。

二、解决方案

1. 依赖数据库的唯一约束

  • 为表中关键字段(如业务唯一键)添加唯一索引,在数据库层面保证数据不会重复插入。

  • 捕获唯一键冲突异常,将插入操作转为更新操作

try {
    myMapper.insert(entity);
} catch (DuplicateKeyException e) {
    myMapper.updateById(entity); // 或其他更新逻辑
}

2. 使用乐观锁

  • 在数据库表中添加版本号字段(如version),通过@Version注解实现乐观锁。更新时自动校验版本号,避免并发覆盖。

@Version
private Integer version;

3. 使用悲观锁

  • 在查询时通过SELECT......FOR UPDATE加锁,阻塞其他事务修改数据。

// 需手动编写带锁的查询
MyEntity entity = myMapper.selectOne(new QueryWrapper().eq("id", id).last("FOR UPDATE"));

4. 使用INSERT......ON DUPLICATE KEY UPDATE

  • 如果数据库使用的是MySql,可以使用INSERT......ON DUPLICATE KEY UPDATE语法,将插入和更新操作合并为原子操作。

这个语法是MySql提供的,当想要在插入数据时,自动判断是否存在主键或唯一索引冲突,就可以使用ON DUPLICATE KEY UPDATE语法,将插入(INSERT)和更新(UPDATE)合并为原子操作(不会被其他线程打断)。

注意事项:

  1. 数据库表中的字段中必须存在主键(PRIMARY KEY)或唯一索引(UNIQUE)

  2. 这个语法只能处理唯一键冲突的情况,不能控制复杂的条件更新

5. 加分布式锁

分布式系统中,可以对关键操作加上分布式锁(如Redis锁),确保在同一时间内只有一个节点能够执行saveOrUpdate()操作。

你可能感兴趣的:(java,Mybatisplus,高并发)