Redis實現延遲任務(過期取消訂單)

1. 生產需求:

  • 用戶下訂單後,15分鐘未支付自動取消;
  • 用戶成功下單支付後確認收貨, 15天默認好評

2. 實現思路

利用redis的排序列表,ZSet進行需求實現, 下面是我的流程圖和思路導線

 

3. 思路說明

我們把Zset中的score當成時間戳, 這樣我們就可以獲得以時間戳排序的任務列表, 這我們通過score區間進行拉取任務,進行消費.

4.代碼封裝實現

  • 首先是封裝延時隊列的工廠(完美契合Spring框架), 如果想要創建自己的特色延時隊列則需要繼承這個抽象工廠
package com.zjrcinfo.zjguahao.common.redis.delayqueue;

import com.zjrcinfo.zjguahao.common.redis.cluster.JedisClusterCache;
import com.zjrcinfo.zjguahao.common.utils.ThreadPoolUtil;
import com.zjrcinfo.zjguahao.common.web.log.LoggerName;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Tuple;

import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Description: 延時隊列工廠
 * User: zhouzhou
 * Date: 2019-09-26
 * Time: 14:32
 */
public abstract class AbstractDelayQueueMachineFactory {

    protected Logger logger = LoggerFactory.getLogger(LoggerName.KAFKA);

    @Autowired
    protected JedisClusterCache jedisClusterCache;

    /**
     * 插入任務id
     * @param jobId 任務id(隊列內唯一)
     * @param time 延時時間(單位 :秒)
     * @return 是否插入成功
     */
    public boolean addJobId(String jobId,  Integer time) {
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND, time);
        long delaySeconds = instance.getTimeInMillis() / 1000;
        Long zadd = jedisClusterCache.zadd(setDelayQueueName(), delaySeconds, jobId);
        return zadd > 0;

    }

    private void startDelayQueueMachine() {
        logger.info(String.format("延時隊列機器{%s}開始運作", setDelayQueueName()));

        // 發生異常捕獲並且繼續不能讓戰鬥停下來
        while (true) {
            try {
                // 獲取當前時間的時間戳
                long now = System.currentTimeMillis() / 1000;
                // 獲取當前時間前的任務列表
                Set<Tuple> tuples = jedisClusterCache.zrangeByScoreWithScores(setDelayQueueName(), 0, now);
                // 如果不爲空則遍歷判斷其是否滿足取消要求
                if (!CollectionUtils.isEmpty(tuples)) {
                    for (Tuple tuple : tuples) {

                        String jobId = tuple.getElement();
                        Long num = jedisClusterCache.zrem(setDelayQueueName(), jobId);
                        // 如果移除成功, 則取消訂單
                        if (num > 0) {
                            ThreadPoolUtil.execute(() ->invoke(jobId));
                        }
                    }
                }

            } catch (Exception e) {
                logger.warn(String.format("處理延時任務發生異常,異常原因爲{%s}", e.getMessage()), e);
            } finally {
                // 間隔一秒鐘搞一次
                try {
                    TimeUnit.SECONDS.sleep(1L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

    }


    /**
     * 最終執行的任務方法
     * @param jobId 任務id
     */
    public abstract void invoke(String jobId);


    /**
     * 要實現延時隊列的名字
     *
     */
    public abstract String setDelayQueueName();


    @PostConstruct
    public void init(){
        new Thread(this::startDelayQueueMachine).start();
    }

}
  • 測試延時隊列的實現
/**
 * Description: 測試訂單延時隊列
 * User: zhouzhou
 * Date: 2019-09-26
 * Time: 15:14
 */
@Component
public class TestOrderDelayQueue extends AbstractDelayQueueMachineFactory {

    @Autowired
    private TestOrderDelayQueueService testOrderDelayQueueService;

    @Override
    public void invoke(String jobId) {
        testOrderDelayQueueService.cancelOrder(jobId);
    }

    @Override
    public String setDelayQueueName() {
        return "TestOrder";
    }


}
  • 具體延時消費的Service
package com.zjrcinfo.zjguahao.product.service.impl;

import org.springframework.stereotype.Service;

/**
 * Description:
 * User: zhouzhou
 * Date: 2019-09-26
 * Time: 15:21
 */
@Service
public class TestOrderDelayQueueService {


    public void cancelOrder(String orderNumber) {
        System.out.println(System.currentTimeMillis() + "ms:redis消費了一個任務:消費的訂單OrderId爲" + orderNumber);
    }

    public void cancelReg(String orderNumber) {
        System.out.println(System.currentTimeMillis() + "ms:redis消費了一個任務:消費的掛號訂單OrderId爲" + orderNumber);
    }
}
  • 測試用的Controller
package com.zjrcinfo.zjguahao.product.controller;


import com.zjrcinfo.zjguahao.common.web.log.LoggerName;
import com.zjrcinfo.zjguahao.product.service.delay.TestOrderDelayQueue;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;

/**
 * Description:
 * User: zhouzhou
 * Date: 2019-06-03
 * Time: 17:47
 */
@RestController
@Api("緩存測試")
@RequestMapping("/redis")
public class RedisTestController {

    private Logger logger = LoggerFactory.getLogger(LoggerName.REMOTE);

    @Autowired
    private TestOrderDelayQueue testOrderDelayQueue;

    // ------------------------  延時隊列 -------------------
    @ApiOperation("添加定時orderId")
    @RequestMapping(value = "/addDelayOrder/{orderId}/{time}", method = RequestMethod.POST)
    public Object addZset(@PathVariable String orderId, @PathVariable Integer time) {

        boolean flag = testOrderDelayQueue.addJobId(orderId, time);
        return String.format("已經存入了訂單id{%s},延時{%s}秒", orderId, time);

    }

}

5.啓動測試

  • 項目啓動:日誌打印延時機器啓動成功
  • 通過swagger向隊列插入任務

  • 等待五秒,日誌打印,測試成功

 

最後奉上, github代碼示例: https://github.com/zjhzzhouzhou/redis-project  希望幫助到大家.

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