侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

使用Redis实现延时任务的解决方案

Redis  /  管理员 发布于 7年前   284

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

候选方案对比

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

方案 优势 劣势 选用场景
JDK 内置的延迟队列 DelayQueue 实现简单 数据内存态,不可靠 一致性相对低的场景
调度框架和 MySQL 进行短间隔轮询 实现简单,可靠性高 存在明显的性能瓶颈 数据量较少实时性相对低的场景
RabbitMQ 的 DLX 和 TTL,一般称为 死信队列 方案 异步交互可以削峰 延时的时间长度不可控,如果数据需要持久化则性能会降低 -
调度框架和 Redis 进行短间隔轮询 数据持久化,高性能 实现难度大 常见于支付结果回调方案
时间轮 实时性高 实现难度大,内存消耗大 实时性高的场景

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

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

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

场景设计

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

否决的候选方案实现思路

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

JDK内置延迟队列

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

public class DelayQueueMain {  private static final Logger LOGGER = LoggerFactory.getLogger(DelayQueueMain.class);  public static void main(String[] args) throws Exception {    DelayQueue<OrderMessage> 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:1310:16:14.237 [DelayWorker] INFO club.throwable.delay.DelayQueueMain - 延迟处理订单消息,订单[ORDER_ID_10087]-创建时间为:2019-08-20 10:16:08,超时时间为:2019-08-20 10:16:1410: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 (这里只是为了方便用相对轻量级的框架实现,生产中可以按场景按需选择其他更合理的框架):

<dependency>  <groupId>mysql</groupId>  <artifactId>mysql-connector-java</artifactId>  <version>5.1.48</version>  <scope>test</scope></dependency><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-jdbc</artifactId>  <version>2.1.7.RELEASE</version>  <scope>test</scope></dependency><dependency>  <groupId>org.quartz-scheduler</groupId>  <artifactId>quartz</artifactId>  <version>2.3.1</version>  <scope>test</scope></dependency>

假设表设计如下:

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@Datapublic class OrderMessage {  private Long id;  private String orderId;  private LocalDateTime createTime;  private LocalDateTime editTime;  private Integer retryTimes;  private Integer orderStatus;}// DAO@RequiredArgsConstructorpublic class OrderMessageDao {  private final JdbcTemplate jdbcTemplate;  private static final ResultSetExtractor<List<OrderMessage>> M = r -> {    List<OrderMessage> 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<OrderMessage> selectPendingRecords(LocalDateTime start,  LocalDateTime end,  List<Integer> 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 status) {    return jdbcTemplate.update("UPDATE t_order_message SET order_status = ?,edit_time = ? WHERE id =?",        p -> {          p.setInt(1, status);          p.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));          p.setLong(3, id);        });  }}// Service@RequiredArgsConstructorpublic class OrderMessageService {  private static final Logger LOGGER = LoggerFactory.getLogger(OrderMessageService.class);  private final OrderMessageDao orderMessageDao;  private static final List<Integer> STATUS = Lists.newArrayList();  static {    STATUS.add(OrderConstants.PENDING);    STATUS.add(OrderConstants.FAIL);  }  public void executeDelayJob() {    LOGGER.info("订单处理定时任务开始执行......");    LocalDateTime end = LocalDateTime.now();    // 一天前    LocalDateTime start = end.minusDays(1);    List<OrderMessage> list = orderMessageDao.selectPendingRecords(start, end, STATUS, OrderConstants.MAX_RETRY_TIMES, OrderConstants.LIMIT);    if (!list.isEmpty()) {      for (OrderMessage m : list) {        LOGGER.info("处理订单[{}],状态由{}更新为{}", m.getOrderId(), m.getOrderStatus(), OrderConstants.SUCCESS);        // 这里其实可以优化为批量更新        orderMessageDao.updateOrderStatus(m.getId(), OrderConstants.SUCCESS);      }    }    LOGGER.info("订单处理定时任务开始完毕......");  }}// Job@DisallowConcurrentExecutionpublic class OrderMessageDelayJob implements Job {  @Override  public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {    OrderMessageService service = (OrderMessageService) jobExecutionContext.getMergedJobDataMap().get("orderMessageService");    service.executeDelayJob();  }  public static void main(String[] args) throws Exception {    HikariConfig config = new HikariConfig();    config.setJdbcUrl("jdbc:mysql://localhost:3306/delayTask?useSSL=false&characterEncoding=utf8");    config.setDriverClassName(Driver.class.getName());    config.setUsername("root");    config.setPassword("root");    HikariDataSource dataSource = new HikariDataSource(config);    OrderMessageDao orderMessageDao = new OrderMessageDao(new JdbcTemplate(dataSource));    OrderMessageService service = new OrderMessageService(orderMessageDao);    // 内存模式的调度器    StdSchedulerFactory factory = new StdSchedulerFactory();    Scheduler scheduler = factory.getScheduler();    // 这里没有用到IOC容器,直接用Quartz数据集合传递服务引用    JobDataMap jobDataMap = new JobDataMap();    jobDataMap.put("orderMessageService", service);    // 新建Job    JobDetail job = JobBuilder.newJob(OrderMessageDelayJob.class)        .withIdentity("orderMessageDelayJob", "delayJob")        .usingJobData(jobDataMap)        .build();    // 新建触发器,10秒执行一次    Trigger trigger = TriggerBuilder.newTrigger()        .withIdentity("orderMessageDelayTrigger", "delayJob")        .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(10).repeatForever())        .build();    scheduler.scheduleJob(job, trigger);    // 启动调度器    scheduler.start();    Thread.sleep(Integer.MAX_VALUE);  }}

这个例子里面用了 create_time 做轮询,实际上可以添加一个调度时间 schedule_time 列做轮询,这样子才能更容易定制空闲时和忙碌时候的调度策略。上面的示例的运行效果如下:

11:58:27.202 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.1) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.11:58:27.202 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'11:58:27.202 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.111:58:27.209 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.11:58:27.212 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers11:58:27.217 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'delayJob.orderMessageDelayJob', class=club.throwable.jdbc.OrderMessageDelayJob11:58:27.219 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@10eb8c5311:58:27.220 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers11:58:27.221 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job delayJob.orderMessageDelayJob11:58:34.440 [DefaultQuartzScheduler_Worker-1] INFO club.throwable.jdbc.OrderMessageService - 订单处理定时任务开始执行......11:58:34.451 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@3d27ece411:58:34.459 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@64e808af11:58:34.470 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@79c8c2b711:58:34.477 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@19a6236911:58:34.485 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.jdbc.JDBC4Connection@1673d01711:58:34.485 [HikariPool-1 connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - After adding stats (total=10, active=0, idle=10, waiting=0)11:58:34.559 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL query11:58:34.565 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [SELECT * FROM t_order_message WHERE create_time >= ? AND create_time <= ? AND order_status IN (?) AND retry_times < ? LIMIT ?]11:58:34.645 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource11:58:35.210 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - SQLWarning ignored: SQL state '22007', error code '1292', message [Truncated incorrect DOUBLE value: '0,-1']11:58:35.335 [DefaultQuartzScheduler_Worker-1] INFO club.throwable.jdbc.OrderMessageService - 处理订单[10086],状态由0更新为111:58:35.342 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL update11:58:35.346 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [UPDATE t_order_message SET order_status = ?,edit_time = ? WHERE id =?]11:58:35.347 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource11:58:35.354 [DefaultQuartzScheduler_Worker-1] INFO club.throwable.jdbc.OrderMessageService - 处理订单[10087],状态由0更新为111:58:35.355 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL update11:58:35.355 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [UPDATE t_order_message SET order_status = ?,edit_time = ? WHERE id =?]11:58:35.355 [DefaultQuartzScheduler_Worker-1] DEBUG org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource11:58:35.361 [DefaultQuartzScheduler_Worker-1] INFO club.throwable.jdbc.OrderMessageService - 订单处理定时任务开始完毕......11:58:35.363 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers11:58:37.206 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'delayJob.orderMessageDelayJob', class=club.throwable.jdbc.OrderMessageDelayJob11:58:37.206 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers

RabbitMQ死信队列

使用 RabbitMQ 死信队列依赖于 RabbitMQ 的两个特性: TTL 和 DLX 。

TTL : Time To Live ,消息存活时间,包括两个维度:队列消息存活时间和消息本身的存活时间。

DLX : Dead Letter Exchange ,死信交换器。

画个图描述一下这两个特性:

下面为了简单起见, TTL 使用了针对队列的维度。引入 RabbitMQ 的Java驱动:

<dependency>  <groupId>com.rabbitmq</groupId>  <artifactId>amqp-client</artifactId>  <version>5.7.3</version>  <scope>test</scope></dependency>

代码如下:

public class DlxMain {  private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");  private static final Logger LOGGER = LoggerFactory.getLogger(DlxMain.class);  public static void main(String[] args) throws Exception {    ConnectionFactory factory = new ConnectionFactory();    Connection connection = factory.newConnection();    Channel producerChannel = connection.createChannel();    Channel consumerChannel = connection.createChannel();    // dlx交换器名称为dlx.exchange,类型是direct,绑定键为dlx.key,队列名为dlx.queue    producerChannel.exchangeDeclare("dlx.exchange", "direct");    producerChannel.queueDeclare("dlx.queue", false, false, false, null);    producerChannel.queueBind("dlx.queue", "dlx.exchange", "dlx.key");    Map<String, Object> queueArgs = new HashMap<>();    // 设置队列消息过期时间,5秒    queueArgs.put("x-message-ttl", 5000);    // 指定DLX相关参数    queueArgs.put("x-dead-letter-exchange", "dlx.exchange");    queueArgs.put("x-dead-letter-routing-key", "dlx.key");    // 声明业务队列    producerChannel.queueDeclare("business.queue", false, false, false, queueArgs);    ExecutorService executorService = Executors.newSingleThreadExecutor(r -> {      Thread thread = new Thread(r);      thread.setDaemon(true);      thread.setName("DlxConsumer");      return thread;    });    // 启动消费者    executorService.execute(() -> {      try {        consumerChannel.basicConsume("dlx.queue", true, new DlxConsumer(consumerChannel));      } catch (IOException e) {        LOGGER.error(e.getMessage(), e);      }    });    OrderMessage message = new OrderMessage("10086");    producerChannel.basicPublish("", "business.queue", MessageProperties.TEXT_PLAIN,        message.getDescription().getBytes(StandardCharsets.UTF_8));    LOGGER.info("发送消息成功,订单ID:{}", message.getOrderId());    message = new OrderMessage("10087");    producerChannel.basicPublish("", "business.queue", MessageProperties.TEXT_PLAIN,        message.getDescription().getBytes(StandardCharsets.UTF_8));    LOGGER.info("发送消息成功,订单ID:{}", message.getOrderId());    message = new OrderMessage("10088");    producerChannel.basicPublish("", "business.queue", MessageProperties.TEXT_PLAIN,        message.getDescription().getBytes(StandardCharsets.UTF_8));    LOGGER.info("发送消息成功,订单ID:{}", message.getOrderId());    Thread.sleep(Integer.MAX_VALUE);  }  private static class DlxConsumer extends DefaultConsumer {    DlxConsumer(Channel channel) {      super(channel);    }    @Override    public void handleDelivery(String consumerTag,      Envelope envelope,      AMQP.BasicProperties properties,      byte[] body) throws IOException {      LOGGER.info("处理消息成功:{}", new String(body, StandardCharsets.UTF_8));    }  }  private static class OrderMessage {    private final String orderId;    private final long timestamp;    private final String description;    OrderMessage(String orderId) {      this.orderId = orderId;      this.timestamp = System.currentTimeMillis();      this.description = String.format("订单[%s],订单创建时间为:%s", orderId,          LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()).format(F));    }    public String getOrderId() {      return orderId;    }    public long getTimestamp() {      return timestamp;    }    public String getDescription() {      return description;    }  }}

运行 main() 方法结果如下:

16:35:58.638 [main] INFO club.throwable.dlx.DlxMain - 发送消息成功,订单ID:1008616:35:58.641 [main] INFO club.throwable.dlx.DlxMain - 发送消息成功,订单ID:1008716:35:58.641 [main] INFO club.throwable.dlx.DlxMain - 发送消息成功,订单ID:1008816:36:03.646 [pool-1-thread-4] INFO club.throwable.dlx.DlxMain - 处理消息成功:订单[10086],订单创建时间为:2019-08-20 16:35:5816:36:03.670 [pool-1-thread-5] INFO club.throwable.dlx.DlxMain - 处理消息成功:订单[10087],订单创建时间为:2019-08-20 16:35:5816:36:03.670 [pool-1-thread-6] INFO club.throwable.dlx.DlxMain - 处理消息成功:订单[10088],订单创建时间为:2019-08-20 16:35:58

时间轮


  • 上一条:
    基于redis实现定时任务的方法详解
    下一条:
    基于redis实现token验证用户是否登陆
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 在Redis中能实现的功能、常见应用介绍(0个评论)
    • 2024年Redis面试题之一(0个评论)
    • 在redis缓存常见出错及解决方案(0个评论)
    • 在redis中三种特殊数据类型:地理位置、基数(cardinality)估计、位图(Bitmap)使用场景介绍浅析(2个评论)
    • Redis 删除 key用 del 和 unlink 有啥区别?(1个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2017-12
    • 2020-03
    • 2020-05
    • 2021-04
    • 2022-03
    • 2022-05
    • 2022-08
    • 2023-02
    • 2023-04
    • 2023-07
    • 2024-01
    • 2024-02
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客