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

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

初始方案

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

@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://blog.51cto.com/14570694/2483731

时间: 2024-10-14 12:41:48

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

Java面试常问题:如何设计一个高并发系统?你该如何优雅的回答

面试原题 如何设计一个高并发系统? 面试官心理分析 说实话,如果面试官问你这个题目,那么你必须要使出全身吃奶劲了.为啥?因为你没看到现在很多公司招聘的 JD 里都是说啥,有高并发就经验者优先. 如果你确实有真才实学,在互联网公司里干过高并发系统,那你确实拿 offer 基本如探囊取物,没啥问题.面试官也绝对不会这样来问你,否则他就是蠢. 假设你在某知名电商公司干过高并发系统,用户上亿,一天流量几十亿,高峰期并发量上万,甚至是十万.那么人家一定会仔细盘问你的系统架构,你们系统啥架构?怎么部署的?部

记一个python+sqlalchemy+tornado的一个高并发下,产生重复记录的bug

场景:在用户通过支付通道支付完成返回时,发现我收到的处理数据记录中有两条同样的数据记录, 也就是同一笔钱,我数据库中记为了两条一样的记录. tornado端代码 from tornado import gen from tornado.concurrent import run_on_executor class processNetPay(BaseHandler): '''处理指定订单,指定支付请求,返回处理结果 ' 返回包含订单信息与用户信息体 ''' @tornado.web.asynch

如何设计一个高并发系统?

其实所谓的高并发,如果你要理解这个问题呢,其实就得从高并发的根源出发,为啥会有高并发?为啥高并发就很牛逼? 我说的浅显一点,很简单,就是因为刚开始系统都是连接数据库的,但是要知道数据库支撑到每秒并发两三千的时候,基本就快完了.所以才有说,很多公司,刚开始干的时候,技术比较 low,结果业务发展太快,有的时候系统扛不住压力就挂了. 当然会挂了,凭什么不挂?你数据库如果瞬间承载每秒 5000/8000,甚至上万的并发,一定会宕机,因为比如 mysql 就压根儿扛不住这么高的并发量. 所以为啥高并发牛

如何设计一个高并发系统

系统拆分,将一个系统拆分为多个子系统,用dubbo来搞.然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以抗高并发么. 缓存,必须得用缓存.大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了.毕竟人家redis轻轻松松单机几万的并发啊.没问题的.所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发. MQ,必须得用MQ.可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删

如何设计一个高可用系统?要考虑哪些地方?

本文已经收录自笔者开源的 JavaGuide: https://github.com/Snailclimb (69k+Star[Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识)如果觉得不错的还,不妨去点个Star,鼓励一下! 一篇短小的文章,面试经常遇到的这个问题.本文主要包括下面这些内容: 高可用的定义 哪些情况可能会导致系统不可用? 有些提高系统可用性的方法?只是简单的提一嘴,更具体内容在后续的文章中介绍,就拿限流来说,你需要搞懂:何为限流?如何限流?为什么要限流

设计一个高并发系统

升级过程为:最初系统——添加负载均衡——数据库分库分表+读写分离——缓存集群+消息中间件集群 1.最初系统 假设系统机器是4核8G,数据库服务器是16核32G.日活用户1W,系统层面每秒10次请求,数据库层每秒30次请求. 2.添加负载均衡 用户量增长了50倍,日活用户50万,高峰期对系统每秒请求500/s,对数据库的每秒请求1500/s 问题:系统CPU负载过高,数据库可以接受 3.数据库分库分表+读写分离 用户量继续增长,达到了1000万注册用户,每天日活用户是100万. 问题:系统层面可以

Netty实战:设计一个IM框架就这么简单!

bitchat 是一个基于 Netty 的 IM 即时通讯框架 项目地址:https://github.com/all4you/bitchat 快速开始 bitchat-example 模块提供了一个服务端与客户端的实现示例,可以参照该示例进行自己的业务实现. 启动服务端 要启动服务端,需要获取一个 Server 的实例,可以通过 ServerFactory 来获取. 目前只实现了单机模式下的 Server ,通过 SimpleServerFactory 只需要定义一个端口即可获取一个单机的 S

高并发超库存下单的一个数据库层面解决小技巧

问题描述:库存更新成负数 产生原因:由于多线程并发时每个下单线程判断是否超库存时,读到了数据库同样的值,都认为库存满足要求,都执行了下单扣库存的操作,结果就是库存被更新成了负数,实际下单量大于实际库存. 解决办法:1.可以通过java的sychronized关键字以及Lock API去加锁,这样实现比较重,并且跨jvm的情况需要考虑分布式锁. 2.在数据库压力不是特别大的情况下,可以有一个小技巧来快速解决问题:(TPS很大的情况需要考虑消息队列等方式) 在sql中加上条件,当前库存>=n(要扣除

java架构师课程、性能调优、高并发、tomcat负载均衡、大型电商项目实战、高可用、高可扩展、数据库架构设计、Solr集群与应用、分布式实战、主从复制、高可用集群、大数据

15套Java架构师详情 * { font-family: "Microsoft YaHei" !important } h1 { background-color: #006; color: #FF0 } 15套java架构师.集群.高可用.高可扩展.高性能.高并发.性能优化.Spring boot.Redis.ActiveMQ.Nginx.Mycat.Netty.Jvm大型分布式项目实战视频教程 视频课程包含: 高级Java架构师包含:Spring boot.Spring  clo