第14章 秒殺
學習目標
-
防止秒殺重複排隊
重複排隊:一個人搶購商品,如果沒有支付,不允許重複排隊搶購
-
併發超賣問題解決
1個商品賣給多個人:1商品多訂單
-
秒殺訂單支付
秒殺支付:支付流程需要調整
-
超時支付訂單庫存回滾
1.RabbitMQ延時隊列 2.利用延時隊列實現支付訂單的監聽,根據訂單支付狀況進行訂單數據庫回滾
1 防止秒殺重複排隊
用戶每次搶單的時候,一旦排隊,我們設置一個自增值,讓該值的初始值爲1,每次進入搶單的時候,對它進行遞增,如果值>1,則表明已經排隊,不允許重複排隊,如果重複排隊,則對外拋出異常,並拋出異常信息100表示已經正在排隊。
1.1 後臺排隊記錄
修改SeckillOrderServiceImpl的add方法,新增遞增值判斷是否排隊中,代碼如下:
上圖代碼如下:
//遞增,判斷是否排隊
Long userQueueCount = redisTemplate.boundHashOps("UserQueueCount").increment(username, 1);
if(userQueueCount>1){
//100:表示有重複搶單
throw new RuntimeException(String.valueOf(StatusCode.REPERROR));
}
2 併發超賣問題解決
超賣問題,這裏是指多人搶購同一商品的時候,多人同時判斷是否有庫存,如果只剩一個,則都會判斷有庫存,此時會導致超賣現象產生,也就是一個商品下了多個訂單的現象。
2.1 思路分析
解決超賣問題,可以利用Redis隊列實現,給每件商品創建一個獨立的商品個數隊列,例如:A商品有2個,A商品的ID爲1001,則可以創建一個隊列,key=SeckillGoodsCountList_1001,往該隊列中塞2次該商品ID。
每次給用戶下單的時候,先從隊列中取數據,如果能取到數據,則表明有庫存,如果取不到,則表明沒有庫存,這樣就可以防止超賣問題產生了。
在我們隊Redis進行操作的時候,很多時候,都是先將數據查詢出來,在內存中修改,然後存入到Redis,在併發場景,會出現數據錯亂問題,爲了控制數量準確,我們單獨將商品數量整一個自增鍵,自增鍵是線程安全的,所以不擔心併發場景的問題。
2.2 代碼實現
每次將商品壓入Redis緩存的時候,另外多創建一個商品的隊列。
修改SeckillGoodsPushTask,添加一個pushIds方法,用於將指定商品ID放入到指定的數字中,代碼如下:
/***
* 將商品ID存入到數組中
* @param len:長度
* @param id :值
* @return
*/
public Long[] pushIds(int len,Long id){
Long[] ids = new Long[len];
for (int i = 0; i <ids.length ; i++) {
ids[i]=id;
}
return ids;
}
修改SeckillGoodsPushTask的loadGoodsPushRedis方法,添加隊列操作,代碼如下:
上圖代碼如下:
//商品數據隊列存儲,防止高併發超賣
Long[] ids = pushIds(seckillGood.getStockCount(), seckillGood.getId());
redisTemplate.boundListOps("SeckillGoodsCountList_"+seckillGood.getId()).leftPushAll(ids);
//自增計數器
redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillGood.getId(),seckillGood.getStockCount());
2.3 超賣控制
修改多線程下單方法,分別修改數量控制,以及售罄後用戶搶單排隊信息的清理,修改代碼如下圖:
上圖代碼如下:
/***
* 多線程下單操作
*/
@Async
public void createOrder(){
//從隊列中獲取排隊信息
SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps("SeckillOrderQueue").rightPop();
try {
//從隊列中獲取一個商品
Object sgood = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).rightPop();
if(sgood==null){
//清理當前用戶的排隊信息
clearQueue(seckillStatus);
return;
}
//時間區間
String time = seckillStatus.getTime();
//用戶登錄名
String username=seckillStatus.getUsername();
//用戶搶購商品
Long id = seckillStatus.getGoodsId();
//獲取商品數據
SeckillGoods goods = (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_" + time).get(id);
//如果有庫存,則創建秒殺商品訂單
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(id);
seckillOrder.setMoney(goods.getCostPrice());
seckillOrder.setUserId(username);
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("0");
//將秒殺訂單存入到Redis中
redisTemplate.boundHashOps("SeckillOrder").put(username,seckillOrder);
//商品庫存-1
Long surplusCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(id, -1);//商品數量遞減
goods.setStockCount(surplusCount.intValue()); //根據計數器統計
//判斷當前商品是否還有庫存
if(surplusCount<=0){
//並且將商品數據同步到MySQL中
seckillGoodsMapper.updateByPrimaryKeySelective(goods);
//如果沒有庫存,則清空Redis緩存中該商品
redisTemplate.boundHashOps("SeckillGoods_" + time).delete(id);
}else{
//如果有庫存,則直數據重置到Reids中
redisTemplate.boundHashOps("SeckillGoods_" + time).put(id,goods);
}
//搶單成功,更新搶單狀態,排隊->等待支付
seckillStatus.setStatus(2);
seckillStatus.setOrderId(seckillOrder.getId());
seckillStatus.setMoney(seckillOrder.getMoney().floatValue());
redisTemplate.boundHashOps("UserQueueStatus").put(username,seckillStatus);
} catch (Exception e) {
e.printStackTrace();
}
}
/***
* 清理用戶排隊信息
* @param seckillStatus
*/
public void clearQueue(SeckillStatus seckillStatus){
//清理排隊標示
redisTemplate.boundHashOps("UserQueueCount").delete(seckillStatus.getUsername());
//清理搶單標示
redisTemplate.boundHashOps("UserQueueStatus").delete(seckillStatus.getUsername());
}
3 訂單支付
完成秒殺下訂單後,進入支付頁面,此時前端會每3秒中向後臺發送一次請求用於判斷當前用戶訂單是否完成支付,如果完成了支付,則需要清理掉排隊信息,並且需要修改訂單狀態信息。
3.2 創建支付二維碼
下單成功後,會跳轉到支付選擇頁面,在支付選擇頁面要顯示訂單編號和訂單金額,所以我們需要在下單的時候,將訂單金額以及訂單編號信息存儲到用戶查詢對象中。
選擇微信支付後,會跳轉到微信支付頁面,微信支付頁面會根據用戶名查看用戶秒殺訂單,並根據用戶秒殺訂單的ID創建預支付信息並獲取二維碼信息,展示給用戶看,此時頁面每3秒查詢一次支付狀態,如果支付成功,需要修改訂單狀態信息。
3.2.1 回顯訂單號、金額
下單後,進入支付選擇頁面,需要顯示訂單號和訂單金額,所以需要在用戶下單後將該數據傳入到pay.html頁面,所以查詢訂單狀態的時候,需要將訂單號和金額封裝到查詢的信息中,修改查詢訂單裝的方法加入他們即可。
修改SeckillOrderController的queryStatus方法,代碼如下:
上圖代碼如下:
return new Result(true,seckillStatus.getStatus(),"搶購狀態",seckillStatus);
使用Postman測試,效果如下:
http://localhost:18084/seckill/order/query
3.2.2 創建二維碼
用戶創建二維碼,可以先查詢用戶的秒殺訂單搶單信息,然後再發送請求到支付微服務中創建二維碼,將訂單編號以及訂單對應的金額傳遞到支付微服務:/weixin/pay/create/native
。
使用Postman測試效果如下:
http://localhost:9022/weixin/pay/create/native?outtradeno=1132510782836314112&money=1
3.3 支付流程分析
如上圖,步驟分析如下:
1.用戶搶單,經過秒殺系統實現搶單,下單後會將向MQ發送一個延時隊列消息,包含搶單信息,延時半小時後才能監聽到
2.秒殺系統同時啓用延時消息監聽,一旦監聽到訂單搶單信息,判斷Redis緩存中是否存在訂單信息,如果存在,則回滾
3.秒殺系統還啓動支付回調信息監聽,如果支付完成,則將訂單喫句話到MySQL,如果沒完成,清理排隊信息回滾庫存
4.每次秒殺下單後調用支付系統,創建二維碼,如果用戶支付成功了,微信系統會將支付信息發送給支付系統指定的回調地址,支付系統收到信息後,將信息發送給MQ,第3個步驟就可以監聽到消息了。
3.4 支付回調更新
支付回調這一塊代碼已經實現了,但之前實現的是訂單信息的回調數據發送給MQ,指定了對應的隊列,不過現在需要實現的是秒殺信息發送給指定隊列,所以之前的代碼那塊需要動態指定隊列。
3.4.1 支付回調隊列指定
關於指定隊列如下:
1.創建支付二維碼需要指定隊列
2.回調地址回調的時候,獲取支付二維碼指定的隊列,將支付信息發送到指定隊列中
在微信支付統一下單API中,有一個附加參數,如下:
attach:附加數據,String(127),在查詢API和支付通知中原樣返回,可作爲自定義參數使用。
我們可以在創建二維碼的時候,指定該參數,該參數用於指定回調支付信息的對應隊列,每次回調的時候,會獲取該參數,然後將回調信息發送到該參數對應的隊列去。
3.4.1.1 改造支付方法
修改支付微服務的WeixinPayController的createNative方法,代碼如下:
修改支付微服務的WeixinPayService的createNative方法,代碼如下:
修改支付微服務的WeixinPayServiceImpl的createNative方法,代碼如下:
我們創建二維碼的時候,需要將下面幾個參數傳遞過去
username:用戶名,可以根據用戶名查詢用戶排隊信息
outtradeno:商戶訂單號,下單必須
money:支付金額,支付必須
queue:隊列名字,回調的時候,可以知道將支付信息發送到哪個隊列
修改WeixinPayApplication,添加對應隊列以及對應交換機綁定,代碼如下:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class WeixinPayApplication {
public static void main(String[] args) {
SpringApplication.run(WeixinPayApplication.class,args);
}
@Autowired
private Environment env;
/***
* 創建DirectExchange交換機
* @return
*/
@Bean
public DirectExchange basicExchange(){
return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);
}
/***
* 創建隊列
* @return
*/
@Bean(name = "queueOrder")
public Queue queueOrder(){
return new Queue(env.getProperty("mq.pay.queue.order"), true);
}
/***
* 創建秒殺隊列
* @return
*/
@Bean(name = "queueSeckillOrder")
public Queue queueSeckillOrder(){
return new Queue(env.getProperty("mq.pay.queue.seckillorder"), true);
}
/****
* 隊列綁定到交換機上
* @return
*/
@Bean
public Binding basicBindingOrder(){
return BindingBuilder
.bind(queueOrder())
.to(basicExchange())
.with(env.getProperty("mq.pay.routing.orderkey"));
}
/****
* 隊列綁定到交換機上
* @return
*/
@Bean
public Binding basicBindingSeckillOrder(){
return BindingBuilder
.bind(queueSeckillOrder())
.to(basicExchange())
.with(env.getProperty("mq.pay.routing.seckillorderkey"));
}
}
修改application.yml,添加如下配置
#位置支付交換機和隊列
mq:
pay:
exchange:
order: exchange.order
seckillorder: exchange.seckillorder
queue:
order: queue.order
seckillorder: queue.seckillorder
routing:
key: queue.order
seckillkey: queue.seckillorder
3.4.1.2 測試
使用Postman創建二維碼測試
http://localhost:18092/weixin/pay/create/native?username=szitheima&out_trade_no=1132510782836314121&total_fee=1&queue=queue.seckillorder&routingkey=queue.seckillorder&exchange=exchange.seckillorder
以後每次支付,都需要帶上對應的參數,包括前面的訂單支付。
3.4.1.3 改造支付回調方法
修改com.changgou.pay.controller.WeixinPayController的notifyUrl方法,獲取自定義參數,並轉成Map,獲取queue地址,並將支付信息發送到綁定的queue中,代碼如下:
3.4.2 支付狀態監聽
支付狀態通過回調地址發送給MQ之後,我們需要在秒殺系統中監聽支付信息,如果用戶已支付,則修改用戶訂單狀態,如果支付失敗,則直接刪除訂單,回滾庫存。
在秒殺工程中創建com.changgou.seckill.consumer.SeckillOrderPayMessageListener,實現監聽消息,代碼如下:
@Component
@RabbitListener(queues = "${mq.pay.queue.seckillorder}")
public class SeckillOrderPayMessageListener {
/**
* 監聽消費消息
* @param message
*/
@RabbitHandler
public void consumeMessage(@Payload String message){
System.out.println(message);
//將消息轉換成Map對象
Map<String,String> resultMap = JSON.parseObject(message,Map.class);
System.out.println("監聽到的消息:"+resultMap);
}
}
修改SeckillApplication創建對應的隊列以及綁定對應交換機。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
@EnableAsync
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class,args);
}
@Bean
public IdWorker idWorker(){
return new IdWorker(1,1);
}
@Autowired
private Environment env;
/***
* 創建DirectExchange交換機
* @return
*/
@Bean
public DirectExchange basicExchange(){
return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true,false);
}
/***
* 創建隊列
* @return
*/
@Bean(name = "queueOrder")
public Queue queueOrder(){
return new Queue(env.getProperty("mq.pay.queue.order"), true);
}
/***
* 創建秒殺隊列
* @return
*/
@Bean(name = "queueSeckillOrder")
public Queue queueSeckillOrder(){
return new Queue(env.getProperty("mq.pay.queue.seckillorder"), true);
}
/****
* 隊列綁定到交換機上
* @return
*/
@Bean
public Binding basicBindingOrder(){
return BindingBuilder
.bind(queueOrder())
.to(basicExchange())
.with(env.getProperty("mq.pay.routing.orderkey"));
}
/****
* 隊列綁定到交換機上
* @return
*/
@Bean
public Binding basicBindingSeckillOrder(){
return BindingBuilder
.bind(queueSeckillOrder())
.to(basicExchange())
.with(env.getProperty("mq.pay.routing.seckillorderkey"));
}
}
修改application.yml文件,添加如下配置:
#位置支付交換機和隊列
mq:
pay:
exchange:
order: exchange.order
seckillorder: exchange.seckillorder
queue:
order: queue.order
seckillorder: queue.seckillorder
routing:
key: queue.order
seckillkey: queue.seckillorder
3.4.3 修改訂單狀態
監聽到支付信息後,根據支付信息判斷,如果用戶支付成功,則修改訂單信息,並將訂單入庫,刪除用戶排隊信息,如果用戶支付失敗,則刪除訂單信息,回滾庫存,刪除用戶排隊信息。
3.4.3.1 業務層
修改SeckillOrderService,添加修改訂單方法,代碼如下
/***
* 更新訂單狀態
* @param out_trade_no
* @param transaction_id
* @param username
*/
void updatePayStatus(String out_trade_no, String transaction_id,String username);
修改SeckillOrderServiceImpl,添加修改訂單方法實現,代碼如下:
/***
* 更新訂單狀態
* @param out_trade_no
* @param transaction_id
* @param username
*/
@Override
public void updatePayStatus(String out_trade_no, String transaction_id,String username) {
//訂單數據從Redis數據庫查詢出來
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);
//修改狀態
seckillOrder.setStatus("1");
//支付時間
seckillOrder.setPayTime(new Date());
//同步到MySQL中
seckillOrderMapper.insertSelective(seckillOrder);
//清空Redis緩存
redisTemplate.boundHashOps("SeckillOrder").delete(username);
//清空用戶排隊數據
redisTemplate.boundHashOps("UserQueueCount").delete(username);
//刪除搶購狀態信息
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
}
3.4.3.2 修改訂單對接
修改微信支付狀態監聽的代碼,當用戶支付成功後,修改訂單狀態,也就是調用上面的方法,代碼如下:
3.4.4 刪除訂單回滾庫存
如果用戶支付失敗,我們需要刪除用戶訂單數據,並回滾庫存。
3.4.4.1 業務層實現
修改SeckillOrderService,創建一個關閉訂單方法,代碼如下:
/***
* 關閉訂單,回滾庫存
*/
void closeOrder(String username);
修改SeckillOrderServiceImpl,創建一個關閉訂單實現方法,代碼如下:
/***
* 關閉訂單,回滾庫存
* @param username
*/
@Override
public void closeOrder(String username) {
//將消息轉換成SeckillStatus
SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);
//獲取Redis中訂單信息
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);
//如果Redis中有訂單信息,說明用戶未支付
if(seckillStatus!=null && seckillOrder!=null){
//刪除訂單
redisTemplate.boundHashOps("SeckillOrder").delete(username);
//回滾庫存
//1)從Redis中獲取該商品
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).get(seckillStatus.getGoodsId());
//2)如果Redis中沒有,則從數據庫中加載
if(seckillGoods==null){
seckillGoods = seckillGoodsMapper.selectByPrimaryKey(seckillStatus.getGoodsId());
}
//3)數量+1 (遞增數量+1,隊列數量+1)
Long surplusCount = redisTemplate.boundHashOps("SeckillGoodsCount").increment(seckillStatus.getGoodsId(), 1);
seckillGoods.setStockCount(surplusCount.intValue());
redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).leftPush(seckillStatus.getGoodsId());
//4)數據同步到Redis中
redisTemplate.boundHashOps("SeckillGoods_"+seckillStatus.getTime()).put(seckillStatus.getGoodsId(),seckillGoods);
//清理排隊標示
redisTemplate.boundHashOps("UserQueueCount").delete(seckillStatus.getUsername());
//清理搶單標示
redisTemplate.boundHashOps("UserQueueStatus").delete(seckillStatus.getUsername());
}
}
3.4.4.2 調用刪除訂單
修改SeckillOrderPayMessageListener,在用戶支付失敗後調用關閉訂單方法,代碼如下:
//支付失敗,刪除訂單
seckillOrderService.closeOrder(attachMap.get("username"));
3.4.4.3 測試
使用Postman完整請求創建二維碼下單測試一次。
商品ID:1131814854034853888
數量:49
下單:
http://localhost:18084/seckill/order/add?id=1131814854034853888&time=2019052614
下單後,Redis數據
下單查詢:
http://localhost:18084/seckill/order/query
創建二維碼:
http://localhost:9022/weixin/pay/create/native?username=szitheima&outtradeno=1132530879663575040&money=1&queue=queue.seckillorder
秒殺搶單後,商品數量變化:
支付微服務回調方法控制檯:
{
nonce_str=Mnv06RIaIwxzg3bA,
code_url=weixin://wxpay/bizpayurl?pr=iTidd5h,
appid=wx8397f8696b538317,
sign=1436E43FBA8A171D79A9B78B61F0A7AB,
trade_type=NATIVE,
return_msg=OK,
result_code=SUCCESS,
mch_id=1473426802,
return_code=SUCCESS,
prepay_id=wx2614182102123859e3869a853739004200
}
{money=1, queue=queue.seckillorder, username=szitheima, outtradeno=1132530879663575040}
訂單微服務控制檯輸出
{
transaction_id=4200000289201905268232990890,
nonce_str=a1aefe00a9bc4e8bb66a892dba38eb42,
bank_type=CMB_CREDIT,
openid=oNpSGwUp-194-Svy3JnVlAxtdLkc,
sign=56679BC02CC82204635434817C1FCA46,
fee_type=CNY,
mch_id=1473426802,
cash_fee=1,
out_trade_no=1132530879663575040,
appid=wx8397f8696b538317,
total_fee=1,
trade_type=NATIVE,
result_code=SUCCESS,
attach={
"username": "szitheima",
"outtradeno": "1132530879663575040",
"money": "1",
"queue": "queue.seckillorder"
}, time_end=20190526141849, is_subscribe=N, return_code=SUCCESS
}
附錄:
支付微服務application.yml
server:
port: 9022
spring:
application:
name: pay
main:
allow-bean-definition-overriding: true
rabbitmq:
host: 127.0.0.1 #mq的服務器地址
username: guest #賬號
password: guest #密碼
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled設置爲false,則請求超時交給ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#微信支付信息配置
weixin:
appid: wx8397f8696b538317
partner: 1473426802
partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
notifyurl: http://2cw4969042.wicp.vip:36446/weixin/pay/notify/url
#位置支付交換機和隊列
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
seckillorder: queue.seckillorder
routing:
orderkey: queue.order
seckillorderkey: queue.seckillorder
秒殺微服務application.yml配置
server:
port: 18084
spring:
application:
name: seckill
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/changgou_seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: itcast
rabbitmq:
host: 127.0.0.1 #mq的服務器地址
username: guest #賬號
password: guest #密碼
main:
allow-bean-definition-overriding: true
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
mybatis:
configuration:
map-underscore-to-camel-case: true
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.changgou.seckill.pojo
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled設置爲false,則請求超時交給ribbon控制
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
#位置支付交換機和隊列
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
seckillorder: queue.seckillorder
routing:
orderkey: queue.order
seckillorderkey: queue.seckillorder
4 RabbitMQ延時消息隊列
4.1 延時隊列介紹
延時隊列即放置在該隊列裏面的消息是不需要立即消費的,而是等待一段時間之後取出消費。
那麼,爲什麼需要延遲消費呢?我們來看以下的場景
網上商城下訂單後30分鐘後沒有完成支付,取消訂單(如:淘寶、去哪兒網)
系統創建了預約之後,需要在預約時間到達前一小時提醒被預約的雙方參會
系統中的業務失敗之後,需要重試
這些場景都非常常見,我們可以思考,比如第二個需求,系統創建了預約之後,需要在預約時間到達前一小時提醒被預約的雙方參會。那麼一天之中肯定是會有很多個預約的,時間也是不一定的,假設現在有1點 2點 3點 三個預約,如何讓系統知道在當前時間等於0點 1點 2點給用戶發送信息呢,是不是需要一個輪詢,一直去查看所有的預約,比對當前的系統時間和預約提前一小時的時間是否相等呢?這樣做非常浪費資源而且輪詢的時間間隔不好控制。如果我們使用延時消息隊列呢,我們在創建時把需要通知的預約放入消息中間件中,並且設置該消息的過期時間,等過期時間到達時再取出消費即可。
Rabbitmq實現延時隊列一般而言有兩種形式:
第一種方式:利用兩個特性: Time To Live(TTL)、Dead Letter Exchanges(DLX)[A隊列過期->轉發給B隊列]
第二種方式:利用rabbitmq中的插件x-delay-message
4.2 TTL DLX實現延時隊列
4.2.1 TTL DLX介紹
TTL
RabbitMQ可以針對隊列設置x-expires(則隊列中所有的消息都有相同的過期時間)或者針對Message設置x-message-ttl(對消息進行單獨設置,每條消息TTL可以不同),來控制消息的生存時間,如果超時(兩者同時設置以最先到期的時間爲準),則消息變爲dead letter(死信)
Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange和x-dead-letter-routing-key(可選)兩個參數,如果隊列內出現了dead letter,則按照這兩個參數重新路由轉發到指定的隊列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key發送
4.2.3 DLX延時隊列實現
4.2.3.1 創建工程
創建springboot_rabbitmq_delay工程,並引入相關依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>changgou_parent</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springboot_rabbitmq_delay</artifactId>
<dependencies>
<!--starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--加入ampq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--測試-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
application.yml配置
spring:
application:
name: springboot-demo
rabbitmq:
host: 127.0.0.1
port: 5672
password: guest
username: guest
4.2.3.2 隊列創建
創建2個隊列,用於接收消息的叫延時隊列queue.message.delay,用於轉發消息的隊列叫queue.message,同時創建一個交換機,代碼如下:
@Configuration
public class QueueConfig {
/** 短信發送隊列 */
public static final String QUEUE_MESSAGE = "queue.message";
/** 交換機 */
public static final String DLX_EXCHANGE = "dlx.exchange";
/** 短信發送隊列 延遲緩衝(按消息) */
public static final String QUEUE_MESSAGE_DELAY = "queue.message.delay";
/**
* 短信發送隊列
* @return
*/
@Bean
public Queue messageQueue() {
return new Queue(QUEUE_MESSAGE, true);
}
/**
* 短信發送隊列
* @return
*/
@Bean
public Queue delayMessageQueue() {
return QueueBuilder.durable(QUEUE_MESSAGE_DELAY)
.withArgument("x-dead-letter-exchange", DLX_EXCHANGE) // 消息超時進入死信隊列,綁定死信隊列交換機
.withArgument("x-dead-letter-routing-key", QUEUE_MESSAGE) // 綁定指定的routing-key
.build();
}
/***
* 創建交換機
* @return
*/
@Bean
public DirectExchange directExchange(){
return new DirectExchange(DLX_EXCHANGE);
}
/***
* 交換機與隊列綁定
* @param messageQueue
* @param directExchange
* @return
*/
@Bean
public Binding basicBinding(Queue messageQueue, DirectExchange directExchange) {
return BindingBuilder.bind(messageQueue)
.to(directExchange)
.with(QUEUE_MESSAGE);
}
}
4.2.3.3 消息監聽
創建MessageListener用於監聽消息,代碼如下:
@Component
@RabbitListener(queues = QueueConfig.QUEUE_MESSAGE)
public class MessageListener {
/***
* 監聽消息
* @param msg
*/
@RabbitHandler
public void msg(@Payload Object msg){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("當前時間:"+dateFormat.format(new Date()));
System.out.println("收到信息:"+msg);
}
}
4.2.3.4 創建啓動類
@SpringBootApplication
@EnableRabbit
public class SpringRabbitMQApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRabbitMQApplication.class,args);
}
}
4.2.3.5 測試
@SpringBootTest
@RunWith(SpringRunner.class)
public class RabbitMQTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/***
* 發送消息
*/
@Test
public void sendMessage() throws InterruptedException, IOException {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("發送當前時間:"+dateFormat.format(new Date()));
Map<String,String> message = new HashMap<>();
message.put("name","szitheima");
rabbitTemplate.convertAndSend(QueueConfig.QUEUE_MESSAGE_DELAY, message, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
System.in.read();
}
}
其中message.getMessageProperties().setExpiration(“10000”);設置消息超時時間,超時後,會將消息轉入到另外一個隊列。
測試效果如下:
5 庫存回滾(作業)
5.1 秒殺流程回顧
如上圖,步驟分析如下:
1.用戶搶單,經過秒殺系統實現搶單,下單後會將向MQ發送一個延時隊列消息,包含搶單信息,延時半小時後才能監聽到
2.秒殺系統同時啓用延時消息監聽,一旦監聽到訂單搶單信息,判斷Redis緩存中是否存在訂單信息,如果存在,則回滾
3.秒殺系統還啓動支付回調信息監聽,如果支付完成,則將訂單喫句話到MySQL,如果沒完成,清理排隊信息回滾庫存
4.每次秒殺下單後調用支付系統,創建二維碼,如果用戶支付成功了,微信系統會將支付信息發送給支付系統指定的回調地址,支付系統收到信息後,將信息發送給MQ,第3個步驟就可以監聽到消息了。
延時隊列實現訂單關閉回滾庫存:
1.創建一個過期隊列 Queue1
2.接收消息的隊列 Queue2
3.中轉交換機
4.監聽Queue2
1)SeckillStatus->檢查Redis中是否有訂單信息
2)如果有訂單信息,調用刪除訂單回滾庫存->[需要先關閉微信支付]
3)如果關閉訂單時,用於已支付,修改訂單狀態即可
4)如果關閉訂單時,發生了別的錯誤,記錄日誌,人工處理
5.2 關閉支付
用戶如果半個小時沒有支付,我們會關閉支付訂單,但在關閉之前,需要先關閉微信支付,防止中途用戶支付。
修改支付微服務的WeixinPayService,添加關閉支付方法,代碼如下:
/***
* 關閉支付
* @param orderId
* @return
*/
Map<String,String> closePay(Long orderId) throws Exception;
修改WeixinPayServiceImpl,實現關閉微信支付方法,代碼如下:
/***
* 關閉微信支付
* @param orderId
* @return
* @throws Exception
*/
@Override
public Map<String, String> closePay(Long orderId) throws Exception {
//參數設置
Map<String,String> paramMap = new HashMap<String,String>();
paramMap.put("appid",appid); //應用ID
paramMap.put("mch_id",partner); //商戶編號
paramMap.put("nonce_str",WXPayUtil.generateNonceStr());//隨機字符
paramMap.put("out_trade_no",String.valueOf(orderId)); //商家的唯一編號
//將Map數據轉成XML字符
String xmlParam = WXPayUtil.generateSignedXml(paramMap,partnerkey);
//確定url
String url = "https://api.mch.weixin.qq.com/pay/closeorder";
//發送請求
HttpClient httpClient = new HttpClient(url);
//https
httpClient.setHttps(true);
//提交參數
httpClient.setXmlParam(xmlParam);
//提交
httpClient.post();
//獲取返回數據
String content = httpClient.getContent();
//將返回數據解析成Map
return WXPayUtil.xmlToMap(content);
}
5.3 關閉訂單回滾庫存
5.3.1 配置延時隊列
在application.yml文件中引入隊列信息配置,如下:
#位置支付交換機和隊列
mq:
pay:
exchange:
order: exchange.order
queue:
order: queue.order
seckillorder: queue.seckillorder
seckillordertimer: queue.seckillordertimer
seckillordertimerdelay: queue.seckillordertimerdelay
routing:
orderkey: queue.order
seckillorderkey: queue.seckillorder
配置隊列與交換機,在SeckillApplication中添加如下方法
/**
* 到期數據隊列
* @return
*/
@Bean
public Queue seckillOrderTimerQueue() {
return new Queue(env.getProperty("mq.pay.queue.seckillordertimer"), true);
}
/**
* 超時數據隊列
* @return
*/
@Bean
public Queue delaySeckillOrderTimerQueue() {
return QueueBuilder.durable(env.getProperty("mq.pay.queue.seckillordertimerdelay"))
.withArgument("x-dead-letter-exchange", env.getProperty("mq.pay.exchange.order")) // 消息超時進入死信隊列,綁定死信隊列交換機
.withArgument("x-dead-letter-routing-key", env.getProperty("mq.pay.queue.seckillordertimer")) // 綁定指定的routing-key
.build();
}
/***
* 交換機與隊列綁定
* @return
*/
@Bean
public Binding basicBinding() {
return BindingBuilder.bind(seckillOrderTimerQueue())
.to(basicExchange())
.with(env.getProperty("mq.pay.queue.seckillordertimer"));
}
5.3.2 發送延時消息
修改MultiThreadingCreateOrder,添加如下方法:
/***
* 發送延時消息到RabbitMQ中
* @param seckillStatus
*/
public void sendTimerMessage(SeckillStatus seckillStatus){
rabbitTemplate.convertAndSend(env.getProperty("mq.pay.queue.seckillordertimerdelay"), (Object) JSON.toJSONString(seckillStatus), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
}
在createOrder方法中調用上面方法,如下代碼:
//發送延時消息到MQ中
sendTimerMessage(seckillStatus);
5.3.3 庫存回滾
創建SeckillOrderDelayMessageListener實現監聽消息,並回滾庫存,代碼如下:
@Component
@RabbitListener(queues = "${mq.pay.queue.seckillordertimer}")
public class SeckillOrderDelayMessageListener {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SeckillOrderService seckillOrderService;
@Autowired
private WeixinPayFeign weixinPayFeign;
/***
* 讀取消息
* 判斷Redis中是否存在對應的訂單
* 如果存在,則關閉支付,再關閉訂單
* @param message
*/
@RabbitHandler
public void consumeMessage(@Payload String message){
//讀取消息
SeckillStatus seckillStatus = JSON.parseObject(message,SeckillStatus.class);
//獲取Redis中訂單信息
String username = seckillStatus.getUsername();
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);
//如果Redis中有訂單信息,說明用戶未支付
if(seckillOrder!=null){
System.out.println("準備回滾---"+seckillStatus);
//關閉支付
Result closeResult = weixinPayFeign.closePay(seckillStatus.getOrderId());
Map<String,String> closeMap = (Map<String, String>) closeResult.getData();
if(closeMap!=null && closeMap.get("return_code").equalsIgnoreCase("success") &&
closeMap.get("result_code").equalsIgnoreCase("success") ){
//關閉訂單
seckillOrderService.closeOrder(username);
}
}
}
}