高并发下的下单功能设计

来源:互联网 发布:网络大电影行业分析 编辑:程序博客网 时间:2024/04/25 23:42

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

初始方案

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

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

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

@Entitypublic 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);}

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

@Servicepublic 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层的设计

@Controllerpublic 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 "购买失败,库存不足";    }}

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

@Controllerpublic 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查询了一次数据库有没有库存,但是比较恶劣的情况是很多人都查到了有库存,这个时候因为程序处理的延迟,没有及时的减少库存,那就出现了脏读。如何在设计上避免呢?最笨的方法是对SecKillControllerseckill方法做同步,每次只有一个人能下单。但是太影响性能了,下单变成了同步操作。

 @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 "购买失败,库存不足";    }

在看看运行情况
这里写图片描述
订单表:
这里写图片描述
在高并发问题下的秒杀情况,即使存在网络延时,也得到了保障。

0 0
原创粉丝点击