写作背景
最近一学妹跳槽到北京某信,闲聊的时候,发现学妹在做餐厅的后端,女生做后端,很强。我说你个餐厅能做什么???然后她秀烂了的我。下面进入正题。
需求背景
你可以在公司吃,也可以点外卖(送到固定的餐柜里)。午餐如果在食堂吃免费,扣主卡的免费次数;如果点外卖,如果你在食堂吃了(扣主卡的免费次数),那么扣副卡的券,反之扣主卡的免费次数。晚餐只扣副卡的券。
你可以一下定一星期的饭,比如现在周一,我可以定明天的午餐,后天的晚餐,大后天的午餐和晚餐。
下单需要调用2个第三方系统,外卖系统和卡系统。
我的想法:so easy
首先以上面为例:定明天的午餐,后天的晚餐,大后天的午餐和晚餐。
首先有一点要明确:我是下4单还是下1单,我说下4单啊,学妹说,如果我只想退明天的午餐呢?所以下4单。
流程图
伪代码
代码语言:javascript复制@RestController
public class OrderController {
@Transactional
public void order(List<Order> orders) throws Exception {
//检查条件
checkCondition(orders);
for (Order order : orders) {
//检查食物是否够
String sql = "select num from food where food id =" order.getFoodId();
Integer number = (Integer) executeSql(sql);
if (number <= 0) {
throw new Exception("没饭了");
}
//库存减一
sql = "update num set num = num - 1 where food id = " order.getFoodId();
executeSql(sql);
//检查柜子是否够
sql = "select num from bod where box id =" order.getBoxId();
number = (Integer) executeSql(sql);
if (number <= 0) {
throw new Exception("没柜子了");
}
//库存减一
sql = "update num set num = num - 1 where box id = " order.getBoxId();
executeSql(sql);
//判断该订单消费主卡还是副卡
addCardType(order);
//扣钱(第三方卡系统)
if (order.getCard() == "主") {
String res = feiginClient("主卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣主卡失败");
}
} else {
String res = feiginClient("副卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣副卡失败");
}
}
//下单(第三方下单系统)
String s = feiginClient(order.getFoodId());
if (null == s || !"ok".equals(s)) {
throw new Exception("下单失败");
}
}
}
private void checkCondition(Object object) {
}
private void addCardType(Order order) {
//一些判断
}
//f执行SQL
private Object executeSql(String sql) {
return null;
}
//feign远程调用第三方系统
private String feiginClient(Object param) {
}
}
@Data
class Order {
//食品ID
private Integer foodId;
//餐柜ID
private Integer boxId;
//用餐类型 午餐 晚餐
private String type;
//订餐时间 2021-01-01
private String dataTime;
//主卡还是副卡: 主,副
private String card;
}
学妹评论
你这代码太垃圾了,一致性都保证不了,我说,我开启事务了,你看不到吗?
举一个例子:你下2单,你的代码中for循环中的第一个成功了,第二个在feign调用的时候出现问题了,请问你第一个for循环中扣的券怎么办???我。。。
学妹,请开始你的表演
版本一:保证数据一致性(当然,我这里的事务失效了,大体上思路重要)
你要记录你哪些成功了,然后在执行反向操作就行了。
代码语言:javascript复制@Transactional
public void orderV1(List<Order> orders) throws Exception {
//检查条件
checkCondition(orders);
//记录成功的订单
List record = new ArrayList();
try {
for (Order order : orders) {
//检查食物是否够
String sql = "select num from food where food id =" order.getFoodId();
Integer number = (Integer) executeSql(sql);
if (number <= 0) {
throw new Exception("没饭了");
}
//库存减一
sql = "update num set num = num - 1 where food id = " order.getFoodId();
executeSql(sql);
record.add(order.getFoodId() "ok");
//检查柜子是否够
sql = "select num from bod where box id =" order.getBoxId();
number = (Integer) executeSql(sql);
if (number <= 0) {
throw new Exception("没柜子了");
}
//库存减一
sql = "update num set num = num - 1 where box id = " order.getBoxId();
executeSql(sql);
record.add(order.getBoxId() "ok");
//判断该订单消费主卡还是副卡
addCardType(order);
//扣钱(第三方卡系统)
if (order.getCard() == "主") {
String res = feiginClient("主卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣主卡失败");
}
} else {
String res = feiginClient("副卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣副卡失败");
}
}
record.add(order.getCard() "card-ok");
//下单(第三方下单系统)
String s = feiginClient(order.getFoodId());
if (null == s || !"ok".equals(s)) {
throw new Exception("下单失败");
}
}
} catch (Exception e) {
try {
for (Object o : record) {
rollback(o);
}
} catch (Exception e) {
System.out.println("ask for root help");
}
}
}
private void checkCondition(Object object) {
}
private void rollback(Object object) {
//加一减一
}
版本二:减少持有数据库锁的时间
核心就是:不使用事务,使用乐观锁,如下所示
代码语言:javascript复制update num set num = num - x where food id = " order.getFoodId() "and num >= x
因为远程调用其实是比较耗时的,如果你一下锁很多记录,并发性就下来了。
代码语言:javascript复制public void orderV2(List<Order> orders) throws Exception {
//检查条件
checkCondition(orders);
//记录成功的订单
List record = new ArrayList();
try {
for (Order order : orders) {
//库存减一
String sql = "update num set num = num - 1 where food id = " order.getFoodId() "and num >= 1";
Integer row = (Integer) executeSql(sql);
if (row != 1) {
throw new Exception("没饭了");
}
record.add(order.getFoodId() "ok");
//库存减一
sql = "update num set num = num - 1 where box id = " order.getBoxId() "and num >= 1";
row = (Integer) executeSql(sql);
if (row != 1) {
throw new Exception("没柜子了");
}
record.add(order.getBoxId() "ok");
//判断该订单消费主卡还是副卡
addCardType(order);
//扣钱(第三方卡系统)
if (order.getCard() == "主") {
String res = feiginClient("主卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣主卡失败");
}
} else {
String res = feiginClient("副卡减一");
if (null == res || !"ok".equals(res)) {
throw new Exception("扣副卡失败");
}
}
record.add(order.getCard() "card-ok");
//下单(第三方下单系统)
String s = feiginClient(order.getFoodId());
if (null == s || !"ok".equals(s)) {
throw new Exception("下单失败");
}
}
} catch (Exception e) {
try {
for (Object o : record) {
rollback(o);
}
} catch (Exception e) {
System.out.println("ask for root help");
}
}
}
版本三:剥离第三方应用
使用事务,剥离远程调用,下面就不贴代码了,写一下逻辑
把远程调用的逻辑发到消息队列里或者事件表里,这样其实是最好的。
1)有现成的事务,却自己实现,自己很更厉害吗?
2)远程调用有一种情况是超时,但是调用成功了,比如说我调用A系统,A系统5秒后给我返回结果,但是Feign设置的超时时间是4秒,在A系统看来,我是成功调用的,但是在我来看,其实你是调用失败的,这种情况虽然是小概率事件,但是尽量追求极致还是没错的。
总结
1)有现成的事务我建议还是用现成的事务的
2)mysql乐观锁了解一下
3)远程调用耗时的可以单独剥离出来走消息队列或者事件表定时任务去扫描
4)其实在下单之前是要检查用户券的数量,也是远程调用,可以先调用一下看看系统还行不行,不行就直接throw别走后面的了