上周的时候有幸和京东大佬来了次线上"交流"(他问我答那种,懂的都懂),由于我下午临时有个会议要参加,原本计划1小时的"交流"缩短到30分钟,前二十分钟聊了聊项目,距离我开会还剩十分钟时,大佬突然问我道:假设有这么一个场景,用户下完单之后没有支付,然后30分钟之后订单自动取消了,你有什么设计思路去实现这个功能呢?
我在那一边盯着手表一边思考这个问题,咦,工作多载,做过订单相关的但没整过支付啊,网上看过一些支付的实现调一些支付宝、微信支付的接口一类的方案,但是大佬的这个问题的重点显然不在问我怎么调API,而是订单超时自动取消啊!大脑快快想。
沉思十几秒左右,脑子里瞬时冒出了第一种方案,完全可以用定时JOB来实现啊,于是整理下思路便说道:"我们可以采用定时JOB方式去数据库中检查订单的状态,大概可以这样设计:1.首先设计一个数据库表,用于存储订单信息,包括订单生成时间、支付状态等字段。2.然后后端开启一个定时JOB,定期(如10秒钟)扫描数据库中的订单。3.对于每个订单,检查其支付状态以及订单生成时间和当前时间相比是否超过了30分钟。4如果订单未支付且时间差超过30分钟,则更新订单状态为已取消,并执行相应的取消逻辑,比如释放库存。"
京东大佬沉默了几秒,没有评价对与错,说道:"这可以作为一种实现方案,有没有更好的方案呢?"
更好的方案??我又开始催促我那每天被工作累死的脑细胞赶紧想了,还好,立马想了一个我自己都觉得不靠谱的方案。
我有点憋笑似的回答到:"也可以不使用定时JOB,用户去查询订单的时候,肯定要请求后端接口么,那时候做处理逻辑,针对性的再去判断自己订单有没有超时,超时的话直接修改状态,增加库存,将最新的结果反馈的页面上,而且服务器还没那么多的压力。"坦白讲,我当时居然还觉得自己的这个方案挺天才,显然,这个答案并不是大佬想听到的答案,还是问我有没有更好的思路。
还剩3分钟会议就要开始了,我实在没时间去想第三种方案了,便和大佬道了歉,得抓紧去开会了,大佬也比较理解,又和我另约了其他的时间再继续沟通。
于是回去开会、继续工作。。。
晚上下班的路上,我静下心来思索着白天的回答,发现自己说的这两种方案真的一个也不靠谱:首先第一种定时任务轮询数据库,为了减少和30分钟的误差,我们必然要把这个轮询订单的间隔时间配置的非常短,几秒那种,京东那么大的公司,每天产生那么都订单,用定时一直去访问DB中订单表,那必然对DB造成的压力太大,甚至会影响到别的业务,性能实在是太低了,怎么可能用这种方案呢? 再说第二种方案,看起来对服务器性能没损耗,而且对用户的感知也做到了打开页面就知道订单取消了,可是有个问题啊,假设用户不打开订单页面咋整,那库存岂不是永远无法释放了?一个用户没有及时打开购物APP看,成千上万个呢?若是京东真是使用的我这第二种"天才方案",那得错过多少假库存不足引起的无法交易呢。 总而言之,两种方案都不是最优解!
反思过后,想了想其实可以使用一些延时处理的策略来实现,于是查了一些资料,下面是我整理的一些设计思路:
- 利用延时队列:DelayQueue:
- DelayQueue是一个无界阻塞队列,其中的元素只能在其延迟期满时才能从队列中获取。
- 将订单作为对象放入DelayQueue,对象的延迟时间设置为30分钟。
- 使用生产者线程将新订单放入DelayQueue。
- 消费者线程从DelayQueue中获取订单,如果订单在30分钟内未被支付,则执行取消操作。
- 定时任务 + 使用Redis的过期键:
- Redis支持设置键的过期时间,当键过期时,Redis会自动删除该键。
- 将订单信息存储为Redis中的键,键的过期时间设置为30分钟。
- 定时扫描Redis,将对DB的压力转移到对Redis上。
- 如果键不存在(即已过期),则返回订单已取消的提示,释放库存。
- 利用消息队列的延时消息:
- 使用如RabbitMQ、Kafka等消息队列系统,它们支持发送延时消息。
- 当订单生成时,发送一条延时为30分钟的消息到消息队列。
- 消费者监听消息队列,当接收到延时消息时,检查对应订单是否已支付。
- 如果未支付,则执行取消订单的逻辑。
- 时间轮算法:
- 时间轮算法是一种高效处理定时任务或延时任务的算法。
- 设计一个时间轮,每个槽位代表一定的时间间隔(如1分钟)。
- 当订单生成时,将其放入对应的时间槽位中。
- 时间轮不断转动,当转到某个槽位时,检查其中的订单是否已支付,未支付则取消。
在实现上述方案时,还需要考虑以下几点:
- 并发控制:确保在检查订单状态和执行取消操作时,订单数据的一致性。
- 事务性:更新订单状态和执行取消操作应在一个事务中完成,确保操作的原子性。
- 通知机制:如果订单被取消,可能需要通知用户或相关系统。
- 日志记录:记录订单取消的原因和时间,方便后续审计和排查问题。