Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

商城平臺用戶下單"這一業務場景相信很多小夥伴並不陌生,在正常的情況下,用戶在提交完訂單/下完單之後,應該是前往"收銀臺"選擇支付方式進行支付,之後只需要提供相應的密碼即可完成整個支付過程;然而,"非正常的情況"也總是會有的,即用戶在提交完訂單之後在"規定的時間內"遲遲沒有支付,這個時候我們就需要採取一些措施了,本文就是講解如何基於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,其完整的業務流程如下圖所示:


Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

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

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

<code>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='用戶下單記錄表';/<code>

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

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

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

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

<code> 

<select>
SELECT
<include>
FROM
user_order
WHERE
is_active = 1
AND pay_status = 1
ORDER BY
order_time DESC
/<select>


<update>
update user_order
set is_active = 0
where id = #{id} and is_active = 1 and pay_status = 1
/<update>/<code>

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

<code>/**用戶下單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;
}
}/<code>

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

<code>/**用戶下單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;
}
}/<code>

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


Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效


Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

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

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


Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

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

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

<code>//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());
}
}/<userorder>/<code>

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

Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

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

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

Redis實戰(12)-基於Key失效和定時任務實現訂單支付超時自動失效

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

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

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

其他相關的技術,感興趣的小夥伴可以私信Debug!


分享到:


相關文章: