反例:循环里一次次改库存
退货时,订单里有几种商品,就在代码里循环几种商品,每循环一次就向数据库发一条「把某个商品的库存加回去」的更新。
退货 3 种商品
→ 更新库存(商品 A)
→ 更新库存(商品 B)
→ 更新库存(商品 C)
→ 共 3 次数据库往返
看起来直观,但有几个问题:
- 商品一多,数据库往返次数线性上涨
- 同一商品若出现多行,还可能被更新多次,既慢又容易算错
正例:先汇总,再一条语句批量更新
先把「要退的商品 + 数量」在内存里整理成一张小清单(同一商品合并数量),再交给数据库 一次更新:用分支表达式按商品编号决定每一行加多少。
退货明细
A × 2
B × 5
A × 1
↓ 合并
清单:A→3,B→5
↓
一条更新:
仓里的 A 库存 +3
仓里的 B 库存 +5
伪代码大意:
1. 遍历退货明细,得到 Map:商品 → 总数量
2. 转成列表:[{商品, 数量}, ...]
3. 执行一次批量 UPDATE:
库存 = 库存 +(按商品编号分支:A 加 3,B 加 5,...)
且只更新列表里出现的那些商品
好处
| 点 | 说明 |
|---|---|
| 更少往返 | N 次更新变成 1 次,延迟和连接开销更低 |
| 语义更清晰 | 「这一单要回多少货」先算清,再落库,业务和写库分层 |
| 避免重复更新 | 同商品多行先合并,库存只加一次正确总量 |
| 更好维护 | 以后换批量策略(分批、消息队列)只动落库这一层 |
一句话:不要在循环里打数据库,先把变更清单算好,再让数据库一次按清单改到位。
代码
优化前
java
// 退货回补库存 ------ 优化前:N 次 SQL
for (OrderLine line : refundLines) {
if (line.getSkuCode() == null) {
continue;
}
BigDecimal qty = line.getQty() != null
? line.getQty().abs()
: BigDecimal.ZERO;
if (qty.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
// 每次循环打一次库
stockService.increase(warehouseId, line.getSkuCode(), qty.intValue());
}
对应 Mapper:
xml
<!-- 单条加库存 -->
<update id="increase">
UPDATE warehouse_stock
SET qty = qty + #{qty}
WHERE warehouse_id = #{warehouseId}
AND sku_code = #{skuCode}
</update>
优化后:聚合 + 一条批量 SQL
java
// 退货回补库存 ------ 优化后:先聚合,再批量 1 条 SQL
Map<String, BigDecimal> qtyBySku = new HashMap<>();
for (OrderLine line : refundLines) {
if (line.getSkuCode() == null) {
continue;
}
BigDecimal qty = line.getQty() != null
? line.getQty().abs()
: BigDecimal.ZERO;
if (qty.compareTo(BigDecimal.ZERO) <= 0) {
continue;
}
// 同一 SKU 合并,避免重复更新
qtyBySku.merge(line.getSkuCode(), qty, BigDecimal::add);
}
if (!qtyBySku.isEmpty()) {
List<StockChangeItem> items = new ArrayList<>();
for (Map.Entry<String, BigDecimal> e : qtyBySku.entrySet()) {
items.add(new StockChangeItem(e.getKey(), e.getValue().intValue()));
}
stockService.batchIncrease(warehouseId, items);
}
DTO:
java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StockChangeItem {
private String skuCode;
private Integer qty;
}
批量 SQL(核心):
xml
<!-- 批量加库存:一条 UPDATE + CASE WHEN -->
<update id="batchIncrease">
UPDATE warehouse_stock
SET qty = qty + CASE sku_code
<foreach collection="items" item="it">
WHEN #{it.skuCode} THEN #{it.qty}
</foreach>
ELSE 0
END
WHERE warehouse_id = #{warehouseId}
AND sku_code IN
<foreach collection="items" item="it" open="(" separator="," close=")">
#{it.skuCode}
</foreach>
</update>
sql
UPDATE warehouse_stock
SET qty = qty + CASE sku_code
WHEN 'SKU001' THEN 2
WHEN 'SKU002' THEN 5
ELSE 0
END
WHERE warehouse_id = 'WH01'
AND sku_code IN ('SKU001', 'SKU002');