功能需求:設計一個秒殺系統(tǒng)
初始方案
商品表設計:熱銷商品提供給用戶秒殺,有初始庫存。
@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{
@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 '購買失敗,庫存不足'; }
}
上面是全部的基礎準備,下面使用一個單元測試方法,模擬高并發(fā)下,很多人來購買同一個熱門商品的情況。
@Controller public class SecKillSimulationOpController {
final String takeOrderUrl = 'http://127.0.0.1:8080/seckill.html';
/** * 模擬并發(fā)下單 */ @RequestMapping('/simulationCocurrentTakeOrder') @ResponseBody public String simulationCocurrentTakeOrder() { //httpClient工廠 final SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory(); //開50個線程模擬并發(fā)秒殺下單 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
改進方案
根據多線程編程的規(guī)范,提倡對共享資源加鎖,在最有可能出現并發(fā)爭搶的情況下加同步塊的思想。應該同一時刻只有一個線程去減少庫存。但是這里給出一個最好的方案,就是利用Oracle,Mysql的行級鎖–同一時間只有一個線程能夠操作同一行記錄,對SecKillGoodsDao進行改造:
public interface SecKillGoodsDao extends JpaRepository{
@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 '購買失敗,庫存不足'; }
在看看運行情況
訂單表:
在高并發(fā)問題下的秒殺情況,即使存在網絡延時,也得到了保障。
出處:http://blog.csdn.net/u013815546/article/details/53928912
|