概述:本系列博文所涉及的相关内容来源于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 selectUnPayOrders();
//TODO:失效订单记录
int unActiveOrder(@Param("id") Integer id);
其对应的动态SQL是在对应的Mapper.xml中实现的,如下所示:
update user_order
set is_active = 0
where id = #{id} and is_active = 1 and pay_status = 1
(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 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