美文网首页互联网科技
面试实战考核:设计一个高并发下的下单功能

面试实战考核:设计一个高并发下的下单功能

作者: 风平浪静如码 | 来源:发表于2020-03-31 21:56 被阅读0次

    功能需求:设计一个秒杀系统

    初始方案

    商品表设计:热销商品提供给用户秒杀,有初始库存。

    @Entity
    public class SecKillGoods implements Serializable{
        @Id
        private String id;
    
        /**
         * 剩余库存
         */
        private Integer remainNum;
    
        /**
         * 秒杀商品名称
         */
        private String goodsName;
    }
    

    秒杀订单表设计:记录秒杀成功的订单情况

    @Entity
    public class SecKillOrder implements Serializable {
        @Id
        @GenericGenerator(name = "PKUUID", strategy = "uuid2")
        @GeneratedValue(generator = "PKUUID")
        @Column(length = 36)
        private String id;
    
        //用户名称
        private String consumer;
    
        //秒杀产品编号
        private String goodsId;
    
        //购买数量
        private Integer num;
    }
    

    Dao设计:主要就是一个减少库存方法,其他CRUD使用JPA自带的方法

    public interface SecKillGoodsDao extends JpaRepository<SecKillGoods,String>{
    
        @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1")
        @Modifying(clearAutomatically = true)
        @Transactional
        int reduceStock(String id,Integer remainNum);
    
    }
    

    数据初始化以及提供保存订单的操作:

    @Service
    public class SecKillService {
    
        @Autowired
        SecKillGoodsDao secKillGoodsDao;
    
        @Autowired
        SecKillOrderDao secKillOrderDao;
    
        /**
         * 程序启动时:
         * 初始化秒杀商品,清空订单数据
         */
        @PostConstruct
        public void initSecKillEntity(){
            secKillGoodsDao.deleteAll();
            secKillOrderDao.deleteAll();
            SecKillGoods secKillGoods = new SecKillGoods();
            secKillGoods.setId("123456");
            secKillGoods.setGoodsName("秒杀产品");
            secKillGoods.setRemainNum(10);
            secKillGoodsDao.save(secKillGoods);
        }
    
        /**
         * 购买成功,保存订单
         * @param consumer
         * @param goodsId
         * @param num
         */
        public void generateOrder(String consumer, String goodsId, Integer num) {
            secKillOrderDao.save(new SecKillOrder(consumer,goodsId,num));
        }
    }
    

    下面就是controller层的设计

    @Controller
    public class SecKillController {
    
        @Autowired
        SecKillGoodsDao secKillGoodsDao;
        @Autowired
        SecKillService secKillService;
    
        /**
         * 普通写法
         * @param consumer
         * @param goodsId
         * @return
         */
        @RequestMapping("/seckill.html")
        @ResponseBody
        public String SecKill(String consumer,String goodsId,Integer num) throws InterruptedException {
            //查找出用户要买的商品
            SecKillGoods goods = secKillGoodsDao.findOne(goodsId);
            //如果有这么多库存
            if(goods.getRemainNum()>=num){
                //模拟网络延时
                Thread.sleep(1000);
                //先减去库存
                secKillGoodsDao.reduceStock(num);
                //保存订单
                secKillService.generateOrder(consumer,goodsId,num);
                return "购买成功";
            }
            return "购买失败,库存不足";
        }
    
    }
    

    上面是全部的基础准备,下面使用一个单元测试方法,模拟高并发下,很多人来购买同一个热门商品的情况。

    @Controller
    public class SecKillSimulationOpController {
    
        final String takeOrderUrl = "http://127.0.0.1:8080/seckill.html";
    
        /**
         * 模拟并发下单
         */
        @RequestMapping("/simulationCocurrentTakeOrder")
        @ResponseBody
        public String simulationCocurrentTakeOrder() {
            //httpClient工厂
            final SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
            //开50个线程模拟并发秒杀下单
            for (int i = 0; i < 50; i++) {
                //购买人姓名
                final String consumerName = "consumer" + i;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        ClientHttpRequest request = null;
                        try {
                            URI uri = new URI(takeOrderUrl + "?consumer=consumer" + consumerName + "&goodsId=123456&num=1");
                            request = httpRequestFactory.createRequest(uri, HttpMethod.POST);
                            InputStream body = request.execute().getBody();
                            BufferedReader br = new BufferedReader(new InputStreamReader(body));
                            String line = "";
                            String result = "";
                            while ((line = br.readLine()) != null) {
                                result += line;//获得页面内容或返回内容
                            }
                            System.out.println(consumerName+":"+result);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
            return "simulationCocurrentTakeOrder";
        }
    
    }
    

    访问localhost:8080/simulationCocurrentTakeOrder,就可以测试了
    预期情况:因为我们只对秒杀商品(123456)初始化了10件,理想情况当然是库存减少到0,订单表也只有10条记录。

    实际情况:订单表记录

    商品表记录

    下面分析一下为啥会出现超库存的情况:
    因为多个请求访问,仅仅是使用dao查询了一次数据库有没有库存,但是比较恶劣的情况是很多人都查到了有库存,这个时候因为程序处理的延迟,没有及时的减少库存,那就出现了脏读。如何在设计上避免呢?最笨的方法是对SecKillController的seckill方法做同步,每次只有一个人能下单。但是太影响性能了,下单变成了同步操作。

     @RequestMapping("/seckill.html")
     @ResponseBody
     public synchronized String SecKill
    

    改进方案

    根据多线程编程的规范,提倡对共享资源加锁,在最有可能出现并发争抢的情况下加同步块的思想。应该同一时刻只有一个线程去减少库存。但是这里给出一个最好的方案,就是利用Oracle,Mysql的行级锁–同一时间只有一个线程能够操作同一行记录,对SecKillGoodsDao进行改造:

    public interface SecKillGoodsDao extends JpaRepository<SecKillGoods,String>{
    
        @Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1 and g.remainNum>0")
        @Modifying(clearAutomatically = true)
        @Transactional
        int reduceStock(String id,Integer remainNum);
    
    }
    

    仅仅是加了一个and,却造成了很大的改变,返回int值代表的是影响的行数,对应到controller做出相应的判断。

    @RequestMapping("/seckill.html")
        @ResponseBody
        public String SecKill(String consumer,String goodsId,Integer num) throws InterruptedException {
            //查找出用户要买的商品
            SecKillGoods goods = secKillGoodsDao.findOne(goodsId);
            //如果有这么多库存
            if(goods.getRemainNum()>=num){
                //模拟网络延时
                Thread.sleep(1000);
                if(goods.getRemainNum()>0) {
                    //先减去库存
                    int i = secKillGoodsDao.reduceStock(goodsId, num);
                    if(i!=0) {
                        //保存订单
                        secKillService.generateOrder(consumer, goodsId, num);
                        return "购买成功";
                    }else{
                        return "购买失败,库存不足";
                    }
                }else {
                    return "购买失败,库存不足";
                }
            }
            return "购买失败,库存不足";
        }
    

    在看看运行情况

    订单表:

    在高并发问题下的秒杀情况,即使存在网络延时,也得到了保障。

    共同进步,学习分享

    欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

    觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

    海量面试、架构资料分享

    相关文章

      网友评论

        本文标题:面试实战考核:设计一个高并发下的下单功能

        本文链接:https://www.haomeiwen.com/subject/wdtnuhtx.html