寫作背景
最近一學妹跳槽到北京某信,閑聊的時候,發現學妹在做餐廳的後端,女生做後端,很強。我說你個餐廳能做什麼???然後她秀爛了的我。下面進入正題。
需求背景
你可以在公司吃,也可以點外賣(送到固定的餐櫃裡)。午餐如果在食堂吃免費,扣主卡的免費次數;如果點外賣,如果你在食堂吃了(扣主卡的免費次數),那麼扣副卡的券,反之扣主卡的免費次數。晚餐隻扣副卡的券。
你可以一下定一星期的飯,比如現在周一,我可以定明天的午餐,後天的晚餐,大後天的午餐和晚餐。
下單需要調用2個第三方系統,外賣系統和卡系統。
我的想法:so easy
首先以上面為例:定明天的午餐,後天的晚餐,大後天的午餐和晚餐。
首先有一點要明确:我是下4單還是下1單,我說下4單啊,學妹說,如果我隻想退明天的午餐呢?是以下4單。
流程圖

僞代碼
@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循環中扣的券怎麼辦???我。。。
學妹,請開始你的表演
版本一:保證資料一緻性(當然,我這裡的事務失效了,大體上思路重要)
你要記錄你哪些成功了,然後在執行反向操作就行了。
@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) {
//加一減一
}
版本二:減少持有資料庫鎖的時間
核心就是:不使用事務,使用樂觀鎖,如下所示
update num set num = num - x where food id = " + order.getFoodId() + "and num >= x
因為遠端調用其實是比較耗時的,如果你一下鎖很多記錄,并發性就下來了。
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)遠端調用耗時的可以單獨剝離出來走消息隊列或者事件表定時任務去掃描