需求:
處理訂單過期自動(dòng)取消,比如下單30分鐘未支付自動(dòng)更改訂單狀態(tài)
解決方案1:
可以利用redis天然的key自動(dòng)過期機(jī)制,下單時(shí)將訂單id寫入redis,過期時(shí)間30分鐘,30分鐘后檢查訂單狀態(tài),如果未支付,則進(jìn)行處理但是key過期了redis有通知嗎?答案是肯定的。
開啟redis key過期提醒
修改redis相關(guān)事件配置。找到redis配置文件redis.conf,查看“notify-keyspace-events”的配置項(xiàng),如果沒有,添加“notify-keyspace-events Ex”,如果有值,添加Ex,相關(guān)參數(shù)說明如下:
K:keyspace事件,事件以__keyspace@<db>__為前綴進(jìn)行發(fā)布;
E:keyevent事件,事件以__keyevent@<db>__為前綴進(jìn)行發(fā)布;
g:一般性的,非特定類型的命令,比如del,expire,rename等;
$:字符串特定命令;
l:列表特定命令;
s:集合特定命令;
h:哈希特定命令;
z:有序集合特定命令;
x:過期事件,當(dāng)某個(gè)鍵過期并刪除時(shí)會(huì)產(chǎn)生該事件;
e:驅(qū)逐事件,當(dāng)某個(gè)鍵因maxmemore策略而被刪除時(shí),產(chǎn)生該事件;
A:g$lshzxe的別名,因此”AKE”意味著所有事件。
redis測(cè)試:
打開一個(gè)redis-cli ,監(jiān)控db0的key過期事件
127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
打開另一個(gè)redis-cli ,發(fā)送定時(shí)過期key
127.0.0.1:6379> setex test_key 3 test_value
觀察上一個(gè)redis-cli ,會(huì)發(fā)現(xiàn)收到了過期的keytest_key,但是無法收到過期的value test_value
127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "test_key"
在springboot中使用
- 1.pom 中添加依賴
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 2.定義配置
RedisListenerConfig
import edu.zut.ding.listener.RedisExpiredListener;
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.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* @Author lsm
* @Date 2018/10/27 20:56
*/
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// container.addMessageListener(new RedisExpiredListener(), new PatternTopic("__keyevent@0__:expired"));
return container;
}
}
- 3.定義監(jiān)聽器,實(shí)現(xiàn)
KeyExpirationEventMessageListener接口,查看源碼發(fā)現(xiàn),該接口監(jiān)聽所有db的過期事件keyevent@*:expired"
import edu.zut.ding.constants.SystemConstant;
import edu.zut.ding.enums.OrderState;
import edu.zut.ding.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 監(jiān)聽所有db的過期事件__keyevent@*__:expired"
* @author lsm
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 針對(duì)redis數(shù)據(jù)失效事件,進(jìn)行數(shù)據(jù)處理
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 用戶做自己的業(yè)務(wù)處理即可,注意message.toString()可以獲取失效的key
String expiredKey = message.toString();
if(expiredKey.startsWith("Order:")){
//如果是Order:開頭的key,進(jìn)行處理
}
}
}
- 或者打開
RedisListenerConfig中container.addMessageListener(new RedisExpiredListener(), new PatternTopic("__keyevent@0__:expired"));注釋,再定義監(jiān)聽器,監(jiān)控__keyevent@0__:expired事件,即db0過期事件。這個(gè)地方定義的比較靈活,可以自己定義監(jiān)控什么事件。
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
/**
* @author lsm
*/
public class RedisExpiredListener implements MessageListener {
/**
* 客戶端監(jiān)聽訂閱的topic,當(dāng)有消息的時(shí)候,會(huì)觸發(fā)該方法;
* 并不能得到value, 只能得到key。
* 姑且理解為: redis服務(wù)在key失效時(shí)(或失效后)通知到j(luò)ava服務(wù)某個(gè)key失效了, 那么在java中不可能得到這個(gè)redis-key對(duì)應(yīng)的redis-value。
* * 解決方案:
* 創(chuàng)建copy/shadow key, 例如 set vkey "vergilyn"; 對(duì)應(yīng)copykey: set copykey:vkey "" ex 10;
* 真正的key是"vkey"(業(yè)務(wù)中使用), 失效觸發(fā)key是"copykey:vkey"(其value為空字符為了減少內(nèi)存空間消耗)。
* 當(dāng)"copykey:vkey"觸發(fā)失效時(shí), 從"vkey"得到失效時(shí)的值, 并在邏輯處理完后"del vkey"
*
* 缺陷:
* 1: 存在多余的key; (copykey/shadowkey)
* 2: 不嚴(yán)謹(jǐn), 假設(shè)copykey在 12:00:00失效, 通知在12:10:00收到, 這間隔的10min內(nèi)程序修改了key, 得到的并不是 失效時(shí)的value.
* (第1點(diǎn)影響不大; 第2點(diǎn)貌似redis本身的Pub/Sub就不是嚴(yán)謹(jǐn)?shù)? 失效后還存在value的修改, 應(yīng)該在設(shè)計(jì)/邏輯上杜絕)
* 當(dāng)"copykey:vkey"觸發(fā)失效時(shí), 從"vkey"得到失效時(shí)的值, 并在邏輯處理完后"del vkey"
*
*/
@Override
public void onMessage(Message message, byte[] bytes) {
byte[] body = message.getBody();// 建議使用: valueSerializer
byte[] channel = message.getChannel();
System.out.print("onMessage >> " );
System.out.println(String.format("channel: %s, body: %s, bytes: %s"
,new String(channel), new String(body), new String(bytes)));
}
}
解決方案2
使用spring + quartz定時(shí)任務(wù)(支持任務(wù)信息寫入mysql,多節(jié)點(diǎn)分布式執(zhí)行任務(wù)),下單成功后,生成一個(gè)30分鐘后運(yùn)行的任務(wù),30分鐘后檢查訂單狀態(tài),如果未支付,則進(jìn)行處理
解決方案3
將訂單過期時(shí)間信息寫入mysql,按分鐘輪詢查詢mysql,如果超時(shí)則進(jìn)行處理,效率差!時(shí)間精準(zhǔn)度底!
解決方案4
使用Java的定時(shí)器,不支持高可用,設(shè)置定時(shí)器的節(jié)點(diǎn)掛掉或者重啟,任務(wù)失效!
結(jié)論
推薦使用方案1和方案2