Java最新java实现延时处理(1),绝对干货分享

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

image.png

  • RabbitMQ实战指南

image.png

  • 手写RocketMQ笔记

image.png

  • 手写“Kafka笔记”

image

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

在这里插入图片描述

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。

这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位)

例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。

如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。

那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)

3.2 实现


我们用Netty的HashedWheelTimer来实现

3.2.1 添加maven依赖

io.netty

netty-all

5.0.0.Alpha2

3.2.2 具体实现

import io.netty.util.HashedWheelTimer;

import io.netty.util.Timeout;

import io.netty.util.Timer;

import io.netty.util.TimerTask;

import java.util.concurrent.TimeUnit;

public class HashedWheelTimerTest {

static class MyTimerTask implements TimerTask{

boolean flag;

public MyTimerTask(boolean flag){

this.flag = flag;

}

public void run(Timeout timeout) throws Exception {

// TODO Auto-generated method stub

System.out.println(“要去数据库删除订单了。。。。”);

this.flag =false;

}

}

public static void main(String[] argv) {

MyTimerTask timerTask = new MyTimerTask(true);

Timer timer = new HashedWheelTimer();

timer.newTimeout(timerTask, 3, TimeUnit.SECONDS);//此处设置在时间轮第几个执行(本代码设置为第3格)

int i = 1;

while(timerTask.flag){

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

System.out.println(i+“秒过去了”);

i++;

}

}

}

测试结果如下:

在这里插入图片描述

3.3 优缺点


  • 优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。

  • 缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

4.redis缓存

======================================================================

对于redis缓存的实现我们有两种方式:

  • 1.利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值

  • 2.使用redis的Keyspace Notifications(键空间机制),就是利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。

4.1 利用redis的zset


添加元素:ZADD key score member [[score member] [score member] …]

按顺序查询元素:ZRANGE key start stop [WITHSCORES]

查询元素score:ZSCORE key member

移除元素:ZREM key member [member …]

下面我通过实操来给大家演示一下

添加单个元素

redis> ZADD page_rank 10 google.com

添加多个元素

redis> ZADD page_rank 9 baidu.com 8 bing.com

(integer) 2

在这里插入图片描述

按顺序查询元素 0 -1表示查询所有

ZRANGE page_rank 0 -1 WITHSCORES

在这里插入图片描述

查询元素的score值

redis> ZSCORE page_rank bing.com

在这里插入图片描述

移除单个元素

redis> ZREM page_rank google.com

在这里插入图片描述

在这里插入图片描述

我们具体将其和和我们要实现的功能联系在一起呢?

在这里插入图片描述

4.1.1 添加redis依赖

org.springframework.boot

spring-boot-starter-redis

1.4.7.RELEASE

具体的实现如下:

import java.util.Calendar;

import java.util.Set;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.Tuple;

public class AppTest {

private static final String ADDR = “192.168.1.2”;

private static final int PORT = 6379;

private static JedisPool jedisPool = new JedisPool(ADDR, PORT);

private static int waitTime = 10;//以10秒为基准

public static Jedis getJedis() {

return jedisPool.getResource();

}

//生产者,生成5个订单放进去

public void productionDelayMessage() {

long startTime = System.currentTimeMillis();

for (int i = 0; i < 5; i++) {

//延迟3秒

Calendar cal1 = Calendar.getInstance();

waitTime += 5;//每个订单之间的间隔为5秒

cal1.add(Calendar.SECOND, waitTime);

int second3later = (int) (cal1.getTimeInMillis() / 1000);

AppTest.getJedis().zadd(“OrderId”, second3later, “OID0000001” + i);

long endTime = System.currentTimeMillis();

System.out.println((endTime - startTime) / 1000 + “s:redis生成了一个订单任务:订单ID为” + “OID0000001” + i);

}

}

//消费者,取订单

public void consumerDelayMessage() {

Jedis jedis = AppTest.getJedis();

long startTime = System.currentTimeMillis();

while (true) {

Set items = jedis.zrangeWithScores(“OrderId”, 0, 1);

if (items == null || items.isEmpty()) {

System.out.println(“当前没有等待的任务”);

try {

Thread.sleep(500);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

continue;

}

int score = (int) ((Tuple) items.toArray()[0]).getScore();

Calendar cal = Calendar.getInstance();

int nowSecond = (int) (cal.getTimeInMillis() / 1000);

if (nowSecond >= score) {

String orderId = ((Tuple) items.toArray()[0]).getElement();

jedis.zrem(“OrderId”, orderId);

long endTime = System.currentTimeMillis();

System.out.println((endTime - startTime) / 1000 + “s:redis消费了一个任务:消费的订单OrderId为” + orderId);

}

}

}

public static void main(String[] args) {

AppTest appTest = new AppTest();

appTest.productionDelayMessage();//创建消费订单

appTest.consumerDelayMessage();//由消费者进行消费

}

}

测试结果如下:

在这里插入图片描述

通过上述结果我们可以看出每隔5秒消费了一笔订单

然而上面的实现却有一个致命的问题: 在高并发条件下,多消费者会取到同一个订单号

我们使用多线程进行测试一下

import java.util.concurrent.CountDownLatch;

public class ThreadTest {

private static final int threadNum = 10;//线程数

private static CountDownLatch cdl = new CountDownLatch(threadNum);

static class DelayMessage implements Runnable {

public void run() {

try {

cdl.await();

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

AppTest appTest = new AppTest();

appTest.consumerDelayMessage();

}

}

public static void main(String[] args) {

AppTest appTest = new AppTest();

appTest.productionDelayMessage();

for (int i = 0; i < threadNum; i++) {

new Thread(new DelayMessage()).start();

cdl.countDown();

}

}

}

在这里插入图片描述

可以看出出现了多个线程消费同一个资源的情况,这种情况肯定是不允许的,所以给出两个解决方案让大家参考

  • 1.用分布式锁,但是用分布式锁,性能下降了

  • 2.对ZREM的返回值进行判断,只有大于0的时候,才消费数据

由于方案1涉及到java分布式锁的设计,后面我会专门出一期关于java分布式锁的内容讲解,同时也会将第一个方法的内容补全,下面我们先看看如何通过ZREM来进行解决多线程消费同一个订单的问题

4.1.2 解决多线程消费同一资源

我们将consumerDelayMessage()方法中的

在这里插入图片描述

改为以下内容

if (nowSecond >= score) {

String orderId = ((Tuple) items.toArray()[0]).getElement();

Long num = jedis.zrem(“OrderId”, orderId);

long endTime = System.currentTimeMillis();

if (num != null && num > 0) {

System.out.println((endTime - startTime) / 1000 + “s:redis消费了一个任务:消费的订单OrderId为” + orderId);

}

}

再次进行多线程的测试

在这里插入图片描述

4.2 使用redis的Keyspace Notifications(键空间机制)


在redis的配置文件中(redis.conf)中,加入以下配置

notify-keyspace-events Ex

具体操作代码如下:

package com.example.test.test;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.JedisPubSub;

public class RedisTest {

private static final String ADDR = “192.168.1.2”;

private static final int PORT = 6379;

private static JedisPool jedis = new JedisPool(ADDR, PORT);

private static RedisSub sub = new RedisSub();

public static void init() {

new Thread(new Runnable() {

public void run() {

jedis.getResource().subscribe(sub, “keyevent@0:expired”);

}

}).start();

}

public static void main(String[] args) throws InterruptedException {

init();

for (int i = 0; i < 10; i++) {

String orderId = “OID000000” + i;

jedis.getResource().setex(orderId, 3, orderId);

更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

image

高效学习视频

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

private static RedisSub sub = new RedisSub();

public static void init() {

new Thread(new Runnable() {

public void run() {

jedis.getResource().subscribe(sub, “keyevent@0:expired”);

}

}).start();

}

public static void main(String[] args) throws InterruptedException {

init();

for (int i = 0; i < 10; i++) {

String orderId = “OID000000” + i;

jedis.getResource().setex(orderId, 3, orderId);

更多:Java进阶核心知识集

包含:JVM,JAVA集合,网络,JAVA多线程并发,JAVA基础,Spring原理,微服务,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存等等

[外链图片转存中…(img-BMVeIpev-1715378040961)]

高效学习视频

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 延时队列是指队列中的任务需要在一定延时后才能被执行,可以有效地解决某些任务需要延时处理的问题。Java Redis是一个基于Redis的Java客户端,它可以实现Java语言对Redis数据结构的操作,因此可以很方便地实现延时队列的功能。 Java Redis实现延时队列的主要思路是利用Redis的Sorted Set数据结构,将任务按照延时时间作为Score,任务内容作为Value,插入Sorted Set中,并设置过期时间,过期后将任务弹出并执行。具体实现步骤如下: 1. 创建一个Sorted Set,将任务插入其中,Score为任务的延时时间,Value为任务的内容。 2. 使用Redis的zremrangebyscore命令扫描Sorted Set中Score小于等于当前时间的任务并弹出,并将任务内容推送到执行队列中。 3. 设置延时任务的过期时间,过期时间为延时时间加当前时间,可以使用Redis的zadd命令添加任务时同时设置Score和过期时间。 4. 执行队列按顺序执行任务,任务执行完成后从执行队列中删除。 通过以上步骤,可以实现一个高效可靠的延时队列,可以优化系统任务调度、异步处理、消息通知等场景下的问题。 ### 回答2: Java Redis延时队列是一种常用的消息队列模式,在很多应用场景中都有应用。Java Redis延时队列通过将消息发送到Redis进行存储,在指定的时间后再将消息取出来进行处理。这个过程中,通过Redis的Sorted Set类型进行排序来保证队列中的消息有序。下面来详细介绍Java Redis延时队列的实现方式。 一、Redis数据结构 Java Redis延时队列的关键在于Redis数据结构的设计。在Redis中,Sorted Set就是用来解决排序问题的。所以我们需要借助Sorted Set实现延时队列。具体来说,可以使用Redis中的zadd命令将消息发送到Sorted Set中,并按照时间顺序进行排序。Sorted Set中的元素包含值和权重,我们可以根据权重(即时间戳)来实现有序存储。 二、消息入队 消息的入队过程如下: 1. 获取消息的过期时间TTL。 2. 计算出当前的时间戳now。 3. 将消息写入到Sorted Set中,权重为now+TTL。 ```redis-cli> ZADD delay-queue (now + TTL) message``` 三、消息出队 消息出队过程如下: 1. 获取当前时间戳now。 2. 使用zrangebyscore命令从Sorted Set中获取所有权重小于等于now的元素,即过期的元素。 3. 遍历查询结果,对每个元素执行出队操作(移除元素)。 ```redis-cli> ZRANGEBYSCORE delay-queue -inf now``` 四、多线程处理 为避免在出队过程中同时处理多个过期元素时出现问题,可以使用多线程处理消息。Java的并发包中提供了Executor框架,这里可以使用ThreadPoolExecutor线程池。 五、消息重试 有时候由于网络波动等原因,在执行消息处理时可能会失败,所以需要将失败的消息重新入队。此时,可以加入重试机制,重新入队时TTL加上重试时间,即可实现延时次数的控制。 六、总结 Java Redis延时队列利用Redis的Sorted Set实现有序存储,使用多线程和重试机制解决了消息处理时的并发和失败问题,保障了消息的可靠性。在实际应用中,可以根据业务需求进行调优和扩展,如设置合理的时间间隔、增加监控和报警等。 ### 回答3: Java Redis实现延时队列可以分为以下几步: 1.将任务加入到延时队列中:首先需要将任务和对应的过期时间放入Redis的有序集合中,以过期时间为score值,任务为value。这样可以保证按照过期时间顺序进行排序,具有先进先出的特点。代码实现如下: ```java //添加任务到延时队列 public void addToDelayQueue(String taskId, long delayTime) { //计算过期时间 long expireTime = System.currentTimeMillis() + delayTime; //添加到有序集合中,score为过期时间 jedis.zadd(DELAY_QUEUE_KEY, expireTime, taskId); } ``` 2.从延时队列中取出任务:需要循环从有序集合中取出第一个score小于等于当前时间的任务,并将其从有序集合中删除。代码实现如下: ```java //获取延时队列中的任务 public void getFromDelayQueue() { while (true) { //获取第一个score小于等于当前时间的任务 Set<String> set = jedis.zrangeByScore(DELAY_QUEUE_KEY, 0, System.currentTimeMillis(), 0, 1); if (set == null || set.isEmpty()) { try { Thread.sleep(1000);//如果没有取到任务,则等待1秒 } catch (InterruptedException e) { e.printStackTrace(); } continue; } String taskId = set.iterator().next(); //删除任务 jedis.zrem(DELAY_QUEUE_KEY, taskId); //处理任务 handleTask(taskId); } } //处理任务 public void handleTask(String taskId) { //TODO: 根据taskId执行对应的任务 } ``` 3.使用Redis发布订阅机制以及线程池来处理任务:在处理任务时,可以使用Redis发布订阅机制将任务相关的信息发布到指定的频道,由相应的消费者线程池来进行任务处理,可以降低单线程处理任务的压力。代码实现如下: ```java //处理任务 public void handleTask(String taskId) { //通过发布订阅机制将任务信息发布到指定频道 jedis.publish(TASK_CHANNEL, taskId); } //消费者线程池处理任务 public void consumeTask() { JedisPubSub jedisPubSub = new JedisPubSub() { public void onMessage(String channel, String message) { //TODO: 根据message信息执行对应的任务 } }; jedis.subscribe(jedisPubSub, TASK_CHANNEL); executorService.execute(jedisPubSub::quit); } ``` 通过以上步骤,我们就可以实现Java Redis延时队列的功能,实现了任务的延迟执行。延时队列的实现可以实现具有任务按照时间顺序执行,且任务可取消、可重试等特点,应用场景广泛。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值