美文网首页
秒杀系统设计

秒杀系统设计

作者: 天地征途_觉醒 | 来源:发表于2017-10-09 19:26 被阅读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 "购买失败,库存不足";

        }

    在看看运行情况

    订单表:

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

    相关文章

      网友评论

          本文标题:秒杀系统设计

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