功能需求:設(shè)計(jì)一個(gè)秒殺系統(tǒng)
初始方案
商品表設(shè)計(jì):熱銷商品提供給用戶秒殺,有初始庫(kù)存。
@Entity
public class SecKillGoods implements Serializable{
@Id
private String id;
/**
* 剩余庫(kù)存
*/
private Integer remainNum;
/**
* 秒殺商品名稱
*/
private String goodsName;
}
秒殺訂單表設(shè)計(jì):記錄秒殺成功的訂單情況
@Entity
public class SecKillOrder implements Serializable {
@Id
@GenericGenerator(name = "PKUUID", strategy = "uuid2")
@GeneratedValue(generator = "PKUUID")
@Column(length = 36)
private String id;
//用戶名稱
private String consumer;
//秒殺產(chǎn)品編號(hào)
private String goodsId;
//購(gòu)買數(shù)量
private Integer num;
}
Dao設(shè)計(jì):主要就是一個(gè)減少庫(kù)存方法,其他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);
}
數(shù)據(jù)初始化以及提供保存訂單的操作:
@Service
public class SecKillService {
@Autowired
SecKillGoodsDao secKillGoodsDao;
@Autowired
SecKillOrderDao secKillOrderDao;
/**
* 程序啟動(dòng)時(shí):
* 初始化秒殺商品侥加,清空訂單數(shù)據(jù)
*/
@PostConstruct
public void initSecKillEntity(){
secKillGoodsDao.deleteAll();
secKillOrderDao.deleteAll();
SecKillGoods secKillGoods = new SecKillGoods();
secKillGoods.setId("123456");
secKillGoods.setGoodsName("秒殺產(chǎn)品");
secKillGoods.setRemainNum(10);
secKillGoodsDao.save(secKillGoods);
}
/**
* 購(gòu)買成功,保存訂單
* @param consumer
* @param goodsId
* @param num
*/
public void generateOrder(String consumer, String goodsId, Integer num) {
secKillOrderDao.save(new SecKillOrder(consumer,goodsId,num));
}
}
下面就是controller層的設(shè)計(jì)
@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);
//如果有這么多庫(kù)存
if(goods.getRemainNum()>=num){
//模擬網(wǎng)絡(luò)延時(shí)
Thread.sleep(1000);
//先減去庫(kù)存
secKillGoodsDao.reduceStock(num);
//保存訂單
secKillService.generateOrder(consumer,goodsId,num);
return "購(gòu)買成功";
}
return "購(gòu)買失敗,庫(kù)存不足";
}
}
上面是全部的基礎(chǔ)準(zhǔn)備潜必,下面使用一個(gè)單元測(cè)試方法每币,模擬高并發(fā)下先壕,很多人來(lái)購(gòu)買同一個(gè)熱門商品的情況翔脱。
@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個(gè)線程模擬并發(fā)秒殺下單
for (int i = 0; i < 50; i++) {
//購(gòu)買人姓名
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;//獲得頁(yè)面內(nèi)容或返回內(nèi)容
}
System.out.println(consumerName+":"+result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
return "simulationCocurrentTakeOrder";
}
}
訪問localhost:8080/simulationCocurrentTakeOrder奴拦,就可以測(cè)試了
預(yù)期情況:因?yàn)槲覀冎粚?duì)秒殺商品(123456)初始化了10件,理想情況當(dāng)然是庫(kù)存減少到0届吁,訂單表也只有10條記錄粱坤。
實(shí)際情況:訂單表記錄
商品表記錄
下面分析一下為啥會(huì)出現(xiàn)超庫(kù)存的情況:
因?yàn)槎鄠€(gè)請(qǐng)求訪問,僅僅是使用dao查詢了一次數(shù)據(jù)庫(kù)有沒有庫(kù)存瓷产,但是比較惡劣的情況是很多人都查到了有庫(kù)存站玄,這個(gè)時(shí)候因?yàn)槌绦蛱幚淼难舆t,沒有及時(shí)的減少庫(kù)存濒旦,那就出現(xiàn)了臟讀株旷。如何在設(shè)計(jì)上避免呢?最笨的方法是對(duì)SecKillController的seckill方法做同步尔邓,每次只有一個(gè)人能下單晾剖。但是太影響性能了,下單變成了同步操作梯嗽。
@RequestMapping("/seckill.html")
@ResponseBody
public synchronized String SecKill
改進(jìn)方案
根據(jù)多線程編程的規(guī)范齿尽,提倡對(duì)共享資源加鎖,在最有可能出現(xiàn)并發(fā)爭(zhēng)搶的情況下加同步塊的思想灯节。應(yīng)該同一時(shí)刻只有一個(gè)線程去減少庫(kù)存循头。但是這里給出一個(gè)最好的方案,就是利用Oracle,MySQL的行級(jí)鎖–同一時(shí)間只有一個(gè)線程能夠操作同一行記錄炎疆,對(duì)SecKillGoodsDao進(jìn)行改造:
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);
}
僅僅是加了一個(gè)and卡骂,卻造成了很大的改變,返回int值代表的是影響的行數(shù)形入,對(duì)應(yīng)到controller做出相應(yīng)的判斷全跨。
@RequestMapping("/seckill.html")
@ResponseBody
public String SecKill(String consumer,String goodsId,Integer num) throws InterruptedException {
//查找出用戶要買的商品
SecKillGoods goods = secKillGoodsDao.findOne(goodsId);
//如果有這么多庫(kù)存
if(goods.getRemainNum()>=num){
//模擬網(wǎng)絡(luò)延時(shí)
Thread.sleep(1000);
if(goods.getRemainNum()>0) {
//先減去庫(kù)存
int i = secKillGoodsDao.reduceStock(goodsId, num);
if(i!=0) {
//保存訂單
secKillService.generateOrder(consumer, goodsId, num);
return "購(gòu)買成功";
}else{
return "購(gòu)買失敗,庫(kù)存不足";
}
}else {
return "購(gòu)買失敗,庫(kù)存不足";
}
}
return "購(gòu)買失敗,庫(kù)存不足";
}
在看看運(yùn)行情況
訂單表:
在高并發(fā)問題下的秒殺情況,即使存在網(wǎng)絡(luò)延時(shí)亿遂,也得到了保障浓若。