为啥不用 MP 的 saveOrUpdateBatch?MySQL 一条 SQL 批量增改才是最优解

大家好,我是大华。

在写后端业务的时候,对于数据的批量操作,我们常常会碰到一种场景:存在就更新,不存在就新增

如果用循环查库,再插入或者更新的方式,不仅代码啰嗦,数据量大的时候还容易被卡死,数据库的压力也很大。一般不会这么操作。用 MyBatis-Plus 的批量方法,在高并发的情况下,还是可能会插入重复数据。

所以这里介绍的是 MySql 的 ON DUPLICATE KEY UPDATE 写法。

举个例子

这是一张电商广告运营的核心表,存的是每天各店铺、各SKU的广告花费、销量、库存等数据:

sql 复制代码
CREATE TABLE `ad_operation_daily` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `shop_name` varchar(100) DEFAULT NULL COMMENT '店铺名称',
  `asin` varchar(20) DEFAULT NULL COMMENT 'ASIN',
  `local_sku` varchar(50) DEFAULT NULL COMMENT 'SKU',
  `ad_spend` decimal(10,2) DEFAULT '0.00' COMMENT '广告花费金额',
  `volume` bigint(20) DEFAULT NULL COMMENT '销量',
  `amount` decimal(15,2) DEFAULT NULL COMMENT '销售额',
  `data_date` datetime DEFAULT NULL COMMENT '数据日期',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  -- 核心:唯一索引,判断"重复"的依据
  UNIQUE KEY `uk_shop_asin_sku_date` (`shop_name`,`asin`,`local_sku`,`data_date`) USING BTREE COMMENT '唯一索引,防止重复数据',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='广告运营数据日表(简化版)';

可以看到数据表中有一个唯一索引 uk_shop_asin_sku_date ,意思是 shop_name+asin+local_sku+data_date 这四个字段组合起来不能重复。

我们这里的需求也很明确:

  • 重复了就更新广告花费、销量、销售额等数据;
  • 没重复就新增一条。

MyBatis 实现代码

不用写复杂逻辑,直接在 MyBatis 的 XML 里写批量插入 SQL,加上 ON DUPLICATE KEY UPDATE 就行。

1、MyBatis XML里的批量插入和更新SQL

xml 复制代码
<insert id="insertOrUpdateBatch">
    INSERT INTO ad_operation_daily (
        shop_name,
        asin,
        local_sku,
        ad_spend,
        volume,
        amount,
        data_date,
        update_time
    ) VALUES
    <foreach collection="list" item="item" separator=",">
        (
            #{item.shopName},
            #{item.asin},
            #{item.localSku},
            #{item.adSpend},
            #{item.volume},
            #{item.amount},
            #{item.dataDate},
            NOW()
        )
    </foreach>
    ON DUPLICATE KEY UPDATE
        ad_spend = VALUES(ad_spend),  -- 重复时更新广告花费
        volume = VALUES(volume),      -- 重复时更新销量
        amount = VALUES(amount),      -- 重复时更新销售额
        update_time = NOW()           -- 重复时更新时间
</insert>

2、Java 代码调用

java 复制代码
// 1. 实体类(对应表字段,不用多写)
@Data
public class AdOperationDaily {
    private Long id;
    private String shopName;
    private String asin;
    private String localSku;
    private BigDecimal adSpend;
    private Long volume;
    private BigDecimal amount;
    private Date dataDate;
    private Date updateTime;
}

// 2. Mapper接口
public interface AdOperationDailyMapper {
    void insertOrUpdateBatch(@Param("list") List<AdOperationDaily> list);
}

// 3. 业务层调用
@Service
public class AdOperationDailyService {
    @Autowired
    private AdOperationDailyMapper adOperationDailyMapper;
    
    public void batchSyncData(List<AdOperationDaily> dataList) {
        // 直接调用批量插入更新方法
        adOperationDailyMapper.insertOrUpdateBatch(dataList);
    }
}

解释

1. 唯一索引是核心

uk_shop_asin_sku_date 这个唯一索引是判断重复的关键,没有它,ON DUPLICATE KEY UPDATE 就会失效;

2. VALUES(字段名)的含义

指的是前面 INSERT 里要插入的这个字段的值,比如 ad_spend = VALUES(ad_spend),就是用新数据的广告花费覆盖旧数据;

3. 批量处理的优势

不管是10条还是1000条数据,<foreach> 会把数据拼成多组 VALUES,一次 SQL 搞定,比循环插库快几十倍;

4. 原子性

在默认 InnoDB + 事务控制下,这条 SQL 是原子执行的,要么全成功要么全失败,不会出现"部分插、部分更"的情况,高并发下也不会插重复数据。

为啥不用 MyBatis-Plus?

有兄弟会问,MyBatis-Plus 的 saveOrUpdateBatch() 不用写XML,为啥不用?结合这个广告数据场景,说两个核心问题:

问题1:MP判断重复的依据是主键,不是唯一索引

咱们的表主键是自增的 id,但判断重复的是 shop_name+asin+local_sku+data_date 这个唯一索引。

MP的批量方法只会判断主键id是否存在,哪怕唯一索引重复,只要id不一样,还是会插新数据,这会直接导致表里出现重复的运营数据。

坑2:高并发下坑你会重复

MyBatis-Plus 的批量方法底层是"先查后改":先查每条数据的主键是否存在,再插/更。

同步广告数据时,高并发下两个请求同时查,都发现"没这条数据",就会同时插入,导致唯一索引冲突报错,或者插出重复数据。

而咱们用的 ON DUPLICATE KEY UPDATE 是数据库层面的原子操作,不管多高并发,只要唯一索引在,就不会出重复。

优化小技巧

1. 控制批量大小:

别一次传1万条数据,拆成500-1000条/批,避免SQL太长导致执行超时。

2.字段按需更新:

不用把所有字段都写在UPDATE里,只更变化的字段(比如广告花费、销量),能提升执行效率。

3.索引优化:

唯一索引uk_shop_asin_sku_date一定要建,查询用的data_dateshop_name也可以单独建索引,提升数据同步和查询速度。

4.避免空值覆盖:

如果新数据里某些字段是空的,不想覆盖旧数据,可以加判断,比如ad_spend = IF(VALUES(ad_spend) IS NOT NULL, VALUES(ad_spend), ad_spend)

本文首发于公众号:程序员大华,专注前端、Java开发,AI应用和工具的分享。关注我,少走弯路,一起进步!

相关推荐
葫芦和十三44 分钟前
图解 MongoDB 22|读写关注:持久性与一致性的档位选择
后端·mongodb·agent
葫芦和十三7 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp8 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑8 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯9 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan11 小时前
多Agent之间的区别
后端
青石路13 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充13 小时前
1.面向对象设计思想
后端
IT_陈寒14 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro14 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端