任务场景
12306订单,比如我们买了一张票,一般会给30分钟的支付时间,如果30分钟内没有支付,则系统会自动取消订单,然后释放锁定座位。
那么超时订单有哪几种实现方式呢?
超时订单自动关闭的优雅实现
1、数据库轮询(30S)
订单表(订单ID,状态,创建时间)
轮询数据库会带来什么问题?
- 轮询大部分时间其实是在做无用功,我们假设一张订单是45分钟过期,每分钟我们扫描一次,对这张订单来说,要扫描45次以后,才会检查到这张订单过期,这就意味着数据库的资源(连接,IO)被白白浪费了;
- 处理上的不及时,一个待支付的电影票订单我们假设是12:00:35过期,但是上次扫描的时间是12:00:30,那么这个订单实际的过期时间是什么时候?12:01:30,和我本来的过期时间差了55秒钟。放在业务上,会带来什么问题?这张电影票,假设是最后一张,有个人12:00:55来买票,买得到吗?当然买不到了。那么这张电影票很有可能就浪费了。如果缩短扫描的时间间隔,第一只能改善不能解决,第二,又会对数据库造成更大的压力。
2、使用DelayQueue 实现
DelayQueue: 阻塞队列(先进先出)
- 支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满
- 支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
Delayed接口使对象成为延迟对象,它使存放在DelayQueue类中的对象具有了激活日期。该接口强制实现下列两个方法。
应用重启带来的问题
- 保存在Queue 中的订单会丢失
- 已过期的订单不会被处理
解决之道
从系统伸缩性角度考虑:应用集群化了怎么办?
集群化了会带来什么问题?应用之间会相互抢夺订单,特别是在应用重启的时候,重新启动的那个应用会把不属于自己的订单,也全部加载到自己的队列里去,一是造成内存的浪费,二来会造成订单的重复处理,而且加大了数据库的压力。
解决方案:
1、 给每台服务器编号,然后在订单表里登记每条订单的服务器编号;2,更简单的,在订单表里登记每台服务器的IP地址,修改相应的sql语句即可。
几个问题:如果有一台服务器挂了怎么办?运维吃干饭的吗?服务器挂了赶紧启动啊。如果是某台服务器下线或者宕机,起不来怎么办?这个还是还是稍微有点麻烦,需要人工干预一下,手动把库里的每条订单数据的服务器编号改为目前正常的服务器的编号,不过也就是一条sql语句的事,然后想办法让正常的服务器进行处理(重启正常的服务器)。
能不能同时解决伸缩性和扩展性问题?
用delayqueue是队列,分布式情况我们何不直接引入消息中间件呢?一举解决我们应用的伸缩性和扩展性问题
我们可以使用ActiveMQ的延迟和定时投递
3、ActiveMQ延迟队列
修改配置文件(activemq.xml),增加延迟和定时投递支持
- <broker xmlns="http://activemq.apache.org/schema/core"
- brokerName="localhost" dataDirectory="${activemq.data}"
- schedulerSupport="true">
需要把几个描述消息定时调度方式的参数作为属性添加到消息,broker端的调度器就会按照我们想要的行为去处理消息。
一共有4个属性:
- AMQ_SCHEDULED_DELAY :延迟投递的时间
- AMQ_SCHEDULED_PERIOD :重复投递的时间间隔
- AMQ_SCHEDULED_REPEAT:重复投递次数
- AMQ_SCHEDULED_CRON:Cron表达式
ActiveMQ也提供了一个封装的消息类型:org.apache.activemq.ScheduledMessage,可以使用这个类来辅助设置,使用例子如:延迟60秒
- MessageProducer producer = session.createProducer(destination);
- TextMessage message = session.createTextMessage("test msg");
- long time = 60 * 1000;
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, time);
- producer.send(message);
例子:延迟30秒,投递10次,间隔10秒:
- TextMessage message = session.createTextMessage("test msg");
- long delay = 30 * 1000;
- long period = 10 * 1000;
- int repeat = 9;
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);
- message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);
- message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);
- 也可使用 CRON 表达式,如message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
也可使用 CRON 表达式,如message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");
4、Redis有序集合
Redis 有序集合
Redis sortedSet 集合(sorted set 也叫zset) 是一个有序集合,每个元素(member)都关联了一个score,可以通过score排序获取集合中的值。
zset常用命令:
- 添加元素:zadd key score member[[score member]]
- 按顺序查询元素:zrange key start stop [withscores]
- 查询元素score:zscore key member
- 移除元素: zrem key member 【member ...】
将订单超时时间戳(long)与订单号分别设置为score与member,系统扫描第一个元素判断定是否超时,拿到分数最小的,超时时间最早的。判断与当前时间戳的关系
生产者
消费者
5、Redis Set 集合
将订单延迟时间的秒级时间戳设置为set集合的key,value 为订单ID
sadd set的key item的项值,item项可以有多个
按秒级的时间进行聚合,即 key为时间戳,里面可以由多个ID
总结
1、DB轮询
- 优点: 实现简单、无 技术难点、异常恢复、支持分布式/进群环境
- 缺点:影响数据库性能、时效性差、效率低
2、DelayedQueue
- 优点: 实现简单、性能较好
- 缺点: 异常恢复困难、分布式/集群实现坤丹
3、redis
优点 ; 解耦、异常恢复、支持分布式/集群环境
缺点
- 增加redis维护、占用宽带
- 有序 集合缺点: 当sortedSet集合中元素过多时,插入性能降低
- Set集合缺点: 已经超时未处理的订单不好处理