App視訊邀請功能

翻身碼農把歌唱發表於2018-09-23

專案中有這麼一個需求:

當使用者餘額不足,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配置。相關配置項如下:

App視訊邀請功能
輸入的引數中至少要有一個 K 或者 E , 否則的話, 不管其餘的引數是什麼, 都不會有任何通知被分發。上表中斜體的部分為通用的操作或者事件,而黑體則表示特定資料型別的操作。在redis的配置檔案redis.conf中修改 notify-keyspace-events “Kx”,注意:這個雙引號是一定要的,否則配置不成功,啟動也不報錯。例如,“Kx”表示想監控某個Key的失效事件。也可以在命令列通過config配置:CONFIG set notify-keyspace-events Ex (但非持久化)。

實現步驟

  1. 修改redis.conf配置檔案中的 notify-keyspace-events “Kx”,redis預設是關閉的
  2. 對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進行分析。

歡迎關注我的公眾號~ 搜尋公眾號: 翻身碼農把歌唱 或者 掃描下方二維碼:

相關文章