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

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

初始方案

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

<code>@Entity
public class SecKillGoods implements Serializable{
@Id
private String id;

/**
* 剩餘庫存
*/
private Integer remainNum;

/**
* 秒殺商品名稱
*/
private String goodsName;
}/<code>

秒殺訂單表設計:記錄秒殺成功的訂單情況

<code>@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;
}/<code>

Dao設計:主要就是一個減少庫存方法,其他CRUD使用JPA自帶的方法

<code>public interface SecKillGoodsDao extends JpaRepository<seckillgoods>{

@Query("update SecKillGoods g set g.remainNum = g.remainNum - ?2 where g.id=?1")
@Modifying(clearAutomatically = true)
@Transactional
int reduceStock(String id,Integer remainNum);

}/<seckillgoods>/<code>

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

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

下面就是controller層的設計

<code>@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 "購買失敗,庫存不足";
}

}/<code>

上面是全部的基礎準備,下面使用一個單元測試方法,模擬高併發下,很多人來購買同一個熱門商品的情況。

<code>@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";
}

}/<code>

訪問localhost:8080/simulationCocurrentTakeOrder,就可以測試了預期情況:因為我們只對秒殺商品(123456)初始化了10件,理想情況當然是庫存減少到0,訂單表也只有10條記錄。

實際情況:訂單表記錄

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

商品表記錄

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

下面分析一下為啥會出現超庫存的情況:因為多個請求訪問,僅僅是使用dao查詢了一次數據庫有沒有庫存,但是比較惡劣的情況是很多人都查到了有庫存,這個時候因為程序處理的延遲,沒有及時的減少庫存,那就出現了髒讀。如何在設計上避免呢?最笨的方法是對SecKillController的seckill方法做同步,每次只有一個人能下單。但是太影響性能了,下單變成了同步操作。

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

改進方案

根據多線程編程的規範,提倡對共享資源加鎖,在最有可能出現併發爭搶的情況下加同步塊的思想。應該同一時刻只有一個線程去減少庫存。但是這裡給出一個最好的方案,就是利用Oracle,Mysql的行級鎖–同一時間只有一個線程能夠操作同一行記錄,對SecKillGoodsDao進行改造:

<code>public interface SecKillGoodsDao extends JpaRepository<seckillgoods>{

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

}/<seckillgoods>/<code>

僅僅是加了一個and,卻造成了很大的改變,返回int值代表的是影響的行數,對應到controller做出相應的判斷。

<code>@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 "購買失敗,庫存不足";
}/<code>

在看看運行情況

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

訂單表:

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

在高併發問題下的秒殺情況,即使存在網絡延時,也得到了保障。

共同進步,學習分享

歡迎大家關注我的公眾號【風平浪靜如碼】,海量Java相關文章,學習資料都會在裡面更新,整理的資料也會放在裡面。

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

海量面試、架構資料分享

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


分享到:


相關文章: