队列的实现方式
1.应用场景
目前系统中有很多需要用到延时处理的功能:支付超时取消、排队超时、短信、微信等提醒延迟发送、token刷新、会员卡过期等等。通过延时处理,极大的节省系统的资源,不必轮询数据库处理任务。
目前大部分功能通过定时任务完成,定时任务还分使用quartz及xxljob两种类型轮询时间短,每秒执行一次,对数据库造成一定的压力,并且会有1秒的误差。轮询时间久,如30分钟一次,03:01插入一条数据,正常3:31执行过期,但是3:30执行轮询时,扫描3:00-3:30的数据,是扫描不到3:31的数据的,需要4:00的时候才能扫描到,相当于多延迟了29分钟!
2.延时处理方式调研
1.DelayQueue
1.实现方式:
jvm提供的延迟阻塞队列,通过优先级队列对不同延迟时间任务进行排序,通过condition进行阻塞、睡眠dealy时间 获取延迟任务。
当有新任务加入时,会判断新任务是否是第一个待执行的任务,若是,会解除队列睡眠,防止新加入的元素时需要执行的元素而不能正常被执行线程获取到。
2.存在的问题:
1.单机运行,系统宕机后,无法进行有效的重试
2.没有执行记录和备份
3.没有重试机制
4.系统重启时,会将任务清空!
5.不能分片消费
3.优势:实现简单,无任务时阻塞,节省资源,执行时间准确
2.延迟队列mq
实现方式:依赖mq,通过设置延迟消费时间,达到延迟消费功能。像rabbitMq、jmq都可以设置延迟消费时间。RabbitMq通过将消息设置过期时间,放入死信队列进行消费实现。
存在的问题:
1.时间设置不灵活,每个queue是固定的到期时间,每次新创建延时队列,需要创建新的消息队列
优点:依靠jmq,可以有效的监控、消费记录、重试,具备多机同时消费能力,不惧怕宕机
3.定时任务
通过定时任务轮询符合条件的数据
缺点:
1.必须要读业务数据库,对数据库造成一定的压力,
2.存在延时
3.一次扫描数据量过大时,占用过多的系统资源。
4. 无法分片消费
优点:
1.消费失败后,下次还能继续消费,具备重试能力,
2.消费能力稳定
4.redis
任务存储在redis中,使用redis的 zset队列根据score进行排序,程序通过线程不断获取队列数据消费,实现延时队列
优点:
1、查询redis相比较数据库快,set队列长度过大,会根据跳表结构进行查询,效率高
2、redis可根据时间戳进行排序,只需要查询当前时间戳内的分数的任务即可
3、无惧机器重启
4、分布式消费
缺点:
1.受限于redis性能,并发10W
2.多个命令无法保证原子性,使用lua脚本会要求所有数据都在一个redis分片上。
5. 时间轮
通过时间轮实现的延迟任务执行,也是基于jvm单机运行,如kafka、netty都有实现时间轮,redisson的看门狗也是通过netty的时间轮实现的。
缺点:不适合分布式服务的使用,宕机后,会丢失任务。