场景分析:订餐下单流程分析

简介: 写作背景 最近一学妹跳槽到北京某信,闲聊的时候,发现学妹在做餐厅的后端,女生做后端,很强。我说你个餐厅能做什么???然后她秀烂了的我。下面进入正题。

需求背景


       你可以在公司吃,也可以点外卖(送到固定的餐柜里)。午餐如果在食堂吃免费,扣主卡的免费次数;如果点外卖,如果你在食堂吃了(扣主卡的免费次数),那么扣副卡的券,反之扣主卡的免费次数。晚餐只扣副卡的券。


    你可以一下定一星期的饭,比如现在周一,我可以定明天的午餐,后天的晚餐,大后天的午餐和晚餐。


     下单需要调用2个第三方系统,外卖系统和卡系统。


我的想法:so easy


首先以上面为例:定明天的午餐,后天的晚餐,大后天的午餐和晚餐。


首先有一点要明确:我是下4单还是下1单,我说下4单啊,学妹说,如果我只想退明天的午餐呢?所以下4单。


流程图


13.png


伪代码


@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)远程调用耗时的可以单独剥离出来走消息队列或者事件表定时任务去扫描

4)其实在下单之前是要检查用户券的数量,也是远程调用,可以先调用一下看看系统还行不行,不行就直接throw别走后面的了

5)事务失效的问题要格外关注

目录
相关文章
|
4月前
|
存储 运维 搜索推荐
重构支付宝商家账单问题之重构支付宝商家账单的目标是什么,如何实现
重构支付宝商家账单问题之重构支付宝商家账单的目标是什么,如何实现
|
5月前
|
供应链 监控 调度
ERP系统中的销售订单管理与订单跟踪解析
【7月更文挑战第25天】 ERP系统中的销售订单管理与订单跟踪解析
478 2
|
4月前
|
XML JSON 监控
淘宝商品数据接口实战:自动化监控与竞品分析
淘宝开放平台提供的商品列表数据接口是一种API,使开发者能编程获取淘宝商品数据。主要功能包括按关键词、分类等获取商品列表及其详情,并支持分页、排序及多维度筛选。常见参数有关键词、页码、排序方式等。使用需注册账号获取API密钥,构建并发送HTTP请求,解析JSON/XML响应数据进行业务处理。此接口适用于商品监控、市场分析等多种场景。[体验API](http://u6v.cn/5W41Dx)
|
5月前
|
前端开发
会员系统02--,后台管理系统,包含网站运营,统计分析,用户中心,财务管理,资金明细,系统管理,参数配置,后台管理系统可以观看配置资料,广告位的相关资料,客服工单最主要是客户反馈给我们的问题,登录统计
会员系统02--,后台管理系统,包含网站运营,统计分析,用户中心,财务管理,资金明细,系统管理,参数配置,后台管理系统可以观看配置资料,广告位的相关资料,客服工单最主要是客户反馈给我们的问题,登录统计
|
7月前
|
消息中间件 供应链 NoSQL
电商订单待支付(思路分析)
电商订单待支付(思路分析)
|
7月前
|
供应链 数据挖掘 API
淘宝API接口系列:数据分析丨Erp上货丨维权控价丨商品搬家丨店铺订单管理
淘宝API接口系列在多个方面为电商业务提供了强大的支持,包括数据分析、ERP上货、维权控价、商品搬家以及店铺订单管理。下面将针对这些方面逐一进行说明。
|
7月前
|
数据采集 API
快手商品数据采集神器,助你轻松获取商品详情数据
快手商品数据采集神器,助你轻松获取商品详情数据
|
XML JSON 分布式计算
如何设计财务对账系统 —— 从0到1搭建对账中心实战
卡拉云快速搭建企业内部对账系统
7912 3
如何设计财务对账系统 —— 从0到1搭建对账中心实战
|
算法 安全 数据安全/隐私保护
【在线支付】在线支付流程分析
【在线支付】在线支付流程分析
331 0
|
存储 消息中间件 JavaScript
支付设计白皮书:支付系统的对账系统设计
支付设计白皮书:支付系统的对账系统设计