SpringBoot實(shí)現(xiàn)監(jiān)聽redis key失效事件

需求:

處理訂單過期自動(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)行處理
        }
    }
}
  • 或者打開RedisListenerConfigcontainer.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

參考:https://spring.io/guides/gs/messaging-redis/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容