Java 使用Redis實現延時隊列

A:需求說明:

  1. 如果系統中需要用到定時執行計劃的,又不想用到中間件,如果輪詢數據庫的話,會導致大量資源消耗,這樣我們就可以使用Redis來實現類似功(需要使用rabbitMQ的請看這裏:https://blog.csdn.net/u010096717/article/details/82148681
  2. 業務類型,如訂單一些評論,如果48h用戶未對商家評論,系統會自動產生一條默認評論,還有排隊到時提醒等

B:實現思路:

  1. 將整個Redis當做消息池,以kv形式存儲消息,key爲id,value爲具體的消息body
  2. 使用ZSET做優先隊列,按照score維持優先級(用當前時間+需要延時的時間作爲score)
  3. 輪詢ZSET,拿出score比當前時間戳大的數據(已過期的)
  4. 根據id拿到消息池的具體消息進行消費
  5. 消費成功,刪除改隊列和消息
  6. 消費失敗,讓該消息重新回到隊列

C:代碼實現

  1. Message消息封裝類
    @Data
    public class Message {
    
        /**
         * 消息id
         */
        private String id;
        /**
         * 消息延遲/毫秒
         */
        private long delay;
    
        /**
         * 消息存活時間
         */
        private int ttl;
        /**
         * 消息體,對應業務內容
         */
        private String body;
        /**
         * 創建時間,如果只有優先級沒有延遲,可以設置創建時間爲0
         * 用來消除時間的影響
         */
        private long createTime;
    
    }

 

2.基於redis的消息隊列

@Component
public class RedisMQ {

    /**
     * 消息池前綴,以此前綴加上傳遞的消息id作爲key,以消息{@link Message}
     * 的消息體body作爲值存儲
     */
    public static final String MSG_POOL = "Message:Pool:";
    /**
     * zset隊列 名稱 queue
     */
    public static final String QUEUE_NAME = "Message:Queue:";

    private static final int SEMIH = 30*60;



    @Autowired
    private RedisService redisService;

    /**
     * 存入消息池
     * @param message
     * @return
     */
    public boolean addMsgPool(Message message) {

        if (null != message) {
            return redisService.setExp(MSG_POOL + message.getId(), message.getBody(), Long.valueOf(message.getTtl() + SEMIH));
        }
        return false;
    }

    /**
     * 從消息池中刪除消息
     * @param id
     * @return
     */
    public void deMsgPool(String id) {
        redisService.remove(MSG_POOL + id);
    }

    /**
     * 向隊列中添加消息
     * @param key
     * @param score 優先級
     * @param val
     * @return 返回消息id
     */
    public void enMessage(String key, long score, String val) {
        redisService.zsset(key,val,score);
    }

    /**
     * 從隊列刪除消息
     * @param id
     * @return
     */
    public boolean deMessage(String key, String id) {
        return redisService.zdel(key, id);
    }
    
}

 

3Redis操作工具類,這個工具類比較多方法,就不貼在這裏了(https://blog.csdn.net/u010096717/article/details/83783865)

4.編寫消息發送(生產者)

@Component
public class MessageProvider {

    static Logger logger = LoggerFactory.getLogger(MessageProvider.class);


    private static int delay = 30;//30秒,可自己動態傳入

    @Resource
    private RedisMQ redisMQ;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //改造成redis
    public void sendMessage(String messageContent) {
        try {
            if (messageContent != null){
                String seqId = UUID.randomUUID().toString();
                // 將有效信息放入消息隊列和消息池中
                Message message = new Message();
                // 可以添加延遲配置
                message.setDelay(delay*1000);
                message.setCreateTime(System.currentTimeMillis());
                message.setBody(messageContent);
                message.setId(seqId);
                // 設置消息池ttl,防止長期佔用
                message.setTtl(delay + 360);
                redisMQ.addMsgPool(message);
                //當前時間加上延時的時間,作爲score
                Long delayTime = message.getCreateTime() + message.getDelay();
                String d = sdf.format(message.getCreateTime());
                System.out.println("當前時間:" + d+",消費的時間:" + sdf.format(delayTime));
                redisMQ.enMessage(RedisMQ.QUEUE_NAME,delayTime, message.getId());
            }else {
                logger.warn("消息內容爲空!!!!!");
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

5.消息消費者

@Component
public class RedisMQConsumer {

    @Resource
    private RedisMQ redisMQ;

    @Autowired
    private RedisService redisService;

    @Autowired
    private MessageProvider provider;

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    /**
     * 消息隊列監聽器<br>
     *
     */
    @Scheduled(cron = "*/1 * * * * *")
    public void monitor() {
        Set<String> set = redisService.rangeByScore(RedisMQ.QUEUE_NAME, 0, System.currentTimeMillis());
        if (null != set) {
            long current = System.currentTimeMillis();
            for (String id : set) {
                long  score = redisService.getScore(RedisMQ.QUEUE_NAME, id).longValue();
                if (current >= score) {
                    // 已超時的消息拿出來消費
                    String str = "";
                    try {
                        str = redisService.get(RedisMQ.MSG_POOL + id);
                        System.out.println("消費了:" + str+ ",消費的時間:" + sdf.format(System.currentTimeMillis()));
                    } catch (Exception e) {
                        e.printStackTrace();
                        //如果出了異常,則重新放回隊列
                        System.out.println("消費異常,重新回到隊列");
                        provider.sendMessage(str);
                    } finally {
                        redisMQ.deMessage(RedisMQ.QUEUE_NAME, id);
                        redisMQ.deMsgPool(id);
                    }
                }
            }
        }
    }
}

6.配置信息

<!--1依賴引入-->
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>


2yml配置
spring:
  redis:
    database: 1
    host: 127.0.0.1
    port: 6379

以上代碼已經實現了延遲消費功能,現在來測試一下,調用MessageProvider的sendMessage方法,我設定了30秒

可以看到結果

因爲我們是用定時器去輪詢的,會出現誤差

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