微服务难不难,不难!无非就是一个消费方,一个生产方,一个注册中心,然后就是实现一些微服务,其实微服务的难点在于治理,给你一堆

微服务,如何来管理?这就有很多方面了,比如容器化,服务间通信,服务上下线发布。我今天要说的是任务调度,如果我们将全部服务中那

些任务都拿出来统一管理,不在服务内使用Scheduled或者Quartz,是不是很爽呢,而且大神们已经帮我们实现了xxl-job,拿来摩擦一下吧。

作者原创文章,谢绝一切转载,违者必究!

本文只发表在"公众号"和"博客园",其他均属复制粘贴!如果觉得排版不清晰,请查看公众号文章。

准备:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Lombok0.28/SpringBoot2.2.4RELEASE/mybatisPlus3.3.0/Soul2.1.2/Dubbo2.7.5/Druid1.2.21/

Zookeeper3.5.5/Mysql8.0.11/Redis5.0.5/Skywalking7.0.0/XXL-JOB2.2.1

难度: 新手--战士--老兵--大师

目标:

  1. 对当天注册成功的用户,发送产品促销邮件。使用BEAN模式实现。
  2. 每5分钟定时扫描订单数据,发现当天30分钟未付款的订单,直接删除。使用HTTP模式实现。

说明:

为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。源码地址:https://github.com/xiexiaobiao/vehicle-shop-admin

1 原理

“调度中心”使用线程池对每个任务隔离运行,并按照Cron表达式将任务分配给具体的“执行器”,其中,调度中心和执行器均可实现HA负载均衡。

HA架构如下图所示,多调度中心必须使用同一DB源,并确保机器时钟一致

2 步骤

2.1 下载运行

下载xxl-job源码,略!再运行其中的sql初始化脚本。IDE打开,先启动xxl-job-admin项目,打开http://localhost:8080/xxl-job-admin/

默认账号密码 admin/123456,即可进入调度中心UI。

2.2 改造vehicle-shop-admin项目

我这里只改了customer 和 order 模块,先以order为例: resources/config/application-dev.yml 添加的配置:

### xxl-job
xxl:
job:
admin:
#admin address list, such as "http://address" or "http://address01,http://address02"
addresses: http://127.0.0.1:8080/xxl-job-admin #调度中心地址
accessToken: 6eccc15a-5fa2-4737-a606-f74d4f3cee61 #需要与调度中心配对使用
# 执行器配置信息
executor:
appname: vehicle-admin-order-service
address: #default use address to registry , otherwise use ip:port if address is null
ip: 127.0.0.1
port: 9998
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: 30 #日志保留时长

以上代码解析:accessToken需要与调度中心配对使用,要么都用,要么都不用。addresses调度中心地址可以有多个,直接逗号隔开即可。

com.biao.shop.order.conf.XxlJobConfig

@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class); @Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays; @Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor;
}
}

以上代码解析:XxlJobSpringExecutor是具体执行器类,结合配置文件和SpringBean机制,完成Bean自动注入,实现OnLine 机器地址自动注册。

com.biao.shop.order.controller.OrderController

@RestController
@RequestMapping("/order")
public class OrderController {
...
// 省略部分代码
...
@SoulClient(path = "/vehicle/order/autoCancel", desc = "自动取消未支付订单")
@GetMapping("/autoCancel")
public ObjectResponse<Integer> autoCancelOrder(){
return orderService.autoCancelOrder();
}
}

以上代码解析: @SoulClient为soul网关注解,即网关转发地址,详细可以看我之前的文章,有关Soul网关的。

com.biao.shop.order.impl.OrderServiceImpl

@Service
public class OrderServiceImpl extends ServiceImpl<ShopOrderDao, ShopOrderEntity> implements OrderService { ...
// 省略部分代码
... @Override
public ObjectResponse<Integer> autoCancelOrder() {
ObjectResponse<Integer> response = new ObjectResponse<>();
try{
// 查找当天30分钟内未付款订单
List<ShopOrderEntity> orderEntityList = shopOrderDao.selectList(new LambdaQueryWrapper<ShopOrderEntity>()
.gt(ShopOrderEntity::getGenerateDate, LocalDate.now())
.lt(ShopOrderEntity::getGenerateDate,LocalDateTime.now().minusMinutes(30L)));
if (!Objects.isNull(orderEntityList) && !orderEntityList.isEmpty()){
int result = shopOrderDao.deleteBatchIds(orderEntityList);
response.setCode(RespStatusEnum.SUCCESS.getCode());
response.setMessage(RespStatusEnum.SUCCESS.getMessage());
response.setData(result);
}
return response;
}catch (Exception e){
response.setCode(RespStatusEnum.FAIL.getCode());
response.setMessage(RespStatusEnum.FAIL.getMessage());
response.setData(null);
return response;
}
} /**
* 这里为了演示http模式,直接使用参数:
* url: http://127.0.0.1:9195/order/vehicle/order/autoCancel
* method: get
* data: content
*/
@XxlJob("autoCancelOrderJobHandler")
public ReturnT<String> autoCancelOrderJob( String param ){
// param parse
if (param==null || param.trim().length()==0) {
XxlJobLogger.log("param["+ param +"] invalid.");
return ReturnT.FAIL;
}
String[] httpParams = param.split("\n");
String url = null;
String method = null;
String data = null;
for (String httpParam: httpParams) {
if (httpParam.startsWith("url:")) {
url = httpParam.substring(httpParam.indexOf("url:") + 4).trim();
}
if (httpParam.startsWith("method:")) {
method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase();
System.out.println("method>>>>>>>>"+ method);
}
if (httpParam.startsWith("data:")) {
data = httpParam.substring(httpParam.indexOf("data:") + 5).trim();
}
} // param valid
if (url==null || url.trim().length()==0) {
XxlJobLogger.log("url["+ url +"] invalid.");
return ReturnT.FAIL;
}
// 限制只支持 "GET" 和 "POST"
if (method==null || !Arrays.asList("GET", "POST").contains(method)) {
XxlJobLogger.log("method["+ method +"] invalid.");
return ReturnT.FAIL;
} // request
HttpURLConnection connection = null;
BufferedReader bufferedReader = null;
try {
// connection
URL realUrl = new URL(url);
connection = (HttpURLConnection) realUrl.openConnection(); // connection setting
connection.setRequestMethod(method);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setReadTimeout(5 * 1000);
connection.setConnectTimeout(3 * 1000);
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); // do connection
connection.connect(); // data
if (data!=null && data.trim().length()>0) {
DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
dataOutputStream.write(data.getBytes("UTF-8"));
dataOutputStream.flush();
dataOutputStream.close();
} // valid StatusCode
int statusCode = connection.getResponseCode();
if (statusCode != 200) {
throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
} // result
bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder result = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
result.append(line);
}
String responseMsg = result.toString(); XxlJobLogger.log(responseMsg);
return ReturnT.SUCCESS;
} catch (Exception e) {
XxlJobLogger.log(e);
return ReturnT.FAIL;
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (Exception e2) {
XxlJobLogger.log(e2);
}
}
}
}

以上代码解析: 1.autoCancelOrder方法直接实现了查找当天30分钟内未付款订单并批量删除的业务需求, 2.autoCancelOrderJob方法

则是跨平台HTTP任务模式的实现,通过通用的URL连接访问方式实现跨平台,即只需有URL地址即可,外部异构系统都可接入Xxl-Job,

系统内则实现了解耦的目的。需注意的是代码中限制了"GET"/"POST",其他动词我没测试,其实这两个也够用了。

Customer模块,因需使用邮件功能,我先在com.biao.shop.common.utils.MailUtil实现了邮件发送功能,直接使用了javax.mail包,腾

讯的SMTP服务,代码明了,非本文目标知识点,不解释:

public class MailUtil {

    @Value("${spring.mail.host}")
private static String host;
@Value("${spring.mail.port}")
private static String port;
@Value("${spring.mail.sendMail}")
private static String sendMail;
@Value("${spring.mail.password}")
private static String myEmailPassword;
@Value("${spring.mail.properties.mail.smtp.auth}")
private static String fallback; // false
@Value("${spring.mail.properties.mail.smtp.socketFactory.class}")
private static String socketFactory; @Resource
private static JavaMailSender mailSender; public static boolean sendMailTo(String userName,String receiveMail) throws Exception {
// JavaMailSender javaMailSender = new JavaMailSenderImpl();
Properties props = new Properties();
props.setProperty("mail.transport.protocol", "smtp");
props.setProperty("mail.smtp.host", host);
props.setProperty("mail.smtp.port", port);
props.setProperty("mail.smtp.auth", "true");
// 如邮箱服务器要求 SMTP 连接需要使用 SSL 安全认证,则需要使用以下配置项
/* SMTP 服务器的端口 (非 SSL 连接的端口一般默认为 25, 可以不添加, 如果开启了 SSL 连接,
需要改为对应邮箱的 SMTP 服务器的端口, 具体可查看对应邮箱服务的帮助,
QQ邮箱的SMTP(SLL)端口为465或587, 其他邮箱自行去查看)*/
/*final String smtpPort = "465";
props.setProperty("mail.smtp.port", smtpPort);
props.setProperty("mail.smtp.socketFactory.class", socketFactory);
props.setProperty("mail.smtp.socketFactory.fallback", fallback);
props.setProperty("mail.smtp.socketFactory.port", smtpPort);*/ Session session = Session.getDefaultInstance(props);
// 设置为debug模式, 可以查看详细的发送 log
session.setDebug(true);
MimeMessage message = createMimeMessage(userName,session, sendMail, receiveMail);
Transport transport = session.getTransport();
transport.connect(sendMail, myEmailPassword);
mailSender.send(message);
// Send the given array of JavaMail MIME messages in batch.
// void send(MimeMessage... mimeMessages) throws MailException;
transport.close();
return true;
} static MimeMessage createMimeMessage(String userName,Session session, String sendMail, String receiveMail) throws Exception {
// MIME邮件类型,还有一种为简单邮件类型
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(sendMail, "龙岗汽车", "UTF-8"));
// 可以增加多个收件人、抄送、密送
message.setRecipient(MimeMessage.RecipientType.TO, new InternetAddress(receiveMail, userName, "UTF-8"));
// 邮件主题
message.setSubject("新品信息", "UTF-8");
// 邮件正文(可以使用html标签)
message.setContent(userName + ",您好,新品到店,快来体验", "text/html;charset=UTF-8");
// 设置发件时间
message.setSentDate(new Date());
// 保存设置
message.saveChanges();
return message;
}
}

然后在com.biao.shop.customer.impl.ShopClientServiceImpl 实现找出当天注册的用户,并发送邮件信息的业务需求:

@org.springframework.stereotype.Service
@Slf4j
public class ShopClientServiceImpl extends ServiceImpl<ShopClientDao, ShopClientEntity> implements ShopClientService {
...
// 省略部分代码
...
@Override
@XxlJob("autoSendPromotionJobHandler")
public ReturnT<String> autoSendPromotion(String param) {
try{
// 找出当天注册的用户
List<ShopClientEntity> clientEntityList =
shopClientDao.selectList(new LambdaQueryWrapper<ShopClientEntity>()
.gt(ShopClientEntity::getGenerateDate,LocalDate.now())
.lt(ShopClientEntity::getGenerateDate,LocalDate.now().plusDays(1L)));
// 发送邮件信息
if (!Objects.isNull(clientEntityList) && !clientEntityList.isEmpty()){
// shopClientEntity中需要设计用户邮箱地址,我这里简化为一个固定的邮箱地址
clientEntityList.forEach(shopClientEntity -> {
try {
MailUtil.sendMailTo(shopClientEntity.getClientName(),mailReceiverAddr);
} catch (Exception e) {
e.printStackTrace();
}
});
}
return ReturnT.SUCCESS;
}catch (Exception e){
return ReturnT.FAIL;
}
}
}
 

其他,如引入依赖之类的边角料代码我就直接略过了 !请直接看代码吧!

2.3 添加执行器

AppName对应resources/config/application-dev.yml中的 xxl.job.executor.appname 配置项,自动注册即通过引入xxl-job-core依赖

和 XxlJobSpringExecutor自动注册,手动录入,即直接填写URL地址和端口信息。

再启动应用vehicle-shop-admin项目,建议将mysql/redis作为window服务开机自启动,顺序: MySQL—>souladmin—>soulbootstrap

—>redis—>authority—>customer—>stock—>order —>business,等待一会,“OnLine 机器地址”就会显示自动注册节点,

比如下图的customer微服务:

2.4 添加任务

WEBUI界面—>任务管理:

建立发送邮件给新用户的任务,重要参数有:

  • JobHandler对应到com.biao.shop.customer.impl.ShopClientServiceImpl 中的@XxlJob("autoSendPromotionJobHandler");
  • 路由策略,即分配任务给多个执行者时的策略;阻塞处理策略,即某个执行者的一个任务还在执行,而后同类任务又到达该执行者时的处理;
  • 运行模式是指任务源码位置,几个GLUE模式是执行器代码直接在调度中心编辑并保存,BEAN则是类模式,需自定义JobHandler类,但不支持自动扫描任务并注入到执行器容器,需要手动注入,就像下图这样,点“新增”后:

建立“自动取消无效订单”任务,注意“任务参数”,对应于com.biao.shop.order.impl.OrderServiceImpl 中 ReturnT autoCancelOrderJob( String param )的实参,

方法中对 param 进行解析,注意其中的url是soul网关的地址,因为我将整个项目都做了网关转发。

操作-->执行一次,再到调度日志中查看,订单服务调度日志:

客户服务调度日志:

最后看个首页报表图,总结就是:有UI,就是爽!

留个问题请君思考:自动取消30分钟未付款的订单,你能想到多少种方案呢?

作者原创文章,谢绝一切转载,违者必究!

全文完!


我的其他文章:

只写原创,敬请关注

分布式任务调度系统 xxl-job的更多相关文章

  1. 分布式任务调度系统:xxl-job

    任务调度,通俗来说实际上就是"定时任务",分布式任务调度系统,翻译一下就是"分布式环境下定时任务系统". xxl-job一个分布式任务调度平台,其核心设计目标是 ...

  2. 基于nginx+xxl-job+springboot高可用分布式任务调度系统

    技术.原理讲解: <分布式任务调度平台XXL-JOB--源码解析一:项目介绍> <分布式任务调度平台XXL-JOB--源码解析二:基于docker搭建admin调度中心和execut ...

  3. 分布式任务调度系统xxl-job搭建

    为解决分布式环境下定时任务的可靠性,稳定性,只执行一次的特性,我找到了个大众点评开源的分布式调度任务解决完整系统,下面我将一步步深入解读该系统,从基本的使用到源码的探究 下载 https://gith ...

  4. 分布式任务调度系统xxl-job搭建(基于docker)

    一.简介 XXL-JOB是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展.现已开放源代码并接入多家公司线上产品线,开箱即用. 更多介绍,请访问官网: http://w ...

  5. 分布式任务调度系统xxl-job源码探究(一、客户端)

    前面讲了xxl-job的搭建,现在来粗略的解析下该分布式调度系统的源码,先来客户点代码 客户端源码 客户端开启的时候会向服务中心进行注册,其实现用的是jetty连接,且每隔半分钟会发送一次心跳,来告诉 ...

  6. .NET Core下的开源分布式任务调度系统ScheduleMaster-v2.0低调发布

    从1月份首次公开介绍这个项目到现在也快4个月了,期间做了一些修修补补整体没什么大的改动.2.0算是发布之后第一个大的版本更新,带来了许多新功能新特性,也修复了一些已知的bug,在此感谢在博客.Issu ...

  7. 分布式任务调度系统xxl-job源码探究(二、服务中心)

    接下来看下服务端代码 服务端源码 服务端通过管理quartz定时任务组件,分发任务 先从入口看起,由web.xml进入,可以看出,自己编写的代码从applicationcontext-xxl-job- ...

  8. 分布式任务调度系统xxl-job相关问题补充

    搭建xxl-job时可能会遇到的问题 邮箱配置不起作用报异常 以163邮箱为例,接收邮件需要开启POP3/STMP服务 光开启服务还不够,需要添加授权码 授权码为手动输入,可以与登录密码不同,所以服务 ...

  9. 分布式定时任务调度系统技术解决方案(xxl-job、Elastic-job、Saturn)

    1.业务场景 保险人管系统每月工资结算,平安有150万代理人,如何快速的进行工资结算(数据运算型) 保险短信开门红/电商双十一 1000w+短信发送(短时汇聚型) 工作中业务场景非常多,所涉及到的场景 ...

  10. 新一代分布式任务调度框架:当当elastic-job开源项目的10项特性

    作者简介: 张亮,当当网架构师.当当技术委员会成员.消息中间件组负责人.对架构设计.分布式.优雅代码等领域兴趣浓厚.目前主导当当应用框架ddframe研发,并负责推广及撰写技术白皮书.   一.为什么 ...

随机推荐

  1. Hadoop 概述(二)

    shell定时上传linux日志信息到hdfs 从标题可以分析出来,我们要使用到shell,还要推送日志信息到hdfs上. 定义出上传的路径和临时路径,并配置好上传的log日志信息.这里我使用了上一节 ...

  2. 史上最详细idea提交代码到github教程

    史上最详细idea提交代码到github教程步骤前言github上创建空项目 idea上代码关联本地gitidea上代码本地提交解决Push rejected: Push to origin/mast ...

  3. LESLIE NOTE ——你的笔记只属于你自己

    LESLIE NOTE 网站:http://www.leslienote.com 简介: [只有数据可控,才是最放心的] [只有多多备份,才是最安全的] LESLIE NOTE 是一款本地笔记软件, ...

  4. css漂亮的弧形

    我们有时会遇到要用实现一个弧形,而这样的弧形要怎么实现呢? 用图片?好像不大现实,因为这样就要无故多加载一张图片了 ,这里我们来说说怎么用css的after伪类来实现弧形. 如果想要调整弧度的话,可以 ...

  5. 使用yarn安装依赖包出现“There appears to be trouble with your network connection. Retrying...”超时的提醒

    我们在使用yarn安装依赖包文件的时候,可能会出现"There appears to be trouble with your network connection. Retrying... ...

  6. Django项目实战:解除跨域限制

    Django项目实战:解除跨域限制 在Web开发中,跨域资源共享(CORS)是一个重要的安全特性,它限制了网页只能与其同源的服务器进行交互.然而,在开发过程中,我们经常需要前端(如Vue.js.Rea ...

  7. 什么是token?token是用来干嘛的?

    从事计算机行业的朋友都听说过token这么个东西,尤其是deepseek爆火后api(大家都知道什么意思吧),但是其他行业的人就很少了解到token,下面就给大家来详细介绍一下token是什么意思?t ...

  8. bash 学习

    学习bash shell 第一天 在百度百科上找的解释 Bash,Unix shell的一种,在1987年由布莱恩·福克斯为了GNU计划而编写.1989年发布第一个正式版本,原先是计划用在GNU操作系 ...

  9. DeepSeek-V3 解读:优化效率与规模

    DeepSeek-V3 是大语言模型(LLM)领域的一项变革性进展,为开源人工智能设定了新的标杆.作为一个拥有 6710 亿参数的专家混合(Mixture-of-Experts,MoE)模型,其中每个 ...

  10. JSONObject String、实体类 list 转换

    JSONObject获取java list JSONObject  -->> JSONArray jsonObject  .getJSONArray("list") J ...