面試實戰考覈:設計一個高併發下的下單功能

功能需求:設計一個秒殺系統

初始方案html

商品表設計:熱銷商品提供給用戶秒殺,有初始庫存。sql

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

}
複製代碼

數據初始化以及提供保存訂單的操做:bash

@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條記錄。併發

實際狀況:訂單表記錄app

商品表記錄ide

下面分析一下爲啥會出現超庫存的狀況: 由於多個請求訪問,僅僅是使用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相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。

以爲寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

相關文章
相關標籤/搜索