前言
在電商、支付等領域,往往會有這樣的場景,用戶下單后放棄支付了,那這筆訂單會在指定的時間段后進行關閉操作,細心的你一定發現了像某寶、某東都有這樣的邏輯,而且時間很準確,誤差在1s內;那他們是怎么實作的呢?
業務場景
我們以訂單功能為例說明下:生成訂單后一段時間不支付訂單會自動關閉,最簡單的想法是設定定時任務輪詢,但是每個訂單的創建時間不一樣,定時任務的規則無法設定,如果將定時任務執行的間隔設定的過短,太影響效率,還有一種想法,在用戶進入訂單界面的時候,判斷時間執行相關操作,
一、定時任務
一般情況下,最不推薦的方式就是關單方式就是定時任務方式,原因我們可以看下面的圖來說明

我們假設,關單時間為下單后10分鐘,定時任務間隔也是10分鐘;通過上圖我們看出,如果在第1分鐘下單,在第20分鐘的時候才能被掃描到執行關單操作,這樣誤差達到10分鐘,這在很多場景下是不可接受的,另外需要頻繁掃描主訂單號造成網路IO和磁盤IO的消耗,對實時交易造成一定的沖擊,所以PASS
二、RabbitMQ延遲佇列
我們在下單后,往MQ投遞一個訊息,設定其有效期為30分鐘,在不設定對應佇列的消費者的情況下,該訊息將一直不被消費,那么30分鐘后,該訊息過期會被投遞到死信佇列,由死信消費者消費,我們就可以在死信消費者根據訂單id執行相應的業務邏輯
- MQ的配置:
#配置virtual-host虛擬主機
spring.rabbitmq.virtual-host=test_order_close
#ip地址
spring.rabbitmq.host=127.0.0.1
#用戶名 密碼
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#連接埠號
spring.rabbitmq.port=5672
#死信佇列
test.dlx.exchange=test_order_dlx_exchange
test.dlx.queue=test_order_dlx_queue
test.dlx.routingKey=dlx
##備胎交換機
test.order.exchange=test_order_exchange
test.order.queue=test_order_queue
test.order.routingKey=test.order
- 死信的config類
@Component
public class DeadLetterMQConfig {
/**
* 訂單交換機
*/
@Value("${test.order.exchange}")
private String orderExchange;
/**
* 訂單佇列
*/
@Value("${test.order.queue}")
private String orderQueue;
/**
* 訂單路由key
*/
@Value("${test.order.routingKey}")
private String orderRoutingKey;
/**
* 死信交換機
*/
@Value("${test.dlx.exchange}")
private String dlxExchange;
/**
* 死信佇列
*/
@Value("${test.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${test.dlx.routingKey}")
private String dlxRoutingKey;
/**
* 宣告死信交換機
*/
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
/**
* 宣告死信佇列
*/
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
/**
* 宣告訂單業務交換機
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
/**
* 宣告訂單佇列 核心操作一
*/
@Bean
public Queue orderQueue() {
Map<String, Object> arguments = new HashMap<>(2);
// 系結我們的死信交換機
arguments.put("x-dead-letter-exchange", dlxExchange);
// 系結我們的路由key
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, arguments);
}
/**
* 系結訂單佇列到訂單交換機
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(orderRoutingKey);
}
/**
* 系結死信佇列到死信交換機
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(dlxRoutingKey);
}
}
- Controller
@GetMapping("/addOrder")
public String addOrder(){
String orderId=System.currentTimeMillis()+"";
OrderEntity orderEntity=new OrderEntity("訂單過期測驗",orderId,0);
int result= orderMapper.addOrder(orderEntity);
if(result<=0){
return "fail";
}
rabbitTemplate.convertAndSend(orderExchange,orderRoutingKey,orderId,messagePostProcessor());
return "success";
}
//處理待發送訊息
private MessagePostProcessor messagePostProcessor(){
return new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//設定有效期20秒
message.getMessageProperties().setExpiration("20000");
return message;
}
};
}
- 死信消費者
@Component
public class OrderDlxConsumer {
@Autowired
private OrderMapper orderMapper;
//監聽死信佇列
@RabbitListener(queues = "test_order_dlx_queue")
public void orderConsumer(String orderId) {
System.out.println("死信佇列獲取訊息:" + orderId);
if (StringUtils.isEmpty(orderId)) {
return;
}
//根據id查詢
OrderEntity orderEntity = orderMapper.getOrder(orderId);
if (null == orderEntity) {
return;
}
//獲取狀態
Integer orderStatus=orderEntity.getOrderStatus();
//判斷未支付 , 關閉訂單
if(0==orderStatus){
orderMapper.updateStatus(orderId,2);
}
}
}
- 測驗

介面測驗工具呼叫我們寫的controller方法
新增成功后資料庫表中會新增一條status為0的記錄

- 過期時間到了之后會執行死信消費者的方法

三、Redis過期監聽
在生成訂單時,向 Redis 中增加一個 KV 鍵值對,K 為訂單號,保證通過 K 能定位到資料庫中的某個訂單即可,V 可為任意值,假設,生成訂單時向 Redis 中存放 K 為訂單號,V 也為訂單號的鍵值對,并設定過期時間為 30 分鐘,如果該鍵值對在 30 分鐘過期后能夠發送給程式一個通知,或者執行一個方法,那么即可解決訂單關閉問題,實作:通過監聽 Redis 提供的過期佇列來實作,監聽過期佇列后,如果 Redis 中某一個 KV 鍵值對過期了,那么將向監聽者發送訊息,監聽者可以獲取到該鍵值對的 K,注意,是獲取不到 V 的,因為已經過期了,這就是上面所提到的,為什么要保證能通過 K 來定位到訂單,而 V 為任意值即可,拿到 K 后,通過 K 定位訂單,并判斷其狀態,如果是未支付,更新為關閉,或者取消狀態即可,
修改 redis 相關事件配置,找到 redis 組態檔 redis.conf,查看 notify-keyspace-events 配置項,如果沒有,添加 notify-keyspace-events Ex,如果有值,則追加 Ex,相關引數說明如下:
- 引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置
定義配置 RedisListenerConfig 實作監聽 Redis key 過期時間
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
定義監聽器 RedisKeyExpirationListener,實作KeyExpirationEventMessageListener 介面,查看原始碼發現,該介面監聽所有 db 的過期事件 keyevent@*:expired
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
/**
* 監聽所有db的過期事件__keyevent@*__:expired"
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 針對 redis 資料失效事件,進行資料處理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 獲取到失效的 key,進行取消訂單業務處理
String expiredKey = message.toString();
System.out.println(expiredKey);
}
}
總結
以上方法只是個人對于關單的一些想法,可能有些地方有疏漏,當然如果你有更好的關單方式也可以隨時在下方評論區溝通交流,另外期待您的關注和回復,謝謝!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/382826.html
標籤:其他
