需求分析
所謂“秒殺”,就是網絡賣家發佈一些超低價格的商品,所有買家在同一時間網上搶購的一種銷售方式。
秒殺商品通常有兩種限制:庫存限制、時間限制。
(1)商品詳細頁顯示秒殺商品信息,點擊立即搶購實現秒殺下單,下單時扣減庫存。當庫存爲0或不在活動期範圍內時無法秒殺。
(2)秒殺下單成功,直接跳轉到支付頁面(支付寶掃碼),支付成功,跳轉到成功頁,填寫收貨地址、電話、收件人等信息,完成訂單。
(3)當用戶秒殺下單5分鐘內未支付,取消預訂單,調用支付寶支付的關閉訂單接口,恢復庫存。
秒殺技術實現核心思想是運用緩存減少數據庫瞬間的訪問壓力!讀取商品詳細信息時運用緩存,當用戶點擊搶購時減少緩存中的庫存數量,當庫存數爲0時或活動期結束時,同步到數據庫。產生的秒殺預訂單也不會立刻寫到數據庫中,而是先寫到緩存,當用戶付款成功後再寫入數據庫。
實現
秒殺商品的查詢
- 判斷redis是否爲空,如果不爲空,就從redis中查詢
- 如果爲空就從數據庫中查詢並將查詢結果存儲到redis中
public List<TbSeckillGoods> findList() {
List<TbSeckillGoods> secKillGoods = redisTemplate.boundHashOps("secKillGoods").values();
if (secKillGoods==null || secKillGoods.size()==0){
TbSeckillGoodsExample example = new TbSeckillGoodsExample();
TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo("1");
//當前時間大於開始時間,小於結束時間
criteria.andStartTimeLessThan(new Date());
criteria.andEndTimeGreaterThan(new Date());
//庫存限制
criteria.andStockCountGreaterThan(0);
secKillGoods = seckillMapper.selectByExample(example);
//存入redis中
for (TbSeckillGoods secKillGood : secKillGoods) {
redisTemplate.boundHashOps("secKillGoods").put(secKillGood.getId(),secKillGood);
}
}
return secKillGoods;
}
秒殺訂單的創建
- 訂單在redis中存儲的大鍵名任意,每個小鍵爲用戶id,值爲訂單的list集合
- 同一個商品一個用戶只能秒殺一件
- 判斷秒殺商品已經從秒殺完從redis中刪除和秒殺商品數量小於等於0的情況(在高併發情況下,庫存數量是有可能爲負的)
- 扣減庫存並更新到redis中
- 當庫存扣減爲0的時候,從緩存中移除 該秒殺商品, 同時將數據同步到 mysql中
- 將秒殺訂單保存在redis中
public Long submitOrder(Long seckillId, String userId) {
//從redis中查詢出用戶的秒殺訂單
List<TbSeckillOrder> OrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
if (OrderList==null){
OrderList = new ArrayList<>();
}
else {
for (TbSeckillOrder secKillorder : OrderList) {
if (secKillorder.getSeckillId().longValue() == seckillId.longValue()){//一個商品一個用戶只能秒殺一次
throw new RuntimeException("你已經成功秒殺到了該商品!");
}
}
}
//從redis中獲取當前秒殺商品的數據,判斷該商品是否還能購買
TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(seckillId);
//判斷秒殺商品已經從秒殺完從redis中刪除和秒殺商品數量小於等於0的情況(在高併發情況下,庫存數量是有可能爲負的)
if (seckillGood==null || seckillGood.getStockCount()<=0){
throw new RuntimeException("很遺憾,該商品已經搶完,謝謝參與");
}
//扣減庫存
seckillGood.setStockCount(seckillGood.getStockCount()-1);
redisTemplate.boundHashOps("secKillGoods").put(seckillId,seckillGood);
// 當庫存扣減爲0的時候,從緩存中移除 該秒殺商品, 同時將數據同步到 mysql中
if (seckillGood.getStockCount()==0){
redisTemplate.boundHashOps("secKillGoods").delete(seckillId);
seckillGoodsMapper.updateByPrimaryKey(seckillGood);
}
//將秒殺訂單保存在redis中
TbSeckillOrder seckillOrder = new TbSeckillOrder();
long orderId = idWorker.nextId();
seckillOrder.setId(orderId);
seckillOrder.setSeckillId(seckillId);
seckillOrder.setMoney(seckillGood.getCostPrice());
seckillOrder.setUserId(userId);
seckillOrder.setSellerId(seckillGood.getSellerId());
seckillOrder.setCreateTime(new Date());
// 未付款
seckillOrder.setStatus("0");
//將訂單加入用戶的秒殺訂單表
OrderList.add(seckillOrder);
redisTemplate.boundHashOps("secKillOrders").put(userId,OrderList);
//返回訂單id
return orderId;
}
高併發壓力測試
測試之前秒殺商品庫存數量是10
在spring-security.xml文件中對測試url進行放行
使用jmeter進行測試,併發測試500
併發測試之後,庫存清爲0
在redis中查看創建訂單的數量爲17,發生了超賣現象
redis分佈式鎖解決超賣
我們通過單節點Redis實現一個分佈式鎖。
利用redis在同一時刻操作一個鍵的值只能有一個進程的特性,如果能設值成功就獲取到鎖;解鎖,就是刪除指定的鍵;
爲防止死鎖可以設置鎖超時時間,如果鎖超時就釋放鎖。
秒殺訂單支付及保存
支付只需要我們從redis中將訂單號和支付金額查詢出,然後調用支付方法即可
頁面監聽訂單是否支付
如果已支付,將訂單保存到mysql數據庫以及更新redis數據庫的信息
將方法寫在業務層還有個好處是,業務層切入了事務,如果沒有執行成功,可以進行回滾
在支付控制層的支付成功判斷裏調用更新方法
public void updateSecKillOrder(String userId, String out_trade_no) {
TbSeckillOrder saveOrder = null;
List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(userId);
for (TbSeckillOrder seckillOrder : secOrderList) {
if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){
seckillOrder.setPayTime(new Date());
seckillOrder.setStatus("1");
//同步到mysql
saveOrder = seckillOrder;
}
}
redisTemplate.boundHashOps("secKillOrders").put(userId,secOrderList);
seckillOrderMapper.insert(saveOrder);
}
秒殺付款超時
public void backAndRemoveOrder(String name, String out_trade_no) {
//還原庫存
//先查詢出超時未付款訂單
TbSeckillOrder order = findSecKillOrderByUserIdAndOrderId(name, Long.valueOf(out_trade_no));
TbSeckillGoods seckillGood = (TbSeckillGoods) redisTemplate.boundHashOps("secKillGoods").get(order.getSeckillId());
if (seckillGood==null){
TbSeckillGoods dbGoods = seckillGoodsMapper.selectByPrimaryKey(order.getSeckillId());
// dbGoods可不可用就不一定了:可能在用戶3分鐘未付款期間,該商品到了秒殺截止時間,
// 此時就不用再添加到 redis緩存了, 但是 數據庫 的庫存 應該 +1
//過期
if (dbGoods.getEndTime().getTime()<new Date().getTime()){
dbGoods.setStockCount(dbGoods.getStockCount()+1);
seckillGoodsMapper.updateByPrimaryKey(dbGoods);
}else {
dbGoods.setStockCount(dbGoods.getStockCount()+1);
seckillGoodsMapper.updateByPrimaryKey(dbGoods);
//更新redis中的數據
redisTemplate.boundHashOps("secKillGoods").put(dbGoods.getId(),dbGoods);
}
}else {
seckillGood.setStockCount(seckillGood.getStockCount()+1);
//更新redis中的數據
redisTemplate.boundHashOps("secKillGoods").put(seckillGood.getId(),seckillGood);
}
//移除未付款訂單
List<TbSeckillOrder> newList = new ArrayList<>();
List<TbSeckillOrder> secOrderList = (List<TbSeckillOrder>) redisTemplate.boundHashOps("secKillOrders").get(name);
for (TbSeckillOrder seckillOrder : secOrderList) {
if (out_trade_no.equals(seckillOrder.getId().longValue()+"")){
}else{
newList.add(seckillOrder);
}
}
redisTemplate.boundHashOps("secKillOrders").put(name,newList);
}
注意
list刪除複雜對象使用remove沒有效果,基本類型的數據可以刪除