分布式 redis 延时任务 基于 springboot 示例
Lilishop 技术栈
官方公众号 & 开源不易,如有帮助请点Star
介绍
Lilishop 是一款Java开发,基于SpringBoot研发的B2B2C多用户商城,前端使用 Vue、uniapp开发 系统全端全部代码开源
本系统用于教大家如何运用系统中的每一个细节,如:支付、第三方登录、日志收集、分布式事务、秒杀场景等各个场景学习方案
git地址 https://gitee.com/beijing_hongye_huicheng/lilishop-spring-learning
本文学习 分布式延时任务
延时任务介绍
即指定一个时间,执行提前约定好的任务,例如:定时取消订单,定时上下架商品,定时开启活动等。
延时任务与定时任务的区别
延时任务适用于个性化的业务场景,比如某订单自动取消,某活动自动开启,某商品自动上下架子。还有一个就是较为精确的,需要实时的事情。
而定时任务适用于全平台的业务,比如计算商品评分统一结算,分销中的可提现金额批量结算,平台统计/店铺统计数据生成等。总的来说就是定时扫描,每天,每小时,每分钟,每个月,不管怎么样都要执行。比如定时上下架,用定时任务也可以,但是要实现精确的任务调度,创建一个每秒任务,是不太理智的。
两个场景需要互补,具体应用什么场景,可以再自己斟酌斟酌。
思路介绍
- 项目启动时启用一个线程,线程用于间隔一定时间去查询redis的待执行任务。其任务id为对象json格式化之后的字符串,值为要执行的时间。
- 查询到执行的任务时,将其从redis的信息中进行删除。(删除成功才执行延时任务,否则不执行,这样可以避免分布式系统延时任务多次执行。)
- 删除redis中的记录之后,启用子线程执行任务。将执行id,也就是json的字符串翻转回要执行的任务信息,这样可以得到用什么执行器去执行任务,参数有哪些。
- 执行延时任务
实际使用
实际场景中,还会设计延时任务修改,删除等,这些场景建议在执行任务创建时,redis标记要执行的任务,如果删除或者修改任务时,修改redis中的标识即可,当然也可以在业务逻辑中做补充的条件判定,都可以。
另外具体执行任务建议使用mq去实现,相当于在执行任务时,线程只是发布一个mq,交给消费者去消费具体的事情。
代码中的进程扫描5秒,也就代表一个延时任务最多延迟5秒去执行,实战场景中可以调整至1秒,或者更低,但是不太建议。另外redis的性能杠杠的,不用太担心redis的连接数导致性能问题。
使用步骤
启用redis,可以本地启动,也可以用ELK中docker-compose启动。
启动springboot应用。
请求springboot 应用 http://127.0.0.1:8080
查看控制台输出内容
2021-06-09 12:41:33.168 INFO 40730 --- [nio-8888-exec-1] l.t.p.d.AbstractDelayQueueMachineFactory : 增加延时任务, 缓存key test_delay, 等待时间 10
2021-06-09 12:41:33.168 INFO 40730 --- [nio-8888-exec-1] c.l.t.p.i.impl.RedisTimerTrigger : 定时执行在【2021-06-09 12:41:43】,消费【test params】
2021-06-09 12:41:44.399 INFO 40730 --- [ Thread-5] l.t.p.d.AbstractDelayQueueMachineFactory : 延时任务开始执行任务:[{"score":1.623213703E9,"value":"{"triggerTime":1623213703,"triggerExecutor":"testTimeTriggerExecutor","param":"test params"}"}]
2021-06-09 12:41:44.403 INFO 40730 --- [pool-2-thread-2] c.l.t.p.i.e.TestTimeTriggerExecutor : 执行器具执行任务test params
关键类介绍
缓存操作类 用于延时任务的核型逻辑,间隔查询需要执行的延时任务,考的就是redis的Sorted Set属性来试下排序,执行任务。
/**
* 向Zset里添加成员
*
* @param key key值
* @param score 分数,通常用于排序
* @param value 值
* @return 增加状态
*/
@Override
public boolean zAdd(String key, long score, String value) {
Boolean result = redisTemplate.opsForZSet().add(key, value, score);
return result;
}
/**
* 获取 某key 下 某一分值区间的队列
*
* @param key 缓存key
* @param from 开始时间
* @param to 结束时间
* @return 数据
*/
@Override
public Set<ZSetOperations.TypedTuple<Object>> zRangeByScore(String key, int from, long to) {
Set<ZSetOperations.TypedTuple<Object>> set = redisTemplate.opsForZSet().rangeByScoreWithScores(key, from, to);
return set;
}
/**
* 移除 Zset队列值
*
* @param key key值
* @param value 删除的集合
* @return 删除数量
*/
@Override
public Long zRemove(String key, String... value) {
return redisTemplate.opsForZSet().remove(key, value);
}
延时队列 抽象类,具体延时队列需继承
package cn.lili.trigger.plugin.delay;
import cn.hutool.json.JSONUtil;
import cn.lili.trigger.plugin.cache.Cache;
import cn.lili.trigger.plugin.util.ThreadPoolUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.DefaultTypedTuple;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.util.Calendar;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 延时队列工厂
*
* @author paulG
* @since 2020/11/7
**/
@Slf4j
public abstract class AbstractDelayQueueMachineFactory {
@Autowired
private Cache cache;
/**
* 插入任务id
*
* @param jobId 任务id(队列内唯一)
* @param time 延时时间(单位 :秒)
* @return 是否插入成功
*/
public boolean addJob(String jobId, Integer time) {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, time);
long delaySeconds = instance.getTimeInMillis() / 1000;
boolean result = cache.zAdd(setDelayQueueName(), delaySeconds, jobId);
log.info("增加延时任务, 缓存key {}, 等待时间 {}", setDelayQueueName(), time);
return result;
}
/**
* 延时队列机器开始运作
*/
private void startDelayQueueMachine() {
log.info("延时队列机器{}开始运作", setDelayQueueName());
// 监听redis队列
while (true) {
try {
// 获取当前时间的时间戳
long now = System.currentTimeMillis() / 1000;
// 获取当前时间前的任务列表
Set<DefaultTypedTuple> tuples = cache.zRangeByScore(setDelayQueueName(), 0, now);
// 如果任务不为空
if (!CollectionUtils.isEmpty(tuples)) {
log.info("延时任务开始执行任务:{}", JSONUtil.toJsonStr(tuples));
for (DefaultTypedTuple tuple : tuples) {
String jobId = (String) tuple.getValue();
// 移除缓存,如果移除成功则表示当前线程处理了延时任务,则执行延时任务
Long num = cache.zRemove(setDelayQueueName(), jobId);
// 如果移除成功, 则执行
if (num > 0) {
ThreadPoolUtil.execute(() -> invoke(jobId));
}
}
}
} catch (Exception e) {
log.error("处理延时任务发生异常,异常原因为{}", e.getMessage(), e);
} finally {
// 间隔5秒钟搞一次
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 最终执行的任务方法
*
* @param jobId 任务id
*/
public abstract void invoke(String jobId);
/**
* 要实现延时队列的名字
*/
public abstract String setDelayQueueName();
@PostConstruct
public void init() {
new Thread(this::startDelayQueueMachine).start();
}
}
延时队列示例实现
package cn.lili.trigger.plugin.delay;
import cn.hutool.json.JSONUtil;
import cn.lili.trigger.plugin.interfaces.TimeTrigger;
import cn.lili.trigger.plugin.interfaces.TimeTriggerExecutor;
import cn.lili.trigger.plugin.model.TimeTriggerMsg;
import cn.lili.trigger.plugin.util.SpringContextUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 测试延时队列
*
* @author paulG
* @version v4.1
* @date 2020/11/17 7:19 下午
* @description
* @since 1
*/
@Component
public class TestDelayQueue extends AbstractDelayQueueMachineFactory {
@Autowired
private TimeTrigger timeTrigger;
@Override
public void invoke(String jobId) {
TimeTriggerMsg timeTriggerMsg = JSONUtil.toBean(jobId, TimeTriggerMsg.class);
TimeTriggerExecutor executor = (TimeTriggerExecutor) SpringContextUtil.getBean(timeTriggerMsg.getTriggerExecutor());
executor.execute(timeTriggerMsg.getParam());
}
@Override
public String setDelayQueueName() {
return "test_delay";
}
}
延时任务接口
package cn.lili.trigger.plugin.interfaces;
import cn.lili.trigger.plugin.model.TimeTriggerMsg;
/**
* 延时执行接口
*
* @author Chopper
*/
public interface TimeTrigger {
/**
* 添加延时任务
*
* @param timeTriggerMsg 延时任务信息
*/
void add(TimeTriggerMsg timeTriggerMsg);
}
Redis延时任务实现类
package cn.lili.trigger.plugin.interfaces.impl;
import cn.hutool.json.JSONUtil;
import cn.lili.trigger.plugin.delay.TestDelayQueue;
import cn.lili.trigger.plugin.interfaces.TimeTrigger;
import cn.lili.trigger.plugin.model.TimeTriggerMsg;
import cn.lili.trigger.plugin.util.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* redis 延时任务
*
* @author Chopper
* @version v1.0
* 2021-06-09 11:00
*/
@Component
@Slf4j
public class RedisTimerTrigger implements TimeTrigger {
@Autowired
private TestDelayQueue testDelayQueue;
@Override
public void add(TimeTriggerMsg timeTriggerMsg) {
//计算延迟时间 执行时间-当前时间
Integer delaySecond = Math.toIntExact(timeTriggerMsg.getTriggerTime() - DateUtil.getDateline());
//设置延时任务
if (Boolean.TRUE.equals(testDelayQueue.addJob(JSONUtil.toJsonStr(timeTriggerMsg), delaySecond))) {
log.info("定时执行在【" + DateUtil.toString(timeTriggerMsg.getTriggerTime(), "yyyy-MM-dd HH:mm:ss") + "】,消费【" + timeTriggerMsg.getParam().toString() + "】");
} else {
log.error("延时任务添加失败:{}", timeTriggerMsg);
}
}
}
延时任务执行器接口
package cn.lili.trigger.plugin.interfaces;
/**
* 延时任务执行器接口
*
* @author Chopper
*/
public interface TimeTriggerExecutor {
/**
* 执行任务
*
* @param object 任务参数
*/
void execute(Object object);
}
延时任务实现
package cn.lili.trigger.plugin.interfaces.execute;
import cn.lili.trigger.plugin.interfaces.TimeTriggerExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* test执行器
*
* @author Chopper
* @version v1.0
* 2021-06-09 10:49
*/
@Component
@Slf4j
public class TestTimeTriggerExecutor implements TimeTriggerExecutor {
@Override
public void execute(Object object) {
log.info("执行器具执行任务{}", object);
}
}
延时任务消5息模型
package cn.lili.trigger.plugin.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 延时任务消息
*
* @author Chopper
* @version v1.0
* @since 2019-02-12 下午5:46
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TimeTriggerMsg implements Serializable {
private static final long serialVersionUID = 8897917127201859535L;
/**
* 执行器 执行时间
*/
private Long triggerTime;
/**
* 执行器beanId
*/
private String triggerExecutor;
/**
* 执行器参数
*/
private Object param;
}
控制器
package cn.lili.trigger.controller;
import cn.lili.trigger.plugin.interfaces.TimeTrigger;
import cn.lili.trigger.plugin.model.TimeTriggerMsg;
import cn.lili.trigger.plugin.util.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private TimeTrigger timeTrigger;
@GetMapping
public void test(Integer seconds) {
Long executeTime = DateUtil.getDateline() + 5;
if (seconds != null) {
executeTime = DateUtil.getDateline() + seconds;
}
TimeTriggerMsg timeTriggerMsg = new TimeTriggerMsg(executeTime, "testTimeTriggerExecutor", "test params");
timeTrigger.add(timeTriggerMsg);
}
}
分布式 redis 延时任务 基于 springboot 示例的更多相关文章
- 基于SpringBoot AOP面向切面编程实现Redis分布式锁
基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式锁 基于SpringBoot AOP面向切面编程实现Redis分布式 ...
- c#实例化继承类,必须对被继承类的程序集做引用 .net core Redis分布式缓存客户端实现逻辑分析及示例demo 数据库笔记之索引和事务 centos 7下安装python 3.6笔记 你大波哥~ C#开源框架(转载) JSON C# Class Generator ---由json字符串生成C#实体类的工具
c#实例化继承类,必须对被继承类的程序集做引用 0x00 问题 类型“Model.NewModel”在未被引用的程序集中定义.必须添加对程序集“Model, Version=1.0.0.0, Cu ...
- 分布式限流组件-基于Redis的注解支持的Ratelimiter
原文:https://juejin.im/entry/5bd491c85188255ac2629bef?utm_source=coffeephp.com 在分布式领域,我们难免会遇到并发量突增,对后端 ...
- Redis分布式锁升级版RedLock及SpringBoot实现
分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式.但是现在 ...
- 基于springboot+bootstrap+mysql+redis搭建一套完整的权限架构【六】【引入bootstrap前端框架】
https://blog.csdn.net/linzhefeng89/article/details/78752658 基于springboot+bootstrap+mysql+redis搭建一套完整 ...
- springboot中加分布式redis锁
分布式redis锁,spring-boot-starter-data-redis,RedisTemplate 公司聊天的聊天系统,近期出现多个客服并发接待同一个客户的记录,经排查,是由于代码加的同步锁 ...
- 基于SpringBoot+Redis的Session共享与单点登录
title: 基于SpringBoot+Redis的Session共享与单点登录 date: 2019-07-23 02:55:52 categories: 架构 author: mrzhou tag ...
- 使用Redis为注册中心的Dubbo微服务架构(基于SpringBoot)
title: 使用Redis为注册中心的Dubbo微服务架构(基于SpringBoot) date: 2019-07-30 14:06:29 categories: 架构 author: mrzhou ...
- 分布式锁(2) ----- 基于redis的分布式锁
分布式锁系列文章 分布式锁(1) ----- 介绍和基于数据库的分布式锁 分布式锁(2) ----- 基于redis的分布式锁 分布式锁(3) ----- 基于zookeeper的分布式锁 代码:ht ...
随机推荐
- hdu4807枚举费用流
题意: 给你一个有向图,每条边上都有每一时刻的最大流量,有k个人在点0,他们要去点n-1,问你最晚到达的那个人最快要多久. 思路: 这个题目做了很多次,用过费用流,也用过最大流,结 ...
- POJ1698 最大流或者匈牙利
题意: 一个人他有n个任务,每个任务都有一些限制: (1)只能在一个星期中指定的日子去做,比如周1 2 6啥的 (2)总工作量有几天,就是一共要工作几天 (3)必须在几周之内完成,就 ...
- POJ1376简单广搜
题意: 给你一个n*m的矩阵,然后给你机器人的起点和终点,还有起点的方向,然后每次机器人有两种操作,左右旋转90度,或者是朝着原来的方向走1,2或者3步,机器人再走的过程中不能碰到格子,也 ...
- Win64 驱动内核编程-30.枚举与删除线程回调
枚举与删除线程回调 进程回调可以监视进程的创建和退出,这个在前面的章节已经总结过了.某些游戏保护的驱动喜欢用这个函数来监视有没有黑名单中的程序运行,如果运行则阻止运行或者把游戏退出.而线程回调则通常用 ...
- properties和yml进行对比
properties和yml进行对比 概念 springboot支持两种格式的配置文件,一种是yml,一种是properties,默认的文件名是application.properties或者appl ...
- Mac下配置Git 的全局忽略文件
$ git config --global core.excludesfile ~/.gitignore_global $ vim ~/.gitignore_global #配置文件参考如下 # fo ...
- 【微信小程序】--bindtap参数传递,配合wx.previewImage实现多张缩略图预览
本文为原创随笔,纯属个人理解.如有错误,欢迎指出. 如需转载请注明出处 在微信小程序中预览图片分为 a.预览本地相册中的图片. b.预览某个wxml中的多张图片. 分析:实质其实是一样的.都是给wx. ...
- Raspberry PI 4B 安装和配置 Raspbian
做记录,以备之后需要,待完成中 目录 做记录,以备之后需要,待完成中 下载镜像和安装程序 ssh 远程访问 下载镜像和安装程序 Raspbian: installer: ssh 远程访问 开启ssh ...
- 【转】docker打包python应用
转自https://www.cnblogs.com/shenh/p/9518343.html 一.前言 容器使用沙箱机制,互相隔离,优势在于让各个部署在容器的里的应用互不影响,独立运行,提供更高的安全 ...
- 使用constexpr时遇到的小坑
最近在使用constexpr的时候无意中踩了个小坑. 下面给个小示例: #include <iostream> constexpr int n = 10; constexpr char * ...