【redis】spring boot利用redis的Keyspace Notifications實現訊息通知

來源:互聯網
上載者:User

標籤:override   逾時   top   記憶體   rgb   dism   iss   logs   container   

前言

  需求:當redis中的某個key失效的時候,把失效時的value寫入資料庫。

  github: https://github.com/vergilyn/RedisSamples

  1、修改redis.conf

  安裝的redis服務預設是: notify-keyspace-events "",修改成 notify-keyspace-events Ex;

  位置:redis安裝目下的redis.windows-service.conf 或 redis.windows.conf。(具體看redis服務載入的哪個配置, 貌似要redis 2.8+才支援)

  可以在redis.conf中找到對應的描述

# K    鍵空間通知,以[email protected]<db>__為首碼# E    鍵事件通知,以[email protected]<db>__為首碼# g    del , expipre , rename 等類型無關的通用命令的通知, ...# $    String命令# l    List命令# s    Set命令# h    Hash命令# z    有序集合命令# x    到期事件(每次key到期時產生)# e    驅逐事件(當key在記憶體滿了被清除時產生)# A    g$lshzxe的別名,因此”AKE”意味著所有的事件
2、通過JedisPubSub實現

    省略spring boot配置,完整代碼見github。

/** * key到期事件推送到topic中只有key,無value,因為一旦到期,value就不存在了。 */@Componentpublic class JedisExpiredListener extends JedisPubSub {    /** 參考redis目錄下redis.conf中的"EVENT NOTIFICATION", redis預設的db{0, 15}一共16個資料庫     * K    Keyspace events, published with [email protected]<db>__ prefix.     * E    Keyevent events, published with [email protected]<db>__ prefix.     *     */    public final static String LISTENER_PATTERN = "[email protected]*__:expired";    /**
     * 雖然能注入,但貌似在listener-class中jedis無法使用(無法建立串連到redis),exception message:
* "only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context" */ @Autowired private Jedis jedis; /** * 初始化按運算式的方式訂閱時候的處理 */ @Override public void onPSubscribe(String pattern, int subscribedChannels) { System.out.print("onPSubscribe >> "); System.out.println(String.format("pattern: %s, subscribedChannels: %d", pattern, subscribedChannels)); } /** * 取得按運算式的方式訂閱的訊息後的處理 */ @Override public void onPMessage(String pattern, String channel, String message) { System.out.print("onPMessage >> "); System.out.println(String.format("key: %s, pattern: %s, channel: %s", message, pattern, channel)); } /** * 取得訂閱的訊息後的處理 */ @Override public void onMessage(String channel, String message) { super.onMessage(channel, message); } /** * 初始化訂閱時候的處理 */ @Override public void onSubscribe(String channel, int subscribedChannels) { super.onSubscribe(channel, subscribedChannels); } /** * 取消訂閱時候的處理 */ @Override public void onUnsubscribe(String channel, int subscribedChannels) { super.onUnsubscribe(channel, subscribedChannels); } /** * 取消按運算式的方式訂閱時候的處理 */ @Override public void onPUnsubscribe(String pattern, int subscribedChannels) { super.onPUnsubscribe(pattern, subscribedChannels); }}
@RunWith(SpringRunner.class)@SpringBootTest(classes=JedisExpiredApplication.class)public class JedisExpiredApplicationTest {    @Autowired    private Jedis jedis;    @Autowired    private JedisExpiredListener expiredListener;    @Before    public void before() throws Exception {        jedis.flushAll();        jedis.set(JedisConfig.DEFAULE_KEY,"123321");        System.out.println(JedisConfig.DEFAULE_KEY + " = " + jedis.get(JedisConfig.DEFAULE_KEY));        System.out.println("set expired 5s");        jedis.expire(JedisConfig.DEFAULE_KEY,5);    }    @Test    public void testPSubscribe(){        /* psubscribe是一個阻塞的方法,在取消訂閱該頻道前,會一直阻塞在這,只有當取消了訂閱才會執行下面的other code         * 可以onMessage/onPMessage裡面收到訊息後,調用了unsubscribe()/onPUnsubscribe(); 來取消訂閱,這樣才會執行後面的other code         */        jedis.psubscribe(expiredListener,JedisExpiredListener.LISTENER_PATTERN);        // other code    }}

輸出結果:

  vkey = 123321
   set expired 5s
   onPSubscribe >> pattern: [email protected]*__:expired, subscribedChannels: 1
   onPMessage >> key: vkey, pattern: [email protected]*__:expired, channel: [email protected]__:expired

3、通過實現添加MessageListener

 省略spring boot的redis配置。

@SpringBootApplicationpublic class RedisExpiredApplication implements CommandLineRunner{    @Autowired    private RedisTemplate redisTemplate;    @Autowired    private RedisExpiredListener expiredListener;    /**     * 解決redisTemplate的key/value亂碼問題:     *  <br/> <a href="http://www.zhimengzhe.com/shujuku/other/192111.html">http://www.zhimengzhe.com/shujuku/other/192111.html</a>     *  <br/> <a href="http://blog.csdn.net/tianyaleixiaowu/article/details/70595073">http://blog.csdn.net/tianyaleixiaowu/article/details/70595073</a>     * @return     */    @Bean("redis")    @Primary    public RedisTemplate redisTemplate(){        RedisSerializer<String> stringSerializer = new StringRedisSerializer();        redisTemplate.setKeySerializer(stringSerializer);        redisTemplate.setValueSerializer(stringSerializer);        redisTemplate.setHashKeySerializer(stringSerializer);        redisTemplate.setHashValueSerializer(stringSerializer);        return redisTemplate;    }    @Bean    public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory redisConnection, Executor executor){        RedisMessageListenerContainer container = new RedisMessageListenerContainer();        // 設定Redis的串連工廠        container.setConnectionFactory(redisConnection);        // 設定監聽使用的線程池//        container.setTaskExecutor(executor);        // 設定監聽的Topic: PatternTopic/ChannelTopic        Topic topic = new PatternTopic(RedisExpiredListener.LISTENER_PATTERN);        // 設定監聽器        container.addMessageListener(new RedisExpiredListener(), topic);        return container;    }    @Bean    public Executor executor(){        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();        executor.setCorePoolSize(10);        executor.setMaxPoolSize(20);        executor.setQueueCapacity(100);        executor.setKeepAliveSeconds(60);        executor.setThreadNamePrefix("V-Thread");        // rejection-policy:當pool已經達到max size的時候,如何處理新任務        // CALLER_RUNS:不在新線程中執行任務,而是由調用者所在的線程來執行        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());        executor.initialize();        return executor;    }    public static void main(String[] args) {        SpringApplication.run(RedisExpiredApplication.class,args);    }    @Override    public void run(String... strings) throws Exception {        redisTemplate.opsForValue().set("vkey", "vergilyn", 5, TimeUnit.SECONDS);        System.out.println("init : set vkey vergilyn ex 5");        System.out.println("thread sleep: 10s");        Thread.sleep(10 * 1000);        System.out.println("thread recover: get vkey = " + redisTemplate.opsForValue().get("vkey"));    }}
@Componentpublic class RedisExpiredListener implements MessageListener {    public final static String LISTENER_PATTERN = "__key*__:*";    /**     * 用戶端監聽訂閱的topic,當有訊息的時候,會觸發該方法;     * 並不能得到value, 只能得到key。     * 姑且理解為: redis服務在key失效時(或失效後)通知到java服務某個key失效了, 那麼在java中不可能得到這個redis-key對應的redis-value。     * 
     * 解決方案:     *  建立copy/shadow key, 例如 set vkey "vergilyn"; 對應copykey: set copykey:vkey "" ex 10;     *  真正的key是"vkey"(業務中使用), 失效觸發key是"copykey:vkey"(其value為空白字元為了減少記憶體空間消耗)。     *  當"copykey:vkey"觸發失效時, 從"vkey"得到失效時的值, 並在邏輯處理完後"del vkey"     *      * 缺陷:     *  1: 存在多餘的key; (copykey/shadowkey)     *  2: 不嚴謹, 假設copykey在 12:00:00失效, 通知在12:10:00收到, 這間隔的10min內程式修改了key, 得到的並不是 失效時的value.     *  (第1點影響不大; 第2點貌似redis本身的Pub/Sub就不是嚴謹的, 失效後還存在value的修改, 應該在設計/邏輯上杜絕)     *  當"copykey:vkey"觸發失效時, 從"vkey"得到失效時的值, 並在邏輯處理完後"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)));    }}

輸出結果:

  init : set vkey vergilyn ex 5
   thread sleep: 10s
   onMessage >> channel: [email protected]__:expired, body: vkey, bytes: __key*__:*
   thread recover: get vkey = null

4、問題

  1) 不管是JedisPubSub,還是MessageListener都不可能得到value。

    個人理解:在12:00:00,java推送給redis一條命令”set vkey vergilyn ex 10”。此時redis服務已經完整的知道了這個key的失效時間,在12:00:10時redis服務把”vkey”失效。

然後通知到java(即回調到JedisPubSub/MessageListener),此時不可能在java中通過”vkey”得到其value。

(最簡單的測試,在Listener中打斷點,然後通過redis-cli.exe命令查看,“vkey”已經不存在了,但Listener才進入到message()方法)


2) redis的expire不是嚴格的即時執行

摘自 http://redisdoc.com/topic/notification.html

Redis 使用以下兩種方式刪除到期的鍵:

  • 當一個鍵被訪問時,程式會對這個鍵進行檢查,如果鍵已經到期,那麼該鍵將被刪除。
  • 底層系統會在後台漸進地尋找並刪除那些到期的鍵,從而處理那些已經到期、但是不會被訪問到的鍵。

當到期鍵被以上兩個程式的任意一個發現、 並且將鍵從資料庫中刪除時, Redis 會產生一個 expired 通知。

Redis 並不保證存留時間(TTL)變為 0 的鍵會立即被刪除: 如果程式沒有訪問這個到期鍵, 或者帶有存留時間的鍵非常多的話, 那麼在鍵的存留時間變為 0 , 直到鍵真正被刪除這中間, 可能會有一段比較顯著的時間間隔。

因此, Redis 產生 expired 通知的時間為到期鍵被刪除的時候, 而不是鍵的存留時間變為 0 的時候。

如果業務無法容忍從到期到刪除中間的時間間隔,那麼就只有用其他的方式了。

 

3) 如何在expire回調中得到expire key的value

  參考:https://stackoverflow.com/questions/26406303/redis-key-expire-notification-with-jedis

set vkey somevalueset shadowkey:vkey "" ex 10

  相當於每個key都有對應的一個shadowkey,”shadowkey”只是用來設定expire時間,”key”才儲存value及參與商務邏輯。

  所以當”shadowkey”失效通知到listener時,程式中可以通過”key”得到其value,並在邏輯處理完時”del key”。

  (“shadowkey”的value為null或Null 字元串,目的是為了節約記憶體空間。)

  缺陷:

    1. 多餘了很多無效的 shadowkey;

      2. 資料不嚴謹。假設copykey在 12:00:00失效, 通知在12:10:00收到, 這間隔的10min內程式修改了key, 得到的並不是 失效時的value.

    相對來說,第1點無關緊要,只是暫時多了一些輔助用的key,但會被程式自己清理掉,不用再去維護,或一直存在於redis緩衝中。

    第2點,更多的是設計邏輯有缺陷,可以把失效時間定的更長,保證在”那個間隔”內不可能出現失效key的修改。


4)  特別

摘自 http://blog.csdn.net/gqtcgq/article/details/50808729

Redis的發布/訂閱目前是即發即棄(fire and forget)模式的,因此無法實現事件的可靠通知。也就是說,如果發布/訂閱的用戶端斷鏈之後又重連,則在用戶端斷鏈期間的所有事件都丟失了。

未來計劃支援事件的可靠通知,但是這可能會通過讓訂閱與發布功能本身變得更可靠來實現,也可能會在Lua指令碼中對訊息的訂閱與發布進行監聽,從而實作類別似將事件推入到列表這樣的操作。


參考:  redis設定鍵的存留時間或到期時間  Redis Key expire notification with Jedis

  (以下的文章都講的差不多)

  JAVA實現redis逾時失效key 的監聽觸發

  spring boot-使用redis的Keyspace Notifications實現定時任務隊列 redis 逾時失效key 的監聽觸發

  Redis鍵空間通知(keyspace notifications)

【redis】spring boot利用redis的Keyspace Notifications實現訊息通知

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.