所谓的延时任务给大家举个例子:你买了一张火车票,必须在30分钟之内付款,否则该订单被自动取消。订单30分钟不付款自动取消,这个任务就是一个延时任务。 我之前已经写过2篇关于延时任务的文章:

这两种方法都有一个缺点:都是基于单体应用的内存的方式运行延时任务的,一旦出现单点故障,可能出现延时任务数据的丢失。所以此篇文章给大家介绍实现延时任务的第三种方式,结合redis zset实现延时任务,可以解决单点故障的问题。给出实现原理、完整实现代码,以及这种实现方式的优缺点。

一、实现原理

首先来介绍一下实现原理,我们需要使用redis zset来实现延时任务的需求,所以我们需要知道zset的应用特性。zset作为redis的有序集合数据结构存在,排序的依据就是score。



所以我们可以利用zset score这个排序的这个特性,来实现延时任务

  • 在用户下单的时候,同时生成延时任务放入redis,key是可以自定义的,比如:delaytask:order
  • value的值分成两个部分,一个部分是score用于排序,一个部分是member,member的值我们设置为订单对象(如:订单编号),因为后续延时任务时效达成的时候,我们需要有一些必要的订单信息(如:订单编号),才能完成订单自动取消关闭的动作。
  • 延时任务实现的重点来了,score我们设置为:订单生成时间 + 延时时长。 这样redis会对zset按照score延时时间进行排序。
  • 开启redis扫描任务,获取"当前时间 > score"的延时任务并执行。即: 当前时间 > 订单生成时间 + 延时时长的时候 ,执行延时任务。

二、准备工作

使用 redis zset 这个方案来完成延时任务的需求,首先肯定是需要redis,这一点毫无疑问。redis的搭建网上有很多的文章,我这里就不赘述了。

其次,笔者长期的java类应用系统开发都是使用SpringBoot来完成,所以也是习惯使用SpringBoot的redis集成方案。首先通过maven坐标引入spring-boot-starter-data-redis

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>

其次需要在Spring Boot的application.yml配置文件中,配置redis数据库的链接信息。我这里配置的是redis的单例,如果大家的生产环境是哨兵模式、或者是集群模式的redis,这里的配置方式需要进行微调。其实这部分内容在我的个人博客里面都曾经系统的介绍过,感兴趣的朋友可以关注我的个人博客。

spring:
redis:
database: 0 # Redis 数据库索引(默认为 0)
host: 192.168.161.3 # Redis 服务器地址
port: 6379 # Redis 服务器连接端口
password: 123456 # Redis 服务器连接密码(默认为空)
timeout: 5000 # 连接超时,单位ms
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-idle: 8 # 连接池中的最大空闲连接 默认 8
min-idle: 0 # 连接池中的最小空闲连接 默认 0

三、代码实现

下面的这个类就是延时任务的核心实现了,一共包含三个核心方法,我们来一一说明一下:

  • produce方法,用于生成订单-order为订单信息,可以是订单流水号,用于延时任务达到时效后关闭订单
  • afterPropertiesSet方法是InitializingBean接口的方法,之所以实现这个接口,是因为我们需要在应用启动的时候开启redis扫描任务。即:当OrderDelayService bean初始化的时候,开启redis扫描任务循环获取延时任务数据。
  • consuming函数,用于从redis获取延时任务数据,消费延时任务,执行超时订单关闭等操作。为了避免阻塞for循环,影响后面延时任务的执行,所以这个consuming函数一定要做成异步的,参考Spring Boot异步任务及Async注解的使用方法。我之前写过一个SpringBoot的可观测、易配置的异步任务线程池开源项目,源代码地址:https://gitee.com/hanxt/zimug-monitor-threadpool 。我的这个zimug-monitor-threadpool开源项目,可以做到对线程池使用情况的监控,我自己平时用的效果还不错,向大家推荐一下!
@Component
public class OrderDelayService implements InitializingBean {
//redis zset key
public static final String ORDER_DELAY_TASK_KEY = "delaytask:order"; @Resource
private StringRedisTemplate stringRedisTemplate; //生成订单-order为订单信息,可以是订单流水号,用于延时任务达到时效后关闭订单
public void produce(String orderSerialNo){
stringRedisTemplate.opsForZSet().add(
ORDER_DELAY_TASK_KEY, // redis key
orderSerialNo, // zset member
//30分钟延时
System.currentTimeMillis() + (30 * 60 * 1000) //zset score
);
} //延时任务,也是异步任务,延时任务达到时效之后关闭订单,并将延时任务从redis zset删除
@Async("test")
public void consuming(){ Set<ZSetOperations.TypedTuple<String>> orderSerialNos = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(
ORDER_DELAY_TASK_KEY,
0, //延时任务score最小值
System.currentTimeMillis() //延时任务score最大值(当前时间)
);
if (!CollectionUtils.isEmpty(orderSerialNos)) {
for (ZSetOperations.TypedTuple<String> orderSerialNo : orderSerialNos) {
//这里根据orderSerialNo去检查用户是否完成了订单支付
//如果用户没有支付订单,去执行订单关闭的操作
System.out.println("订单" + orderSerialNo.getValue() + "超时被自动关闭");
//订单关闭之后,将订单延时任务从队列中删除
stringRedisTemplate.opsForZSet().remove(ORDER_DELAY_TASK_KEY, orderSerialNo.getValue());
}
}
} //该类对象Bean实例化之后,就开启while扫描任务
@Override
public void afterPropertiesSet() throws Exception {
new Thread(() -> { //开启新的线程,否则SpringBoot应用初始化无法启动
while(true){
try {
Thread.sleep(5 * 1000); //每5秒扫描一次redis库获取延时数据,不用太频繁没必要
} catch (InterruptedException e) {
e.printStackTrace(); //本文只是示例,生产环境请做好相关的异常处理
}
consuming();
}
}).start();
}
}

更多的内容参考代码中的注释,需要关注的点是:

  • 上文中的rangeByScoreWithScores方法用于从redis中获取延时任务,score大于0小于当前时间的所有延时任务,都将被从redis里面取出来。每5秒执行一次,所以延时任务的误差不会超过5秒。
  • 上文中的订单信息,我只保留了订单唯一流水号,用于关闭订单。如果你的业务需要传递更多的订单信息,请使用RedisTemplate操作订单类对象,而不是StringRedisTemplate操作订单流水号字符串。

订单下单的时候,使用如下的方法,将订单序列号放入redis zset中即可实现延时任务

orderDelayService.produce("这里填写订单编号");

四、优缺点

使用redis zset来实现延时任务的优点是:相对于本文开头介绍的两种方法,我们的延时任务是保存在redis里面的,redis具有数据持久化的机制,可以有效的避免延时任务数据的丢失。另外,redis还可以通过哨兵模式、集群模式有效的避免单点故障造成的服务中断。

至于缺点嘛,我觉得没什么缺点。如果非要勉强的说一个缺点的话,那就是我们需要额外维护redis服务,增加了硬件资源的需求和运维成本。但是现在随着微服务的兴起,redis几乎已经成了应用系统的标配,redis复用即可,所以我感觉这也算不上什么缺点吧!

码文不易,如果您觉得有帮助,请帮忙点击在看或者分享,没有您的支持我可能无法坚持下去!

欢迎关注我的公告号:字母哥杂谈,回复003赠送作者专栏《docker修炼之道》的PDF版本,30余篇精品docker文章。字母哥博客:zimug.com

延时任务-基于redis zset的完整实现的更多相关文章

  1. 基于Redis实现延时队列服务

    背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单 b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论 c.点我达 ...

  2. 【转】基于Redis实现延时队列服务

    背景 在业务发展过程中,会出现一些需要延时处理的场景,比如: a.订单下单之后超过30分钟用户未支付,需要取消订单b.订单一些评论,如果48h用户未对商家评论,系统会自动产生一条默认评论c.点我达订单 ...

  3. 基于redis有序集合,实现简单的延时任务

    基于redis有序集合,实现简单的延时任务 延时任务的场景很多,开发过程中我们经常会遇到,比如说: 1.订单未付款,5分钟后自动取消,这是电商网站非常普遍的需求: 2.用户创建订单不付款,3分钟后自动 ...

  4. PHP基于Redis实现轻量级延迟队列

    延迟队列,顾名思义它是一种带有延迟功能的消息队列. 那么,是在什么场景下我才需要这样的队列呢? 一.背景 先看看一下业务场景: 1.会员过期前3天发送召回通知 2.订单支付成功后,5分钟后检测下游环节 ...

  5. 基于redis分布式缓存实现(新浪微博案例)

    第一:Redis 是什么? Redis是基于内存.可持久化的日志型.Key-Value数据库 高性能存储系统,并提供多种语言的API. 第二:出现背景 数据结构(Data Structure)需求越来 ...

  6. 想知道谁是你的最佳用户?基于Redis实现排行榜周期榜与最近N期榜

    本文由云+社区发表 前言 业务已基于Redis实现了一个高可用的排行榜服务,长期以来相安无事.有一天,产品说:我要一个按周排名的排行榜,以反映本周内用户的活跃情况.于是周榜(按周重置更新的榜单)诞生了 ...

  7. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  8. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  9. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

随机推荐

  1. 互联网公司实行目标管理(OKR)五点原则和基础

    下面从公司文化.组织架构.管理者.落地执行和区别绩效考核五个方面,讲述了如何在公司落地目标管理(OKR),这些是实施OKR的基础,也是原则,虽然写得比较简单,其实是我过去两年多不断观察.实践和摸索的总 ...

  2. synchronized下的 i+=2 和 i++ i++执行结果居然不一样

    起因 逛[博客园-博问]时发现了一段有意思的问题: 问题链接:https://q.cnblogs.com/q/140032/ 这段代码是这样的: import java.util.concurrent ...

  3. mac上使用Vmware Fusion虚拟机配置Centos的静态ip

    一.背景 本文简单记录一下,在mac arm 架构下使用 Vmware Fusion虚拟机下Centos7下如何配置静态ip地址.如果使用dhcp静态ip地址的动态分配,那么可能ip地址会发生变化,因 ...

  4. ExtJS 布局-Table布局(Table layout)

    更新记录: 2022年6月1日 开始. 2022年6月10日 发布. 1.说明 table布局类似表格,通过指定行列数实现布局. 2.设置布局方法 在父容器中指定 layout: 'table' la ...

  5. ssm框架layui分页下标中文乱码,或者请选择中文乱码,提示乱码等

    开始我以为是layui的bug 后来发现不是 用过的方法: 1.修改layui的js文件  将其中的中文变为encdoe 代码 比如laypage.js下的中文 2.添加web.xml的过滤器 该代码 ...

  6. python简单处理验证码,三分钟,不能再多了

    序言 大家好鸭, 又是我小熊猫啦 我们在做采集数据的时候,过快或者访问频繁,或者一访问就给弹出验证码,然后就蚌珠了~今天就给大家来一个简单处理验证码的方法 环境模块 Python和pycharm如果还 ...

  7. Windows 启动过程

    引言 启动过程是我们了解操作系统的第一个环节.了解 Windows 的启动过程,可以帮助我们解决一些启动的问题,也能帮助我们了解 Windows 的整体结构. 以下内容将分为[加载内核].[内核初始化 ...

  8. 毫秒值的概念和作用 --Date类的构造方法和成员方法

    一,  Date类类 Date 表示特定的瞬间,精确到毫秒. 毫秒:千分之一秒作用:可以对时间和日期进行计算可一把日期转换为毫秒进行计算,计算完毕,再转换为日期. 把日期转换为毫秒:当前的日期:202 ...

  9. C# 11 的新特性和改进前瞻

    前言 .NET 7 的开发还剩下一个多月就要进入 RC,C# 11 的新特性和改进也即将敲定.在这个时间点上,不少新特性都已经实现完毕并合并入主分支 C# 11 包含的新特性和改进非常多,类型系统相比 ...

  10. 未找到与名为“xxx”的控制器匹配的类型。

    自己封装了一个BaseApiControllerr把他独立成一个项目出来在引用不行,而用默认自带的ApiControllerr可以. <Error> <Message> 未找到 ...