前言
咱们聊聊那些在开发过程中经常遇到的延时处理需求吧。比如说,网购时那些迟迟不付款的订单,或者是社交软件里那些需要稍后处理的消息,再或者是金融交易中那些需要等待确认的交易。这些都是咱们得搞定的活儿。
不过,很多时候,咱们看到的文章或者面试题,好多都是为技术而技术,感觉就像是空中楼阁,只谈理论,不接地气。很多人可能连自己说的方案能不能行都不知道,就敢拿出来说。说实话,这种只谈技术不谈实际应用的情况,真的挺让人头疼的。
所以,今天咱们就来点实在的。咱们不聊那些高大上的理论,而是聊聊在实际工作中,这些延时处理需求到底是怎么搞定的。这里采用大家都比较熟悉的一个业务场景:超时订单失效来作为需求设计,咱们会分享一些公司中真实落地的案例,看看这些方案在真实环境中到底是怎么落地的,希望能给咱们的读者朋友们带来一些实实在在的帮助和启发。
需求背景
想象一下,顾客在电商平台上选了一堆东西,结果过了半天也没付款,这订单就悬那儿了。这时候,咱们得有个机制来自动处理这些订单,比如取消订单,释放库存,或者给顾客发个提醒,看他们是否还要继续购买。这事儿得做得既及时又高效,不能让顾客等太久,也不能让系统因为处理这些订单而变慢。设计的时候,咱们得考虑怎么设置合理的超时时间,怎么优雅地处理订单取消流程,还得确保这个处理过程不影响其他用户的购物体验。简单来说,就是得让系统既能快速反应,又得稳定可靠。
实现方案一:懒延时被动关闭
首先,客户端(比如一个手机App或者网页)在拉取订单的时候,会同时从服务器获取当前的时间戳和订单的超时时间。客户端拿到这些信息后,会自己计算:当前时间减去订单的超时时间,看结果是不是大于订单的创建时间。如果是,那就说明订单已经超时了。客户端就会在界面上给用户一个明确的提示,比如显示“订单已超时”或者用不同的颜色标记(或者未支付订单直接消失),让用户知道这个订单已经不能继续付款了。同时,客户端会悄悄地(隐式地)向服务器发送一个请求,告诉服务器:“嘿,这个订单超时了。”这个请求不需要用户干预,是自动进行的。服务器收到客户端的超时请求后,就会进行相应的处理,比如更新数据库中的订单状态,标记为“已关闭”或者“超时”,并释放订单占用的库存或者资源。后续查询订单列表直接就可以基于订单状态来过滤掉该超时订单。
这个方案的好处是减轻了服务器的负担,因为服务器不需要定时去检查每个订单的状态。适用于数据量比较小的业务场景,总的来说,这个方案就是让客户端多做一些工作,服务器端则更轻松一些。但是这个方案有可能导致大量脏数据堆积问题,所以可以再配合一个方案二来进一步优化,这样既能提高系统的响应速度,也能节省服务器资源。不过,实现的时候要注意各种异常情况的处理,确保系统的稳定性和准确性。
实现方案二:定时任务周期轮询数据库批处理
每当用户下单时,系统会计算出一个超时时间戳并保存到数据库中;接着,通过xxl-job设置一个定时任务,每隔5秒,系统就会查询数据库中所有超时时间戳小于当前时间并且订单状态为待支付的订单( order_status = '待支付' && pay_expire_time<now);通过对超时时间戳字段建立了索引,这样的查询既快速又高效,即使面对数百万订单也能稳定运行;一旦检测到超时订单,系统会自动执行后续处理,如更新订单状态、释放库存等,同时,整个过程会有详细的日志记录和监控,如果发现任务执行失败或者超时,及时发送报警通知给运维人员。并且在正式环境部署之前,可以先在小范围内进行灰度发布,观察系统的表现,确保方案的可行性和稳定性。
这个基于定时轮询处理超时未支付订单的方案适用于那些数据量较大,对订单处理时效性要求不是非常严格的业务场景,特别是在系统资源有限或需要简单处理机制的情况下,它能够提供一种易于实现、监控和维护的解决方案。尽管存在一定的处理延迟,并且可能在高流量时段无法立即响应所有订单,但通过合理设置轮询间隔,可以有效平衡服务器负载和业务需求,确保系统的稳定性和可靠性。不过,对于需要快速响应或处理大量订单的系统,可能需要考虑更高效的处理策略,如消息队列或事件驱动模型,以提高处理效率和实时性。
实现方案三:MQ延时消息
利用消息队列(MQ、或者Redis的Stream流)的延时消息特性来处理超时未支付订单的问题。当用户创建订单时,系统会向MQ发送一条设定了延时时间的消息,这个延时时间与订单的超时时间相匹配。如果到了延时时间用户还未支付,MQ会将消息发送给消费者,由消费者来执行超时订单的相关处理逻辑。
这个方案适用于搭建了 RocketMQ的公司,想利用 RocketMQ的延时消息来实现该功能,优点在于利用中间件 MQ的特性,避免业务端自己实现,并且自带有持久化、重试机制等保证消息的正确消费,但是如果使用开源的 Apache RocketMQ 4.x版本,消息延时只有特定的 18个级别,所以业务的超时时间要和 RocketMQ的延时级别相匹配才能使用,并且对于开源版的 RocketMQ没有删除延时消息的功能,因此需要对每条消息都做超时判断,增加了很多无效的数据处理,同一个时刻大量消息会导致消息延迟:定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。并且每个订单需要新增一个定时消息,且不会马上消费,给MQ带来很大的存储成本。
不推荐:Redis的过期监听
这个方案表面看起来没问题,但是在实际生产上不推荐,我们来看下Redis过期时间的原理
每当我们对一个key设置了过期时间,Redis就会把该key带上过期时间,存到过期字典中,在redisDb中通过expires字段维护:
typedef struct redisDb {
dict *dict; /* 维护所有key-value键值对 */
dict *expires; /* 过期字典,维护设置失效时间的键 */
....
} redisDb;
过期字典本质上是一个链表,每个节点的数据结构结构如下:
- key是一个指针,指向某个键对象。
- value是一个long long类型的整数,保存了key的过期时间。
Redis主要使用了定期删除和惰性删除策略来进行过期key的删除
- 定期删除:每隔一段时间(默认100ms)就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。之所以这么做,是为了通过限制删除操作的执行时长和频率来减少对cpu的影响。不然每隔100ms就要遍历所有设置过期时间的key,会导致cpu负载太大。
- 惰性删除:不主动删除过期的key,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key。惰性删除有一个问题,如果这个key已经过期了,但是一直没有被访问,就会一直保存在数据库中。
从以上的原理可以得知,Redis过期删除是不精准的,在订单超时处理的场景下,惰性删除基本上也用不到,无法保证key在过期的时候可以立即删除,更不能保证能立即通知。如果订单量比较大,那么延迟几分钟也是有可能的。
Redis过期通知也是不可靠的https://redis.io/docs/manual/keyspace-notifications/ Redis在过期通知的时候,如果应用正好重启了,那么就有可能通知事件就丢了,会导致订单一直无法关闭,有稳定性问题。如果一定要使用Redis过期监听方案,建议再通过定时任务做补偿机制。
总结
- 如果数据量较小,推荐使用懒延时 + 定期数据库轮询标注的方案,提高系统的响应速度,也能节省服务器资源
- 如果对于超时精度比较高,超时时间在24小时内,且不会有峰值压力的场景,推荐使用RocketMQ的定时消息解决方案。
- 如果对于超时精度没有那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的跑批解决方案。