转自:https://www.cnblogs.com/rjzheng/p/8972725.html
延时任务(eg:订单超时未支付):延时任务在某事件触发后一段时间内执行,没有执行周期
1.时间论算法
时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)
实现
我们用Netty的HashedWheelTimer来实现
1 public class MyTimerTaskTest { 2 static class MyTimerTask implements TimerTask{ 3 boolean flag; 4 5 public MyTimerTask(boolean flag) { 6 this.flag = flag; 7 } 8 9 @Override 10 public void run(Timeout timeout) throws Exception { 11 System.out.println("要去数据库删除订单了。。。。。。。。。。。。"); 12 } 13 } 14 15 public static void main(String[] args) { 16 MyTimerTask timerTask = new MyTimerTask(true); 17 Timer timer = new HashedWheelTimer(); 18 timer.newTimeout(timerTask, 5, TimeUnit.SECONDS); 19 int i = 1; 20 while (timerTask.flag){ 21 try { 22 Thread.sleep(1000); 23 }catch (Exception e){ 24 e.printStackTrace(); 25 } 26 System.out.println(i + "秒过去了。。。。。。。。。。。"); 27 i++; 28 } 29 } 30 31 }
优缺点
优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
2.redis缓存
利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值 k(score):订单超时时间戳 v(member):订单号
1 private static JedisSentinelPool jedisPool = new JedisSentinelPool("mymaster",getSentinalSet(), "123456"); 2 3 public static Jedis getJedis(){ 4 return jedisPool.getResource(); 5 } 6 7 //生产者 生成5个订单 8 public void productionDelayMessage(){ 9 for (int i = 0; i < 5; i++){ 10 //延迟三秒 11 Calendar call = Calendar.getInstance(); 12 call.add(Calendar.SECOND, 3); 13 int second3later = (int)(call.getTimeInMillis()/1000); 14 Jedis jedis = RedisTest.getJedis(); 15 jedis.zadd("order", second3later, "OID1000" + i); 16 System.out.println(System.currentTimeMillis() + "redis 生成了一个订单任务" + "OID1000" + i); 17 } 18 } 19 20 //消费者 取订单 21 public void consumerDelayMessage(){ 22 Jedis jedis = RedisTest.getJedis(); 23 while (true){ 24 Set<Tuple> items = jedis.zrangeWithScores("order",0,1); 25 if (items == null || items.isEmpty()) { 26 System.out.println("当前没有等待任务"); 27 try { 28 Thread.sleep(500); 29 } catch (Exception e) { 30 e.printStackTrace(); 31 } 32 continue; 33 } 34 int score = (int)((Tuple)items.toArray()[0]).getScore(); 35 Calendar cal = Calendar.getInstance(); 36 int nowSecond = (int)(cal.getTimeInMillis() / 1000); 37 if (nowSecond > score){ 38 String oid = ((Tuple)items.toArray()[0]).getElement(); 39 jedis.zrem("order",oid)41 System.out.println("消费者消费了一个订单任务 " + oid); 42 } 43 } 44 } 45 46 public static void main(String[] args) { 47 RedisTest redisTest = new RedisTest(); 48 redisTest.productionDelayMessage(); 49 redisTest.consumerDelayMessage(); 50 } 51 52 private static Set<String> getSentinalSet(){ 53 Set<String> set = new HashSet<>(); 54 set.add("192.168.10.251:26379"); 55 set.add("192.168.10.253:26379"); 56 set.add("192.168.10.254:26379"); 57 return set; 58 }
在高并发条件下,多消费者会取到同一个订单号
private static final int threadNum = 10; private static CountDownLatch cdl = new CountDownLatch(threadNum); static class DelayMessage implements Runnable{ @Override public void run() { try { cdl.await();//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 } catch (InterruptedException e) { e.printStackTrace(); } RedisTest test = new RedisTest(); test.consumerDelayMessage(); } } public static void main(String[] args) { RedisTest test = new RedisTest(); test.productionDelayMessage(); for (int i = 0; i < threadNum; i++){ new Thread(new DelayMessage()).start(); cdl.countDown();//将count值减一 } }
解决方案
*(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。
* (2)对ZREM的返回值进行判断,只有大于0的时候,才消费数据
Long num = jedis.zrem("order",oid); if (num != null && num > 0) System.out.println("消费者消费了一个订单任务 " + oid);
3.使用消息队列 我们可以采用rabbitMQ的延时队列。
RabbitMQ具有以下两个特性,可以实现延迟队列 RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可
选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。
结合以上两个特性,就可以模拟出延迟消息的功能,具体的,我改天再写一篇文章,这里再讲下去,篇幅太长。
优缺点
优点: 高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
缺点:本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高 4.JDK的延迟队列 该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对
象,是必须实现Delayed接口的。
其中Poll():获取并移除队列的超时元素,没有则返回空
take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
优缺点
优点:效率高,任务触发时间延迟低。
缺点:
(1)服务器重启后,数据全部消失,怕宕机
(2)集群扩展相当麻烦
(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
(4)代码复杂度较高
5.数据库轮询 通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作
优缺点
优点:简单易行,支持集群操作
缺点:
(1)对服务器内存消耗大
(2)存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟
(3)假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大