为啥不用 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应用和工具的分享。关注我,少走弯路,一起进步!

相关推荐
武子康2 小时前
大数据-242 离线数仓 - DataX 实战:MySQL 全量/增量导入 HDFS + Hive 分区(离线数仓 ODS
大数据·后端·apache hive
砍材农夫3 小时前
TCP和UDP区别
后端
千寻girling3 小时前
一份不可多得的 《 Django 》 零基础入门教程
后端·python·面试
千寻girling3 小时前
Python 是用来做 AI 人工智能 的 , 不适合开发 Web 网站 | 《Web框架》
人工智能·后端·算法
贾铭3 小时前
如何实现一个网页版的剪映(三)使用fabric.js绘制时间轴
前端·后端
xiaoye20183 小时前
Spring 自定义 Redis 超时:TTL、TTI 与 Pipeline 实战
后端
xiaoye20184 小时前
Lettuce连接模型、命令执行、Pipeline 浅析
java
程序员爱钓鱼6 小时前
GoHTML解析利器:github.com/PuerkitoBio/goquery实战指南
后端·google·go
golang学习记7 小时前
从“大泥球“到模块化单体:Spring Modulith + IntelliJ IDEA 拯救你的代码
后端·intellij idea