專案中有這麼一個需求:
當使用者餘額不足,1分鐘後,機器人進行視訊邀請,當使用者點選接聽時,則提示使用者充值;當使用者點選拒絕,3分鐘後,再對該使用者使用機器人進行視訊邀請,當使用者點選接聽時,則提示使用者充值;當使用者點選拒絕,10分鐘後,再次對該使用者使用機器人進行視訊邀請,當使用者點選接聽時,則提示使用者充值;當使用者點選拒絕,3次誘導充值結束。
當使用者餘額充足,1分鐘後,推薦真實使用者對該使用者進行視訊邀請,若該使用者接聽,則對真實使用者傳送視訊邀請;當使用者結束通話,3分鐘後,繼續推薦真實使用者進行視訊邀請,若該使用者接聽,則對真實使用者傳送視訊邀請,當使用者結束通話,10分鐘後,繼續推薦真實使用者進行視訊邀請。
當使用者餘額不夠時,繼續走餘額不夠的邏輯。
分析這個需求,難點無非就是三次時間間隔,開始考慮的是使用訊息佇列RocketMQ,但用RocketMQ有點大材小用的意思。後面考慮用Redis,如果Redis有對過期時間的監聽,那豈不美哉,我擦,谷歌了一發,還真TM有。於是,就研究了一發,也是比較簡單。
Redis對過期時間的監聽是這樣的:使用String型別,設定Key-Value,對該Key設定過期時間,當時間過期後,觸發某個事件,這就是所謂的 對過期事件的監聽。過期事件是通過Redis的釋出訂閱功能來進行分發。
事件型別
對於每個修改資料庫的操作,鍵空間通知都會傳送兩種不同型別的事件訊息:keyspace 和 keyevent。以 keyspace 為字首的頻道被稱為鍵空間通知(key-space notification), 而以 keyevent 為字首的頻道則被稱為鍵事件通知(key-event notification)。
事件是用 keyspace@DB:KeyPattern 或者 keyevent@DB:OpsType 的格式來發布訊息的。
DB表示在第幾個庫;KeyPattern則是表示需要監控的鍵模式(可以用萬用字元,如:key:);OpsType則表示操作型別。因此,如果想要訂閱特殊的Key上的事件,應該是訂閱keyspace。
比如說,對 0 號資料庫的鍵 mykey 執行 DEL 命令時, 系統將分發兩條訊息, 相當於執行以下兩個 PUBLISH 命令:
PUBLISH keyspace@0:sampleKey del
PUBLISH keyevent@0:del sampleKey
訂閱第一個頻道 keyspace@0:mykey 可以接收 0 號資料庫中所有修改鍵 mykey 的事件,而訂閱第二個頻道 keyevent@0:del 則可以接收 0 號資料庫中所有執行 del 命令的鍵。
開啟配置
鍵空間通知通常是不啟用的,因為這個過程會產生額外消耗。所以在使用該特性之前,請確認一定是要用這個特性的,然後修改配置檔案,或使用config配置。相關配置項如下:
輸入的引數中至少要有一個 K 或者 E , 否則的話, 不管其餘的引數是什麼, 都不會有任何通知被分發。上表中斜體的部分為通用的操作或者事件,而黑體則表示特定資料型別的操作。在redis的配置檔案redis.conf中修改 notify-keyspace-events “Kx”,注意:這個雙引號是一定要的,否則配置不成功,啟動也不報錯。例如,“Kx”表示想監控某個Key的失效事件。也可以在命令列通過config配置:CONFIG set notify-keyspace-events Ex (但非持久化)。實現步驟
- 修改redis.conf配置檔案中的 notify-keyspace-events “Kx”,redis預設是關閉的
- 對SpringBoot整合 Redis的釋出訂閱,指定監聽類和監聽型別
程式碼示例
pom依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
複製程式碼
redis工具類(部分)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* redis快取客戶端
*/
@Component
public class RedisCacheUtils<T> {
@Autowired
private RedisTemplate<String, T> redisTemplate;
/**
* 寫入單個物件到快取(可以設定有效時間)
* @param key
* @param value
* @param expireTime 有效時間 單位秒
* @return
*/
public boolean set(final String key, T value, Long expireTime) {
boolean result = false;
try {
ValueOperations<String, T> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
throw e;
}
return result;
}
/**
* 自增
* @param key
* @param by
* @param seconds
* @return
*/
public Long incr(final String key, final long by,final long seconds) {
Long count = redisTemplate.opsForValue().increment(key, by);
redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
return count;
}
}
複製程式碼
監聽配置
import com.app.common.constants.SystemConstant;
import org.springframework.beans.factory.annotation.Autowired;
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.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisLinstenerConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public ConsumerRedisListener consumerRedis() {
return new ConsumerRedisListener();
}
@Bean
public ChannelTopic topic() {
return new ChannelTopic("__keyevent@0__:expired");
}
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(consumerRedis(),topic());
return container;
}
}
複製程式碼
redis監聽器:
import com.app.cache.RedisCacheUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.StringRedisTemplate;
public class ConsumerRedisListener implements MessageListener {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisCacheUtils redisCacheUtils;
@Override
public void onMessage(Message message, byte[] pattern) {
doBusiness(message);
}
/**
* 列印 message body 內容
* @param message
*/
public void doBusiness(Message message) {
Object value = stringRedisTemplate.getValueSerializer().deserialize(message.getBody());
byte[] body = message.getBody();
byte[] channel = message.getChannel();
String topic = new String(channel);
String itemValue = new String(body);
System.out.println("itemValue-----------------------" + itemValue);
// 如果key中包含^,則說明是 視訊邀請的
if(itemValue.contains("^")) {
String[] keyArr = itemValue.split("\\^");
String userId = keyArr[1];
// 防止重複消費,設定一個過期時間
Long num = redisCacheUtils.incr(userId + "_incr", 1L, 60L);
if(StringUtils.isBlank(userId)) {
return;
}
if(num == 1){
// 處理邏輯,給App推送訊息,調起視訊呼叫
//…………
}
}
}
}
複製程式碼
看了上面的程式碼可能有點懵,貌似和上述所說的時間間隔並沒有什麼瓜葛,然而並不是。首先,當使用者當日首次登陸App時,客戶端用呼叫一個介面,表示使用者進入App,我會在介面中判斷使用者是不是當日首次登陸,如果是,則使用"video" + "^" + 使用者的ID + "^" + 180 作為一個Key,value無所謂,並對該key設定60秒的過期時間,當該key過期,則會進入到redis監聽中,並對客戶端推送訊息,其中,訊息體中包含一個關鍵欄位,此關鍵欄位就是下次需要間隔多久來發起視訊邀請,即之前過期Key後面跟隨的180,當客戶端點選結束通話,呼叫結束通話介面時,就將此欄位傳過來,然後 使用"video" + "^" + 使用者的ID + "^" + 600 作為一個Key,並對該key設定180秒的過期時間,後面邏輯同理……
然而,因為專案是分散式專案,會部署多個節點,這樣就存在重複訂閱,因為這一部分資料老大要求不能存到資料庫,所以使用了redis 的incr來記錄進入過期監聽器的次數,並設定過期時間為60秒,這樣 多個節點即使重複訂閱,也會只有一個訂閱者可以處理邏輯,即對客戶端推送訊息,這裡的推送訊息使用的是融雲的IM,後續對該IM進行分析。
歡迎關注我的公眾號~ 搜尋公眾號: 翻身碼農把歌唱 或者 掃描下方二維碼: