redis调度mysql_使用Redis实现延时任务的解决方案

最近在生产环境刚好遇到了延时任务的场景,调研了一下目前主流的方案,分析了一下优劣并且敲定了最终的方案。这篇文章记录了调研的过程,以及初步方案的实现。候选方案对比下面是想到的几种实现延时任务的方案,总结了一下相应的优势和劣势。方案优势劣势选用场景JDK内置的延迟队列DelayQueue实现简单数据内存态,不可靠一致性相对低的场景调度框架和MySQL进行短间隔轮询实现简单,可靠性高存在明显的性能瓶颈数...
摘要由CSDN通过智能技术生成

最近在生产环境刚好遇到了延时任务的场景,调研了一下目前主流的方案,分析了一下优劣并且敲定了最终的方案。这篇文章记录了调研的过程,以及初步方案的实现。

候选方案对比

下面是想到的几种实现延时任务的方案,总结了一下相应的优势和劣势。

方案

优势

劣势

选用场景

JDK内置的延迟队列DelayQueue

实现简单

数据内存态,不可靠

一致性相对低的场景

调度框架和MySQL进行短间隔轮询

实现简单,可靠性高

存在明显的性能瓶颈

数据量较少实时性相对低的场景

RabbitMQ的DLX和TTL,一般称为死信队列方案

异步交互可以削峰

延时的时间长度不可控,如果数据需要持久化则性能会降低

-

调度框架和Redis进行短间隔轮询

数据持久化,高性能

实现难度大

常见于支付结果回调方案

时间轮

实时性高

实现难度大,内存消耗大

实时性高的场景

如果应用的数据量不高,实时性要求比较低,选用调度框架和 MySQL 进行短间隔轮询这个方案是最优的方案。但是笔者遇到的场景数据量相对比较大,实时性并不高,采用扫库的方案一定会对 MySQL 实例造成比较大的压力。记得很早之前,看过一个PPT叫《盒子科技聚合支付系统演进》,其中里面有一张图片给予笔者一点启发:

b79e4915fbfeb973747b11be29e181e5.png

里面刚好用到了调度框架和 Redis 进行短间隔轮询实现延时任务的方案,不过为了分摊应用的压力,图中的方案还做了分片处理。鉴于笔者当前业务紧迫,所以在第一期的方案暂时不考虑分片,只做了一个简化版的实现。

由于PPT中没有任何的代码或者框架贴出,有些需要解决的技术点需要自行思考,下面会重现一次整个方案实现的详细过程。

场景设计

实际的生产场景是笔者负责的某个系统需要对接一个外部的资金方,每一笔资金下单后需要延时30分钟推送对应的附件。这里简化为一个订单信息数据延迟处理的场景,就是每一笔下单记录一条订单消息(暂时叫做 OrderMessage ),订单消息需要延迟5到15秒后进行异步处理。

07b20cc88331968c473d001d8690a4d2.png

否决的候选方案实现思路

下面介绍一下其它四个不选用的候选方案,结合一些伪代码和流程分析一下实现过程。

JDK内置延迟队列

DelayQueue 是一个阻塞队列的实现,它的队列元素必须是 Delayed 的子类,这里做个简单的例子:

public class DelayQueueMain {

private static final Logger LOGGER = LoggerFactory.getLogger(DelayQueueMain.class);

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

DelayQueue queue = new DelayQueue<>();

// 默认延迟5秒

OrderMessage message = new OrderMessage("ORDER_ID_10086");

queue.add(message);

// 延迟6秒

message = new OrderMessage("ORDER_ID_10087", 6);

queue.add(message);

// 延迟10秒

message = new OrderMessage("ORDER_ID_10088", 10);

queue.add(message);

ExecutorService executorService = Executors.newSingleThreadExecutor(r -> {

Thread thread = new Thread(r);

thread.setName("DelayWorker");

thread.setDaemon(true);

return thread;

});

LOGGER.info("开始执行调度线程...");

executorService.execute(() -> {

while (true) {

try {

OrderMessage task = queue.take();

LOGGER.info("延迟处理订单消息,{}", task.getDescription());

} catch (Exception e) {

LOGGER.error(e.getMessage(), e);

}

}

});

Thread.sleep(Integer.MAX_VALUE);

}

private static class OrderMessage implements Delayed {

private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

/**

* 默认延迟5000毫秒

*/

private static final long DELAY_MS = 1000L * 5;

/**

* 订单ID

*/

private final String orderId;

/**

* 创建时间戳

*/

private final long timestamp;

/**

* 过期时间

*/

private final long expire;

/**

* 描述

*/

private final String description;

public OrderMessage(String orderId, long expireSeconds) {

this.orderId = orderId;

this.timestamp = System.currentTimeMillis();

this.expire = this.timestamp + expireSeconds * 1000L;

this.description = String.format("订单[%s]-创建时间为:%s,超时时间为:%s", orderId,

LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F),

LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F));

}

public OrderMessage(String orderId) {

this.orderId = orderId;

this.timestamp = System.currentTimeMillis();

this.expire = this.timestamp + DELAY_MS;

this.description = String.format("订单[%s]-创建时间为:%s,超时时间为:%s", orderId,

LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F),

LocalDateTime.ofInstant(Instant.ofEpochMilli(expire), ZoneId.systemDefault()).format(F));

}

public String getOrderId() {

return orderId;

}

public long getTimestamp() {

return timestamp;

}

public long getExpire() {

return expire;

}

public String getDescription() {

return description;

}

@Override

public long getDelay(TimeUnit unit) {

return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);

}

@Override

public int compareTo(Delayed o) {

return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));

}

}

}

注意一下, OrderMessage 实现 Delayed 接口,关键是需要实现 Delayed#getDelay() 和 Delayed#compareTo() 。运行一下 main() 方法:

10:16:08.240 [main] INFO club.throwable.delay.DelayQueueMain - 开始执行调度线程...

10:16:13.224 [DelayWorker] INFO club.throwable.delay.DelayQueueMain - 延迟处理订单消息,订单[ORDER_ID_10086]-创建时间为:2019-08-20 10:16:08,超时时间为:2019-08-20 10:16:13

10:16:14.237 [DelayWorker] INFO club.throwable.delay.DelayQueueMain - 延迟处理订单消息,订单[ORDER_ID_10087]-创建时间为:2019-08-20 10:16:08,超时时间为:2019-08-20 10:16:14

10:16:18.237 [DelayWorker] INFO club.throwable.delay.DelayQueueMain - 延迟处理订单消息,订单[ORDER_ID_10088]-创建时间为:2019-08-20 10:16:08,超时时间为:2019-08-20 10:16:18

调度框架 + MySQL

使用调度框架对 MySQL 表进行短间隔轮询是实现难度比较低的方案,通常服务刚上线,表数据不多并且实时性不高的情况下应该首选这个方案。不过要注意以下几点:

MySQL

引入 Quartz 、 MySQL 的Java驱动包和 spring-boot-starter-jdbc (这里只是为了方便用相对轻量级的框架实现,生产中可以按场景按需选择其他更合理的框架):

mysql

mysql-connector-java

5.1.48

test

org.springframework.boot

spring-boot-starter-jdbc

2.1.7.RELEASE

test

org.quartz-scheduler

quartz

2.3.1

test

假设表设计如下:

CREATE DATABASE `delayTask` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci;

USE `delayTask`;

CREATE TABLE `t_order_message`

(

id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,

order_id VARCHAR(50) NOT NULL COMMENT '订单ID',

create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期时间',

edit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改日期时间',

retry_times TINYINT NOT NULL DEFAULT 0 COMMENT '重试次数',

order_status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',

INDEX idx_order_id (order_id),

INDEX idx_create_time (create_time)

) COMMENT '订单信息表';

# 写入两条测试数据

INSERT INTO t_order_message(order_id) VALUES ('10086'),('10087');

编写代码:

// 常量

public class OrderConstants {

public static final int MAX_RETRY_TIMES = 5;

public static final int PENDING = 0;

public static final int SUCCESS = 1;

public static final int FAIL = -1;

public static final int LIMIT = 10;

}

// 实体

@Builder

@Data

public class OrderMessage {

private Long id;

private String orderId;

private LocalDateTime createTime;

private LocalDateTime editTime;

private Integer retryTimes;

private Integer orderStatus;

}

// DAO

@RequiredArgsConstructor

public class OrderMessageDao {

private final JdbcTemplate jdbcTemplate;

private static final ResultSetExtractor> M = r -> {

List list = Lists.newArrayList();

while (r.next()) {

list.add(OrderMessage.builder()

.id(r.getLong("id"))

.orderId(r.getString("order_id"))

.createTime(r.getTimestamp("create_time").toLocalDateTime())

.editTime(r.getTimestamp("edit_time").toLocalDateTime())

.retryTimes(r.getInt("retry_times"))

.orderStatus(r.getInt("order_status"))

.build());

}

return list;

};

public List selectPendingRecords(LocalDateTime start,

LocalDateTime end,

List statusList,

int maxRetryTimes,

int limit) {

StringJoiner joiner = new StringJoiner(",");

statusList.forEach(s -> joiner.add(String.valueOf(s)));

return jdbcTemplate.query("SELECT * FROM t_order_message WHERE create_time >= ? AND create_time <= ? " +

"AND order_status IN (?) AND retry_times < ? LIMIT ?",

p -> {

p.setTimestamp(1, Timestamp.valueOf(start));

p.setTimestamp(2, Timestamp.valueOf(end));

p.setString(3, joiner.toString());

p.setInt(4, maxRetryTimes);

p.setInt(5, limit);

}, M);

}

public int updateOrderStatus(Long id, int

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值