Redis實戰(12)-基於Redis的Key失效和定時任務調度實現訂單超時未支付自動失效(延時隊列)

概述:本系列博文所涉及的相關內容來源於debug親自錄製的實戰課程:緩存中間件Redis技術入門與應用場景實戰(SpringBoot2.x + 搶紅包系統設計與實戰),感興趣的小夥伴可以點擊自行前往學習(畢竟以視頻的形式來掌握技術 會更快!) 文章所屬技術專欄:緩存中間件Redis技術入門與實戰

摘要:“商城平臺用戶下單”這一業務場景相信很多小夥伴並不陌生,在正常的情況下,用戶在提交完訂單/下完單之後,應該是前往“收銀臺”選擇支付方式進行支付,之後只需要提供相應的密碼即可完成整個支付過程;然而,“非正常的情況”也總是會有的,即用戶在提交完訂單之後在“規定的時間內”遲遲沒有支付,這個時候我們就需要採取一些措施了,本文就是講解如何基於Redis的Key失效,即TTL + 定時任務調度 實現這一業務場景的功能。

內容:前面篇章中,我們基本上給各位小夥伴介紹完了緩存中間件Redis各種典型且常見的數據結構及其典型的應用場景,這些數據結構包括字符串String、列表List、集合Set、有序集合SortedSet以及哈希Hash,其常見的業務場景包括“實體對象信息的存儲”、“商品列表有序存儲”、“List隊列特性實現消息的廣播通知”、“重複提交”、“隨機獲取試卷題目列表”、“排行榜”以及“數據字典的實時觸發緩存存儲”,可以說,真正地做到了技術的學以致用!

本文我們將給大家介紹一個目前在“電商平臺”比較常見、典型的業務場景,即“用戶在下單之後,超時未支付而自動失效該訂單”的功能!對於這一功能的實現,如果有小夥伴擼過我的那套“消息中間件RabbitMQ實戰視頻教程”的課程,那麼肯定知曉如何實現!沒錯,就是利用“死信隊列”來實現的!

而現在,我們要介紹的並非RabbitMQ的死信隊列,而是想如何基於緩存中間件Redis來實現這一功能!我們知道在使用Redis的緩存功能時,無非就是SET Key Value,這是最爲“常規的操作”,但千萬要記住,Redis提供的功能的還遠不止於此,像設置Key的失效時間,即SET Key Value TTL,其作用就是“設置某個Key的值爲Value,同時設置了它在緩存Redis中能存活的時間”。

有些小夥伴聽到“能存活的時間”,可能腦袋會靈機一動,“這不跟RabbitMQ死信隊列中的消息能存活的時間TTL差不多是一個意思嗎?”哈哈,確實是差不多那個意思,我們只需要將用戶下單成功得到的“訂單號”塞入緩存Redis,並設置其TTL即可(就像我們在RabbitMQ的死信隊列設置“訂單號”這一消息的TTL一樣!)

但有這個還不夠,因爲 “Redis的Key的TTL一到就自動從緩存中剔除” 這個過程是Redis底層自動觸發的,而在我們的程序、代碼裏是完全感知不到的,因爲我們得藉助某種機制來幫助我們主動地去檢測Redis緩存中那些Key已經失效了,而且,我們希望這種檢測可以是“近實時”的

故而我們將基於Redis的Key失效/存活時間TTL + 定時任務調度(用於主動定時的觸發,去檢測緩存Redis那些失效了的Key,而且希望Cron可以設置得足夠合理,實現“近實時”的功效)!

現在我們基本已經確實了這一功能的實現方案了,等待着我們要去做的無非就是擼碼實戰了,當然啦,在開始施展我們的代碼才華之前,我們有必要給大家貼一下這一業務場景的整體業務流程圖!整個業務流程可以說包含兩大功能模塊,即“用戶提交訂單/下訂單模塊”、“定時任務調度定時檢測Redis的訂單存活時間+自動失效訂單記錄模塊”

一、用戶提交訂單的核心流程

對於“用戶下訂單”的功能模塊,其實也不是很複雜,就是將前端用戶提交過來的信息經過處理生成相應的訂單號,然後將該訂單記錄插入數據庫、插入緩存Redis,並設置對應的Key的存活時間TTL,其完整的業務流程如下圖所示:

 

下面我們就進入代碼實戰環節。

(1)工欲善其事,必先利其器,我們首先仍然需要建立一張數據庫表user_order,用於記錄用戶的下單記錄,其DDL定義如下所示:

CREATE TABLE `user_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用戶id',
  `order_no` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '訂單編號',
  `pay_status` tinyint(255) DEFAULT '1' COMMENT '支付狀態(1=未支付;2=已支付;3=已取消)',
  `is_active` tinyint(255) DEFAULT '1' COMMENT '是否有效(1=是;0=否)',
  `order_time` datetime DEFAULT NULL COMMENT '下單時間',
  `update_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COMMENT='用戶下單記錄表';

然後,基於Mybatis的逆向工程或者代碼生成器生成該數據庫表的實體類Entity、Mapper操作接口及其對應的用於寫動態SQL的Mapper.xml,在這裏我們只貼出兩個Mapper操作接口吧,如下所示:  

//TODO:查詢有效+未支付的訂單列表    
List<UserOrder> selectUnPayOrders();

//TODO:失效訂單記錄
int unActiveOrder(@Param("id") Integer id);

其對應的動態SQL是在對應的Mapper.xml中實現的,如下所示:  

<!--查詢未支付的用戶訂單列表-->
  <select id="selectUnPayOrders" resultType="com.boot.debug.redis.model.entity.UserOrder">
    SELECT
        <include refid="Base_Column_List"/>
    FROM
        user_order
    WHERE
        is_active = 1
    AND pay_status = 1
    ORDER BY
        order_time DESC
  </select>

  <!--失效訂單-->
  <update id="unActiveOrder">
    update user_order
    set is_active = 0
    where id = #{id} and is_active = 1 and pay_status = 1
  </update>

(2)之後,我們開發一個UserOrderController,用於接收前端過來的請求參數,並在UserOrderService實現“用戶下單”的整個業務邏輯,其完整的源代碼如下所示:  

/**用戶下單controller
 * @Author:debug (SteadyJack)
 * @Link: weixin-> debug0868 qq-> 1948831260**/
@RestController
@RequestMapping("user/order")
public class UserOrderController {
    private static final Logger log= LoggerFactory.getLogger(UserOrderController.class);

    @Autowired
private UserOrderService userOrderService;

    //下單
    @RequestMapping(value = "put",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public BaseResponse put(@RequestBody @Validated UserOrder userOrder, BindingResult result){
        String checkRes=ValidatorUtil.checkResult(result);
        if (StrUtil.isNotBlank(checkRes)){
            return new BaseResponse(StatusCode.InvalidParams.getCode(),checkRes);
        }
        BaseResponse response=new BaseResponse(StatusCode.Success);
        try {
            log.info("--用戶下單:{}",userOrder);

            String res=userOrderService.putOrder(userOrder);
            response.setData(res);
        }catch (Exception e){
            log.error("--用戶下單-發生異常:",e.fillInStackTrace());
            response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
        }
        return response;
    }
}

(3)其對應的userOrderService.putOrder(userOrder);便是真正實現業務服務邏輯的地方,如下所示:  

/**用戶下單service
 * @Author:debug (SteadyJack)
 * @Link: weixin-> debug0868 qq-> 1948831260**/
@EnableScheduling
@Service
public class UserOrderService {
    private static final Logger log= LoggerFactory.getLogger(UserOrderService.class);

    //雪花算法生成訂單編號
    private static final Snowflake SNOWFLAKE=new Snowflake(3,2);

    //存儲至緩存的用戶訂單編號的前綴
    private static final String RedisUserOrderPrefix="SpringBootRedis:UserOrder:";

    //用戶訂單失效的時間配置 - 30min
private static final Long UserOrderTimeOut=30L;

    @Autowired
    private UserOrderMapper userOrderMapper;

    @Autowired
private StringRedisTemplate stringRedisTemplate;

    /**下單服務
     * @param entity
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public String putOrder(UserOrder entity) throws Exception{
        //用戶下單-入庫
        String orderNo=SNOWFLAKE.nextIdStr();
        entity.setOrderNo(orderNo);
        entity.setOrderTime(DateTime.now().toDate());
        int res=userOrderMapper.insertSelective(entity);

        if (res>0){
            //TODO:入庫成功後-設定TTL 插入緩存 - TTL一到,該訂單對應的Key將自動從緩存中被移除(間接意味着:延遲着做某些時間)
            stringRedisTemplate.opsForValue().set(RedisUserOrderPrefix+orderNo,entity.getId().toString(),UserOrderTimeOut, TimeUnit.MINUTES);
        }
        return orderNo;
}
}

(4)至此,“用戶下單”的功能模塊我們就擼完了,下面我們用Postman測試一波吧,如下幾張圖所示:

 

 

二、定時任務調度定時檢測Redis的訂單存活時間 + 自動失效訂單記錄模塊

對於“定時任務調度定時檢測Redis的訂單存活時間 + 自動失效訂單記錄模塊”的功能模塊,同理也不是很複雜,無非就是開啓一個定時任務調度,拉取出數據庫DB中“有效且未支付的訂單列表”,然後逐個遍歷,前往緩存Redis查看該訂單編號對應的Key是否還存在,如果不存在,說明TTL早已到期,也就間接地說明了用戶在規定的時間TTL內沒有完成整個支付流程,此時需要前往數據庫DB中失效其對應的訂單記錄,其完整的業務流程如下圖所示:

 

同理,我們基於此流程圖進入代碼實戰環節!

(1)我們在UserOrderService中定義一個定時任務,並設置該定時頻率Cron爲每5分鐘執行一次,其業務邏輯即爲上面流程圖所繪製的,完整的代碼如下所示:

    //TODO:定時任務調度-拉取出 有效 + 未支付 的訂單列表,前往緩存查詢訂單是否已失效
    @Scheduled(cron = "0 0/5 * * * ?")
    @Async("threadPoolTaskExecutor")
    public void schedulerCheckOrder(){
        try {
            List<UserOrder> list=userOrderMapper.selectUnPayOrders();
            if (list!=null && !list.isEmpty()){

                list.forEach(entity -> {
                    final String orderNo=entity.getOrderNo();
                    String key=RedisUserOrderPrefix+orderNo;
                    if (!stringRedisTemplate.hasKey(key)){
                        //TODO:表示緩存中該Key已經失效了,即“該訂單已經是超過30min未支付了,得需要前往數據庫將其失效掉”
                        userOrderMapper.unActiveOrder(entity.getId());
                        log.info("緩存中該訂單編號已經是超過指定的時間未支付了,得需要前往數據庫將其失效掉!orderNo={}",orderNo);
                    }
                });
            }
        }catch (Exception e){
            log.error("定時任務調度-拉取出 有效 + 未支付 的訂單列表,前往緩存查詢訂單是否已失效-發生異常:",e.fillInStackTrace());
        }
    }

其中的@Async("threadPoolTaskExecutor"),代表該定時任務將採用“異步”+“線程池~多線程”的方式進行執行,其配置如下所示:  

/**多線程配置
 * @Author:debug (SteadyJack)
 * @Link: weixin-> debug0868 qq-> 1948831260 **/
public class ThreadConfig {

    @Bean("threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor(){
        ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(8);
        executor.setKeepAliveSeconds(10);
        executor.setQueueCapacity(8);

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

(3)之後,便是啓動項目,等待5min,即可看到奇蹟的發生!如下圖所示:

 

當然啦,如果在TTL(即30min)內,如果用戶完成了支付,那麼pay_status將不再爲1,即定時任務也就不會拉取到該訂單記錄了!如果某一訂單記錄被失效了,那麼is_active將變爲0,即定時任務在下一次Cron到來時也就不會拉取到該訂單記錄了!

如下圖所示爲被拉取到的“未支付+有效”的訂單列表在指定的TTL時間內沒有支付後採取的“強硬措施”,即所謂的“失效該訂單記錄”!

 

至此,我們已經基於 定時任務調度 + Redis的Key失效TTL 相結合實現了“商城平臺中用戶下單後在指定的時間TTL內沒有完成支付而自定失效該訂單記錄”的功能!

當然啦,這僅僅是一種實現方式,其性能還是有待考究的,畢竟定時任務需要從DB中查詢未失效的數據,如果這個數據量過大,那麼其佔據的內存明顯將很大,更有甚者可能會在某一時刻出現OOM的蛋疼情況

好了,本篇文章我們就介紹到這裏了,建議各位小夥伴一定要照着文章提供的樣例代碼擼一擼,只有擼過才能知道這玩意是咋用的,否則就成了“空談者”!

對Redis相關技術棧以及實際應用場景實戰感興趣的小夥伴可以前往debug搭建的技術社區的課程中心進行學習觀看:程序員實戰基地 !其他相關的技術,感興趣的小夥伴可以關注底部debug的技術公衆號,一起學習、共同成長!

補充:

1、本文涉及到的相關的源代碼可以到此地址,check出來進行查看學習:https://gitee.com/steadyjack/SpringBootRedis

2、目前debug已將本文所涉及的內容整理錄製成視頻教程,感興趣的小夥伴可以前往觀看學習:https://edu.csdn.net/course/detail/26619

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