Redis實戰(zhàn)(12)-基于Redis的Key失效和定時任務調(diào)度實現(xiàn)訂單支付超時自動失效(延時隊列)

作者: 修羅debug
版權聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權協(xié)議,轉載請附上原文出處鏈接和本聲明。



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

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

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

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

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

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

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

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

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

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


下面我們就進入代碼實戰(zhàn)環(huán)節(jié)。

(1)工欲善其事,必先利其器,我們首先仍然需要建立一張數(shù)據(jù)庫表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 '支付狀態(tài)(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的逆向工程或者代碼生成器生成該數(shù)據(jù)庫表的實體類Entity、Mapper操作接口及其對應的用于寫動態(tài)SQL的Mapper.xml,在這里我們只貼出兩個Mapper操作接口吧,如下所示:  

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

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

其對應的動態(tài)SQL是在對應的Mapper.xml中實現(xiàn)的,如下所示:  

<!--查詢未支付的用戶訂單列表-->
<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)之后,我們開發(fā)一個UserOrderController,用于接收前端過來的請求參數(shù),并在UserOrderService實現(xiàn)“用戶下單”的整個業(yè)務邏輯,其完整的源代碼如下所示:  

/**用戶下單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("--用戶下單-發(fā)生異常:",e.fillInStackTrace());
response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
}
return response;
}
}

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

/**用戶下單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測試一波吧,如下幾張圖所示:



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

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


同理,我們基于此流程圖進入代碼實戰(zhàn)環(huán)節(jié)!

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

    //TODO:定時任務調(diào)度-拉取出 有效 + 未支付 的訂單列表,前往緩存查詢訂單是否已失效
@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已經(jīng)失效了,即“該訂單已經(jīng)是超過30min未支付了,得需要前往數(shù)據(jù)庫將其失效掉”
userOrderMapper.unActiveOrder(entity.getId());
log.info("緩存中該訂單編號已經(jīng)是超過指定的時間未支付了,得需要前往數(shù)據(jù)庫將其失效掉!orderNo={}",orderNo);
}
});
}
}catch (Exception e){
log.error("定時任務調(diào)度-拉取出 有效 + 未支付 的訂單列表,前往緩存查詢訂單是否已失效-發(fā)生異常:",e.fillInStackTrace());
}
}

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

/**多線程配置
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260 **/
@Configuration
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,即可看到奇跡的發(fā)生!如下圖所示:


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

如下圖所示為被拉取到的“未支付+有效”的訂單列表在指定的TTL時間內(nèi)沒有支付后采取的“強硬措施”,即所謂的“失效該訂單記錄”!


至此,我們已經(jīng)基于 定時任務調(diào)度 + Redis的Key失效TTL 相結合實現(xiàn)了“商城平臺中用戶下單后在指定的時間TTL內(nèi)沒有完成支付而自定失效該訂單記錄”的功能!

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

對Redis相關技術棧以及實際應用場景實戰(zhàn)感興趣的小伙伴可以前往Debug搭建的技術社區(qū)的課程中心進行學習觀看:https://www.fightjava.com/web/index/course/detail/12 !

其他相關的技術,感興趣的小伙伴可以關注底部Debug的技術公眾號,或者加Debug的微信,拉你進“微信版”的真正技術交流群!一起學習、共同成長!

補充:

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

https://gitee.com/steadyjack/SpringBootRedis

2、目前Debug已將本文所涉及的內(nèi)容整理錄制成視頻教程,感興趣的小伙伴可以前往觀看學習:https://www.fightjava.com/web/index/course/detail/12

3、關注一下Debug的技術微信公眾號,最新的技術文章、課程以及技術專欄將會第一時間在公眾號發(fā)布哦!