Java之Redis隊列+Websocket+定時器實現跑馬燈實時刷新

1.業務描述

這幾天,公司有個業務,具體內容如下:

在儀表盤banner區域滾動播放提示信息。也就是實現一個實時播放消息的跑馬燈功能。播放的是一個任務內容(數據庫有一張表pm_task)。

跑馬燈消息提示內容總共有四種:

  • 任務下發——P3(消息播放隊列優先級) 
    任務被下發時進行提示。 
    文字提示內容:任務已下發:任務編號 任務名稱

  • 一般任務複覈通過——P4 
    任務複覈通過時進行提示。 
    文字提示內容:任務已通過:任務編號 任務名稱

  • 關鍵決策任務複覈通過——P1 
    任務複覈通過時進行提示。 
    文字提示內容:關鍵決策任務通過:任務編號 任務名稱

  • 關鍵驗證任務複覈通過——P2 
    任務複覈通過時進行提示。 
    文字提示內容:關鍵驗證任務通過:任務編號 任務名稱

滾動播放時,每個提示信息之間應由字符間隔。播放速度到時根據具體代碼運行情況進行分析,播放速度應不超過一般閱讀速度。當有提示信息生成時,末位補進提示信息隊列。 
特殊情況: 
1. 當信息同時生成時,同級任務信息按時間進行排序。 
2. 消息插播優先級:P1>P2>P3>P4。 
3. 插播爲即時插播,未播放完的消息不移除播放隊列。

2.技術分析

2.1業務實現流程

做業務功能實現的時候,流程基本都是這樣的:熟悉業務 —>>>分析業務—>>>拆分業務—>>> 尋找拆分任務的技術解決方案 —>>>編碼實現 —>>>愉快的玩耍

在業務沒想清楚之前,千萬不要動手寫代碼。

2.2技術選擇

跑馬燈功能

網上搜索前端跑馬燈功能實現,一堆,可以看看文章最後參考文章那一節。具體實現就是HTML的一個便籤< marquee>

<marquee behavior="alternate">我來回滾動</marquee>


Spring+Websocket實現消息

消息推送,公司既有的框架就是Websocket,所以可以在用戶進入頁面的時候,訂閱相關通道,用戶退出頁面的時候,取消相關的通道。在需要推送消息時候,實現消息推送既可。

Redis隊列存儲消息

後端產生的消息,事實上有2種存儲方法:

1.我利用數據庫,建立一張表,產生的每條消息都保存到表(xxx_marquee_msg)裏面。當前端跑馬燈需要數據的時候,從數據庫讀取一條優先級高的數據,返回給前端。與此同時,我把該條數據刪除,實現一個類似隊列這樣的一個功能。

2.我利用Redis的阻塞隊列功能,將數據存放到redis隊列中。前端需要的時候,我再從隊列中獲取數據。

兩種方法的比較:

利用數據庫方法實現,簡單,業務邏輯好控制,缺點是:你得實現表的增刪改查操作,需要些很對的代碼,從控制層,業務層,DAO層,一層一層的寫,一堆代碼,麻煩。

相比之下,如果用Redis的阻塞隊列來實現,我不需要寫增刪改查操操作,只需要get和push消息到隊列中即可,同時因爲在緩存中,效率高,缺點是:業務邏輯不好控制,比如我要實現隊列的排序,優先級,相對來說都比較麻煩。

就這樣糾結啊,糾結啊,我覺得選擇第二種方式,出於不想寫代碼的原因,加上第二種方式逼格高,效率高等等。

Redis消息隊列優先級解決方法

仔細看下需求,你會發現,需求中要求消息是排優先級的,這點就有點頭疼了,不過好在,我們的消息只有4中優先級,所以具體解決方案如下:

我定義4個隊列(queue),分別存放 P1 P2 P3 P4 四種基本的消息,取數據的時候,我先從P1隊列開始取,獲取不到時,依次從P2 P3 P4去消息。

可以參考這篇文章用redis實現支持優先級的消息隊列

Spring + Quartz實現定時刷新

因爲跑馬燈的功能要實現實時刷新,也就是當有新的消息產生的時候,要實時刷新跑馬燈的內容,我選擇的方案是:在後端開啓一個定時器,實時的去Redis緩存隊列獲取相關的信息,推送給前端。

3.代碼實現

3.1定時器代碼實現

我在pcsMainTaskService這個業務類實現一個定時器,定時器的方法是pcsMarqueeRefresh:

<bean name="pcsMarqueeRefreshParseJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"
p:targetObject-ref="pcsMainTaskService" p:targetMethod="pcsMarqueeRefresh"
p:concurrent="false"/>


<bean id="pcsMarqueeRefreshTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="pcsMarqueeRefreshParseJob"/>
<property name="cronExpression" value="0/10 * * * * ?"/>
</bean>


<util:list id="schedulerTriggers">
<ref bean="pcsMarqueeRefreshTrigger"></ref>
</util:list>

3.2業務代碼實現

    /**
* 描述:跑馬燈刷新(定時器)
*/

public void pcsMarqueeRefresh() throws Exception{
// 推送內容
String pushContent = null;
if(StringUtils.isEmpty(pushContent)){
pushContent = RedisUtils.getFromQueue(MarqueeRefreshUtils.REDIS_MARQUEE_P1_KEY);
}
if(StringUtils.isEmpty(pushContent)){
pushContent = RedisUtils.getFromQueue(MarqueeRefreshUtils.REDIS_MARQUEE_P2_KEY);
}
if(StringUtils.isEmpty(pushContent)){
pushContent = RedisUtils.getFromQueue(MarqueeRefreshUtils.REDIS_MARQUEE_P3_KEY);
}
if(StringUtils.isEmpty(pushContent)){
pushContent = RedisUtils.getFromQueue(MarqueeRefreshUtils.REDIS_MARQUEE_P4_KEY);
}
//推送消息
if(StringUtils.isNotEmpty(pushContent)){
//查詢系統所有的用戶
List<String> userIds = sysUserService.find(new ArrayList<>()).stream().map(SysUser::getId).collect(Collectors.toList());
//websocket推送消息
redisPubSubService.publish(new RedisMessage(pushContent, userIds, MarqueeRefreshUtils.MARQUEE_CHANNEL,false));
}
}

3.3 WebSocket消息推送代碼實現

消息推送代碼比較簡單,獲取系統用戶,往通道(MarqueeRefreshUtils.MARQUEE_CHANNEL)推送消息。

//查詢系統所有的用戶
List<String> userIds = sysUserService.find(new ArrayList<>()).stream().map(SysUser::getId).collect(Collectors.toList());
//websocket推送消息
redisPubSubService.publish(new RedisMessage(pushContent, userIds, MarqueeRefreshUtils.MARQUEE_CHANNEL,false));

3.4消息存取實現

package com.evada.de.projcommand.utils;

import com.evada.de.common.enums.projcommond.TaskDeliverStatus;
import com.evada.de.common.enums.projcommond.TaskTypeEnum;
import com.evada.de.common.util.RedisUtils;
import com.evada.de.projcommand.model.PcsTask;

/**
* 描述:跑馬燈消息刷新
* Created by huangwy on 2017/1/9.
*/

public class MarqueeRefreshUtils {

// 隊列總共分爲4個級別,分別爲 P1 P2 P3 P4
public static final String REDIS_MARQUEE_P1_KEY = "inno.pcs.marquee.refresh.p1";
public static final String REDIS_MARQUEE_P2_KEY = "inno.pcs.marquee.refresh.p2";
public static final String REDIS_MARQUEE_P3_KEY = "inno.pcs.marquee.refresh.p3";
public static final String REDIS_MARQUEE_P4_KEY = "inno.pcs.marquee.refresh.p4";
// 訂閱頻道
public static final String MARQUEE_CHANNEL = "inno.pcs.marquee.refresh";
//消息前綴
public static final String PRE_P1_MESSAGE = "關鍵決策任務通過:";
public static final String PRE_P2_MESSAGE = "關鍵驗證任務通過:";
public static final String PRE_P3_MESSAGE = "任務已下發:";
public static final String PRE_P4_MESSAGE = "任務已通過:";


public static void pushToQueue(PcsTask pcsTask){
if(!(pcsTask.getWorkitemStatus().equals("3")
|| pcsTask.getDeliverStatus().equals(TaskDeliverStatus.TASK_DELIVER.toString()))){
return;
}
StringBuffer content = new StringBuffer();
//關鍵決策任務
if(TaskTypeEnum.KEY_DECISION_TASK.toString().equals(pcsTask.getType()) && pcsTask.getWorkitemStatus().equals("3")){
content.append(PRE_P1_MESSAGE).append(pcsTask.getCode()).append(" ").append(pcsTask.getName());
RedisUtils.putToQueue(REDIS_MARQUEE_P1_KEY,content.toString());
}
//關鍵驗證任務
if(TaskTypeEnum.KEY_VALIDATION_TASK.toString().equals(pcsTask.getType()) && pcsTask.getWorkitemStatus().equals("3")){
content.append(PRE_P2_MESSAGE).append(pcsTask.getCode()).append(" ").append(pcsTask.getName());
RedisUtils.putToQueue(REDIS_MARQUEE_P2_KEY,content.toString());
}
//任務已下發
if(TaskTypeEnum.KEY_VALIDATION_TASK.toString().equals(pcsTask.getType())
&& pcsTask.getDeliverStatus().equals(TaskDeliverStatus.TASK_DELIVER.toString())){
content.append(PRE_P3_MESSAGE).append(pcsTask.getCode()).append(" ").append(pcsTask.getName());
RedisUtils.putToQueue(REDIS_MARQUEE_P3_KEY,content.toString());
}
//一般任務
if(TaskTypeEnum.GENERAL_TASK.toString().equals(pcsTask.getType()) && pcsTask.getWorkitemStatus().equals("3")){
content.append(PRE_P4_MESSAGE).append(pcsTask.getCode()).append(" ").append(pcsTask.getName());
RedisUtils.putToQueue(REDIS_MARQUEE_P4_KEY,content.toString());
}
}
}

3.5緩存工具類實現

該工具類主要是實現隊列數據的存和取,相對來說比較簡單:

package com.evada.de.common.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisUtils {
private static RedisTemplate tmp;

@Autowired
RedisUtils(RedisTemplate redisTemplate) {
tmp = redisTemplate;
}


/**
* set value to queue
* @param key
* @param value
* @return
*/

public static Long putToQueue(final String key, final String value) {
Long l = (Long) tmp.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection)throws DataAccessException {
return connection.lPush(key.getBytes(), value.getBytes());
}
});
return l;
}

/**
* get value from queue
* @param key
* @return
*/

public static String getFromQueue(final String key) {
byte[] b = (byte[]) tmp.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection)throws DataAccessException {
return connection.lPop(key.getBytes());
}
});
if(b != null){
return new String(b);
}
return null;
}

}

好了,寫到這裏基本就實現了,很簡單有木有~~~

阿里面試官:HashMap中的8和6的關係(1)

各大互聯網企業Java面試題彙總,如何成功拿到百度的offer

阿里面試題剖析,如何保證消息不被重複消費?

一文搞定併發面試題

深入理解JVM垃圾收集機制,下次面試你準備好了嗎

Spring反射+策略模式Demo





交流/資源分享

OMG關注它



點亮 ,告訴大家你也在看 


本文分享自微信公衆號 - Java高級架構師(java968)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章