大家好,我是大华。
在写后端业务的时候,对于数据的批量操作,我们常常会碰到一种场景:存在就更新,不存在就新增。
如果用循环查库,再插入或者更新的方式,不仅代码啰嗦,数据量大的时候还容易被卡死,数据库的压力也很大。一般不会这么操作。用 MyBatis-Plus 的批量方法,在高并发的情况下,还是可能会插入重复数据。
所以这里介绍的是 MySql 的 ON DUPLICATE KEY UPDATE 写法。
举个例子
这是一张电商广告运营的核心表,存的是每天各店铺、各SKU的广告花费、销量、库存等数据:
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
<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 代码调用
// 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_date、shop_name也可以单独建索引,提升数据同步和查询速度。
4.避免空值覆盖:
如果新数据里某些字段是空的,不想覆盖旧数据,可以加判断,比如ad_spend = IF(VALUES(ad_spend) IS NOT NULL, VALUES(ad_spend), ad_spend)。
本文首发于公众号:程序员大华,专注前端、Java开发,AI应用和工具的分享。关注我,少走弯路,一起进步!