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失效通知,有兩個註意事項各位需要註意:
- 事件通過 Redis 的訂閱與發佈功能(pub/sub)來進行分發,故需要訂閱(__keyevent@0__:expired)頻道 0表示db0 根據自己的dbindex選擇合適的數字
- 修改 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); } } }
測試日志如下:
通過日志我們發現,當優惠券到失效時,redis立即發送一條消息告知此優惠券失效,我們可以在監聽程序中獲取當前的id,查詢數據庫修改狀態
到此這篇關於spring整合redis消息監聽通知使用的文章就介紹到這瞭,更多相關spring redis消息監聽內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring IOC創建對象的兩種方式
- Spring項目中使用Junit單元測試並配置數據源的操作
- Spring超詳細講解註解開發
- Spring詳細講解@Autowired註解
- Spring 控制反轉和依賴註入的具體使用