spring整合redis消息監聽通知使用的實現示例

問題引入

在電商系統中,秒殺,搶購,紅包優惠卷等操作,一般都會設置時間限制,比如訂單15分鐘不付款自動關閉,紅包有效期24小時等等。那對於這種需求最簡單的處理方式就是使用定時任務,定時掃描數據庫的方式處理。但是為瞭更加精確的時間控制,定時任務的執行時間會設置的很短,所以會造成很大的數據庫壓力。

是否有更加穩妥的解決方式呢?我們可以利用REDIS的key失效機制結合REDIS的消息通知機制結合完成類似問題的處理。

1.1 過期問題描述

在電商系統中,秒殺,搶購,紅包優惠卷等操作,一般都會設置時間限制,比如訂單15分鐘不付款自動關閉,紅包有效期24小時等等

1.2 常用解決方案分析

目前企業中最常見的解決方案大致分為兩種:

  • 使用定時任務處理,定時掃描數據庫中過期的數據,然後進行修改。但是為瞭更加精確的時間控制,定時任務的執行時間會設置的很短,所以會造成很大的數據庫壓力。
  • 使用消息通知,當數據失效時發送消息,程序接收到失效消息後對響應的數據進行狀態修改。此種方式不會對數據庫造成太大的壓力

1.3.整合SpringData Redis開發

我們使用redis解決過期優惠券和紅包等問題,並且在java環境中使用redis的消息通知。目前世面比較流行的java代碼操作redis的AIP有:Jedis和RedisTemplate

Jedis是Redis官方推出的一款面向Java的客戶端,提供瞭很多接口供Java語言調用。

SpringData Redis是Spring官方推出,可以算是Spring框架集成Redis操作的一個子框架,封裝瞭Redis的很多命令,可以很方便的使用Spring操作Redis數據庫。由於現代企業開發中都使用Spring整合項目,所以在API的選擇上我們使用Spring提供的SpringData Redis

spring整合redis監聽消息

1. 配置監聽redis消息

如果要在java代碼中監聽redis的主題消息,我們還需要自定義處理消息的監聽器,

MessageListener類的源碼:

package org.springframework.data.redis.connection;
import org.springframework.lang.Nullable;

/**
 * Listener of messages published in Redis.
 *
 */
public interface MessageListener {

	/**
	 * Callback for processing received objects through Redis.
	 *
	 * @param message message must not be {@literal null}.
	 * @param pattern pattern matching the channel (if specified) - can be {@literal null}.
	 */
	void onMessage(Message message, @Nullable byte[] pattern);
}

拓展這個接口的代碼如下

/**
 * 消息監聽器:需要實現MessageListener接口
 * 		實現onMessage方法
 */
public class RedisMessageListener implements MessageListener {

	/**
	 * 	處理redis消息:當從redis中獲取消息後,打印主題名稱和基本的消息
	 */
	public void onMessage(Message message, byte[] pattern) {
		 System.out.println("從channel為" + new String(message.getChannel())
	                + "中獲取瞭一條新的消息,消息內容:" + new String(message.getBody()));
	}

}

這樣我們就定義好瞭一個消息監聽器,當訂閱的頻道有一條新的消息發送過來之後,通過此監聽器中的onMessage方法處理

當監聽器程序寫好之後,我們還需要在springData redis的配置文件中添加監聽器以及訂閱的頻道主題,

我們測試訂閱的頻道為ITCAST,配置如下:

	<!-- 配置處理消息的消息監聽適配器 -->
	<bean class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter" id="messageListener">
		<!-- 構造方法註入:自定義的消息監聽 -->
	    <constructor-arg>
	        <bean class="cn.itcast.redis.listener.RedisKeyExpiredMessageDelegate"/>
	    </constructor-arg>
	</bean>
	
	<!-- 消息監聽者容器:對所有的消息進行統一管理 -->
	<bean class="org.springframework.data.redis.listener.RedisMessageListenerContainer" id="redisContainer">
	    <property name="connectionFactory" ref="connectionFactory"/>
	    <property name="messageListeners">
	    	<map>
	    		<!-- 配置頻道與監聽器
	    			將此頻道中的內容交由此監聽器處理
	    			key-ref:監聽,處理消息
	    			ChannelTopic:訂閱的消息頻道
	    		 -->
	            <entry key-ref="messageListener">
	                <list>
	                    <bean class="org.springframework.data.redis.listener.ChannelTopic">
	                        <constructor-arg value="ITCAST"></constructor-arg>
	                    </bean>
	                </list>
	            </entry>
		    </map>
		 </property>
	</bean>

2 測試消息

配置好消息監聽,已經訂閱的主題之後就可以啟動程序進行測試瞭。由於有監聽程序在,隻需要已java代碼的形式啟動,創建spring容器(當spring容器加載之後,會創建監聽器一直監聽對應的消息)。

	public static void main(String[] args) {
		ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext-data-redis.xml");
	}

當程序啟動之後,會一直保持運行狀態。即訂閱瞭ITCSAT頻道的消息,這個時候通過redis的客戶端程序(redis-cli)發佈一條消息

在這裡插入圖片描述

命令解釋:

publish topic名稱 消息內容 : 向指定頻道發送一條消息

發送消息之後,我們在來看java控制臺輸出可驗證獲取到瞭此消息

結合redis的key失效機制和消息完成過期優惠券處理

解決過期優惠券的問題處理起來比較簡單:

​ 在redis的內部當一個key失效時,也會向固定的頻道中發送一條消息,我們隻需要監聽到此消息獲取數據庫中的id,修改對應的優惠券狀態就可以瞭。這也帶來瞭一些繁瑣的操作:用戶獲取到優惠券之後需要將優惠券存入redis服務器並設置超時時間。

由於要借助redis的key失效通知,有兩個註意事項各位需要註意:

  1. 事件通過 Redis 的訂閱與發佈功能(pub/sub)來進行分發,故需要訂閱(__keyevent@0__:expired)頻道 0表示db0 根據自己的dbindex選擇合適的數字
  2. 修改 redis.conf 文件

修改 notify-keyspace-events Ex

# K    鍵空間通知,以__keyspace@<db>__為前綴
# E    鍵事件通知,以__keysevent@<db>__為前綴
# g    del , expipre , rename 等類型無關的通用命令的通知, ...
# $    String命令
# l    List命令
# s    Set命令
# h    Hash命令
# z    有序集合命令
# x    過期事件(每次key過期時生成)
# e    驅逐事件(當key在內存滿瞭被清除時生成)
# A    g$lshzxe的別名,因此”AKE”意味著所有的事件

1 模擬過期代金卷案例

前置性的內容已經和大傢都介紹完畢,接下來我們就可以使用redis的消息通知結合springDataRedis完成一個過期優惠券的處理,為瞭更加直觀的展示問題,這裡準備瞭兩個程序:

第一個程序(coupon-achieve)用來模擬用戶獲取一張優惠券並保存到數據庫,存入redis緩存。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class CouponTest {

	@Autowired
	private CouponMapper couponMapper;
	
	@Autowired
	private RedisTemplate<String, String> redisTemplate;
	
	@Test
	public void testSaveCoupon() {
		
		Date now = new Date();
		int timeOut = 1;//設置優惠券的失效時間(一分鐘後失效)
		
		//自定義一張優惠券,
		Coupon coupon = new Coupon();
		coupon.setName("測試優惠券");//設置名稱
		coupon.setMoney(BigDecimal.ONE);//設置金額
		coupon.setCouponDesc("全品類優惠10元");//設置說明
		coupon.setCreateTime(now);//設置獲取時間
		//設置超時時間:優惠券有效期1分鐘後超時
		coupon.setExpireTime(DataUtils.addTime(now, timeOut));
		//設置狀態:0-可用 1-已失效 2-已使用
		coupon.setState(0);
		couponMapper.saveCoupon(coupon );
		
		/**
		 * 將優惠券信息保存到redis服務器中:
		 * 	為瞭方便處理,由於我們處理的時候隻需要獲取id就可以瞭,
		 * 		所以保存的key設置為coupon:優惠券的主鍵
		 * 		value:設置為主鍵
		 */
		redisTemplate.opsForValue().set("coupon:"+coupon.getId(), coupon.getId()+"", (long)timeOut, TimeUnit.MINUTES);
	}

第二個程序(coupon-expired)配置消息通知監聽redis的key失效,獲取通知之後修改優惠券狀態

數據庫表:

CREATE TABLE `t_coupon` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `name` varchar(60) DEFAULT NULL COMMENT '優惠券名稱',
  `money` decimal(10,0) DEFAULT NULL COMMENT '金額',
  `coupon_desc` varchar(128) DEFAULT NULL COMMENT '優惠券說明',
  `create_time` datetime DEFAULT NULL COMMENT '獲取時間',
  `expire_time` datetime DEFAULT NULL COMMENT '失效時間',
  `state` int(1) DEFAULT NULL COMMENT '狀態,0-有效,1-已失效,2-已使用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

2 配置redis中key失效的消息監聽

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:task="http://www.springframework.org/schema/task"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
		">
		
	<description>spring-data整合jedis</description>
	
	<!-- springData Redis的核心API -->
	<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
		<property name="connectionFactory" ref="connectionFactory"></property>
		<property name="keySerializer">
			<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>	
		</property>
		<property name="valueSerializer">
			<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean>
		</property>
	</bean>
	
	<!-- 連接工廠 -->
	<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
		<property name="hostName" value="127.0.0.1"></property>
		<property name="port" value="6379"></property>
		<property name="database" value="0"></property>
		<property name="poolConfig" ref="poolConfig"></property>
	</bean>
	
	<!-- 連接池基本配置 -->
	<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="maxIdle" value="5"></property>
		<property name="maxTotal" value="10"></property>
		<property name="testOnBorrow" value="true"></property>
	</bean>
	
	<!-- 配置監聽 -->
	<bean class="org.springframework.data.redis.listener.adapter.MessageListenerAdapter" id="messageListener">
	    <constructor-arg>
	        <bean class="cn.itcast.redis.listener.RedisKeyExpiredMessageDelegate"/>
	    </constructor-arg>
	</bean>
	
	<!-- 監聽容器 -->
	<bean class="org.springframework.data.redis.listener.RedisMessageListenerContainer" id="redisContainer">
	    <property name="connectionFactory" ref="connectionFactory"/>
	    <property name="messageListeners">
	    	<map>
	            <entry key-ref="messageListener">
	                <list>
	                	<!-- __keyevent@0__:expired  配置訂閱的主題名稱
	                	此名稱時redis提供的名稱,標識過期key消息通知
	                			0表示db0 根據自己的dbindex選擇合適的數字
	                	 -->
	                    <bean class="org.springframework.data.redis.listener.ChannelTopic">
	                        <constructor-arg value="__keyevent@0__:expired"></constructor-arg>
	                    </bean>
	                </list>
	            </entry>
		    </map>
		 </property>
	</bean>

</beans>

3 接收失效消息完成過期代金卷處理

package cn.itcast.redis.listener;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;

import cn.itcast.entity.Coupon;
import cn.itcast.mapper.CouponMapper;

public class RedisKeyExpiredMessageDelegate implements MessageListener {

	@Autowired
	private CouponMapper couponMapper;
	
	public void onMessage(Message message, byte[] pattern) {
		//1.獲取失效的key
		String key = new String(message.getBody());
		//判斷是否時優惠券失效通知
		if(key.startsWith("coupon")){
			//2.從key中分離出失效優惠券id
			String redisId = key.split(":")[1];
			//3.查詢優惠卷信息
			Coupon coupon = couponMapper.selectCouponById(Long.parseLong(redisId));
			//4.修改狀態
			coupon.setState(1);
			//5.更新數據庫
			couponMapper.updateCoupon(coupon);
		}
	}
}

測試日志如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6jqnUGHO-1638810946988)(images\image012.png)]

通過日志我們發現,當優惠券到失效時,redis立即發送一條消息告知此優惠券失效,我們可以在監聽程序中獲取當前的id,查詢數據庫修改狀態

到此這篇關於spring整合redis消息監聽通知使用的文章就介紹到這瞭,更多相關spring redis消息監聽內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: