问题描述

在观看黑马的苍穹外卖系列教程中的用户取消订单功能代码开发时,我们注意到作者首先通过查询获取了被用户取消的订单的所有属性,并存储在ordersDB变量中
然而,在修改数据库时,作者并未直接对ordersDB变量进行操作,而是创建了一个新的Orders类型的orders变量,仅包含需要修改的属性,然后进行了update操作
详细代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Orders implements Serializable {

/**
* 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
*/
public static final Integer PENDING_PAYMENT = 1;
public static final Integer TO_BE_CONFIRMED = 2;
public static final Integer CONFIRMED = 3;
public static final Integer DELIVERY_IN_PROGRESS = 4;
public static final Integer COMPLETED = 5;
public static final Integer CANCELLED = 6;

/**
* 支付状态 0未支付 1已支付 2退款
*/
public static final Integer UN_PAID = 0;
public static final Integer PAID = 1;
public static final Integer REFUND = 2;

private static final long serialVersionUID = 1L;

private Long id;

//订单号
private String number;

//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 7退款
private Integer status;

//下单用户id
private Long userId;

//地址id
private Long addressBookId;

//下单时间
private LocalDateTime orderTime;

//结账时间
private LocalDateTime checkoutTime;

//支付方式 1微信,2支付宝
private Integer payMethod;

//支付状态 0未支付 1已支付 2退款
private Integer payStatus;

//实收金额
private BigDecimal amount;

//备注
private String remark;

//用户名
private String userName;

//手机号
private String phone;

//地址
private String address;

//收货人
private String consignee;

//订单取消原因
private String cancelReason;

//订单拒绝原因
private String rejectionReason;

//订单取消时间
private LocalDateTime cancelTime;

//预计送达时间
private LocalDateTime estimatedDeliveryTime;

//配送状态 1立即送出 0选择具体时间
private Integer deliveryStatus;

//送达时间
private LocalDateTime deliveryTime;

//打包费
private int packAmount;

//餐具数量
private int tablewareNumber;

//餐具数量状态 1按餐量提供 0选择具体数量
private Integer tablewareStatus;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void userCancelById(Long id) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);

// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}

//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
if (ordersDB.getStatus() > 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

Orders orders = new Orders();
orders.setId(ordersDB.getId());

// 订单处于待接单状态下取消,需要进行退款
if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
//调用微信支付退款接口
weChatPayUtil.refund(
ordersDB.getNumber(), //商户订单号
ordersDB.getNumber(), //商户退款单号
new BigDecimal(0.01),//退款金额,单位 元
new BigDecimal(0.01));//原订单金额

//支付状态修改为 退款
orders.setPayStatus(Orders.REFUND);
}

// 更新订单状态、取消原因、取消时间
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("用户取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>

原因分析

在Orders类中,属性众多,而update方法作为一个通用的数据库更新操作,可能涉及多个属性的修改
该方法在更新时,会根据传入的Orders对象中的属性值进行条件判断,若属性值非空则进行更新(有新数据就更新成最新数据,没有新数据就保持原样)
然而,在用户取消订单的场景中,实际需要修改的属性仅有三个:订单状态、取消原因和取消时间
所以有必要进行这么多次的判断和修改吗?
答案当然是没有
如果直接对ordersDB变量执行update操作,可能会引发以下两种问题

并发操作下的更新覆盖问题

在并发环境下,多个用户可能同时对同一组数据进行操作
我们用一个例子来说明这个问题
用户A正在执行id修改update操作,希望将id由1修改为2
用户B执行了用户ID换绑操作,希望将userId由LN修改为LTC
由于数据库操作的原子性,这两个操作在数据库层面是串行执行的
然而,如果直接对ordersDB变量进行update,那么即使后一个操作(用户ID换绑)先完成,其更改也可能被前一个操作(更改id)中的update语句所覆盖

1
2
3
4
5
6
7
8
OrderDB:
id: 1
number:3
status:1->2 #修改状态为2
userId: LN
addressBookId: 3
orderTime:2024-11-15 13:05:06
checkTime:2024-11-15 13:05:06
1
2
3
4
5
6
7
8
OrderDB:
id: 1
number:3
status: 1
userId: LN ->LTC #修改userId
addressBookId:3
orderTime:2024-11-15 13:05:07
checkTime:2024-11-15 13:05:07
1
2
3
4
5
6
7
8
OrderDB:
id: 1
number: 3
status: 1
userId: LTC
addressBookId:3
orderTime:2024-11-15 13:05:08
checkTime:2024-11-15 13:05:08
1
2
3
4
5
6
7
8
OrderDB:
id: 1
number:3
status:2
userId: LN #重新以1的userld覆盖了刚才的更改
addressBookId:3
orderTime:2024-11-15 13:05:06
checkTime:2024-11-15 13:05:06

为了避免这种并发操作下的更新覆盖问题,作者选择创建一个新的Orders对象orders,仅包含需要修改的属性,并对其进行update操作。这样,即使存在并发操作,也只会影响需要修改的属性,而不会覆盖其他属性的更改。

增加的 Binlog 日志数量及性能影响

当选择直接修改现有的 ordersDB 数据库中的表结构或数据记录时
每次修改(无论是添加、删除还是更新数据)都会被记录在 MySQL 的二进制日志(binlog)中
这些日志对于数据恢复、复制和审计至关重要,然而,如果频繁地对大型表进行结构修改或大量数据更新,会导致 binlog 日志量显著增加

总结

在进行数据库更新操作时,通过创建新的对象并仅修改需要更新的属性,可以有效地避免并发操作下的更新覆盖问题,并减少不必要的binlog日志生成,从而优化数据库性能和稳定性