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

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

需求背景


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


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


     下单需要调用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)事务失效的问题要格外关注

目录
相关文章
|
28天前
|
缓存 NoSQL Java
京东电商下单黄金链路:防止订单重复提交与支付的深度解析
【10月更文挑战第21天】在电商领域,尤其是在像京东这样的大型电商平台中,防止订单重复提交与支付是一项至关重要的任务。
92 44
|
6月前
|
XML JSON 安全
借助API接口实现自营商城上货采集,无货源模式采集商品
在无货源模式的自营商城中,通过API接口实现商品采集是一个高效且灵活的方式。这种方式允许商家直接从供应商或其他电商平台的API接口中获取商品信息,然后将这些信息导入到自己的商城中,无需自己拥有实际的库存。
|
3月前
|
存储 运维 搜索推荐
重构支付宝商家账单问题之重构支付宝商家账单的目标是什么,如何实现
重构支付宝商家账单问题之重构支付宝商家账单的目标是什么,如何实现
|
4月前
|
前端开发
会员系统02--,后台管理系统,包含网站运营,统计分析,用户中心,财务管理,资金明细,系统管理,参数配置,后台管理系统可以观看配置资料,广告位的相关资料,客服工单最主要是客户反馈给我们的问题,登录统计
会员系统02--,后台管理系统,包含网站运营,统计分析,用户中心,财务管理,资金明细,系统管理,参数配置,后台管理系统可以观看配置资料,广告位的相关资料,客服工单最主要是客户反馈给我们的问题,登录统计
|
6月前
|
消息中间件 供应链 NoSQL
电商订单待支付(思路分析)
电商订单待支付(思路分析)
|
6月前
|
供应链 数据挖掘 API
淘宝API接口系列:数据分析丨Erp上货丨维权控价丨商品搬家丨店铺订单管理
淘宝API接口系列在多个方面为电商业务提供了强大的支持,包括数据分析、ERP上货、维权控价、商品搬家以及店铺订单管理。下面将针对这些方面逐一进行说明。
|
6月前
|
新零售 人工智能 供应链
排队免单返利商城系统开发|成熟源码部署|案例详情
新零售业是零售业发展的重要趋势,它通过技术的创新和变革,重新定义了传统零售业的模式和方式
|
算法 安全 数据安全/隐私保护
【在线支付】在线支付流程分析
【在线支付】在线支付流程分析
322 0
云his门诊业务模块常见问题分析和门诊业务使用流程
云HIS:门诊业务使用流程 患者可以通过网上预约,现场挂号,获取门诊就诊号,排队就诊。 对于基层医院,不收取挂号费,地区慢性病,复诊较多时,可以直接通过历史患者,”复诊“功能,直接挂号。 门诊医生开具处方单,检验检查申请单,发送成功后,收费室会显示待收费的患者以及处方明细。收费后,药房会显示患者以及处方明细,药房人员依据处方发药。患者领取药物,如果存在注射药物,那么治疗室会有患者的注射用药明细。
192 0
|
存储 安全 区块链
深入分析字画拍卖竞拍系统开发详情及功能
 什么是所谓的数字藏品,其实就是指使用区块链技术,将一些作品、艺术品生成相对应的数字凭证,不仅可以保护版权,还能实现数字化发行、购买、收藏和使用,具有可追溯、难以篡改、唯一性等特点。
深入分析字画拍卖竞拍系统开发详情及功能