mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
402 lines
27 KiB
HTML
402 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/00 开篇导读.md.html">00 开篇导读</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/01 以真实“商场停车”业务切入——需求分析.md.html">01 以真实“商场停车”业务切入——需求分析</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/02 具象业务需求再抽象分解——系统设计.md.html">02 具象业务需求再抽象分解——系统设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/03 第一个 Spring Boot 子服务——会员服务.md.html">03 第一个 Spring Boot 子服务——会员服务</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/04 如何维护接口文档供外部调用——在线接口文档管理.md.html">04 如何维护接口文档供外部调用——在线接口文档管理</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html">05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/06 服务多不易管理如何破——服务注册与发现.md.html">06 服务多不易管理如何破——服务注册与发现</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/07 如何调用本业务模块外的服务——服务调用.md.html">07 如何调用本业务模块外的服务——服务调用</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html">08 服务响应慢或服务不可用怎么办——快速失败与服务降级</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/09 热更新一样更新服务的参数配置——分布式配置中心.md.html">09 热更新一样更新服务的参数配置——分布式配置中心</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/10 如何高效读取计费规则等热数据——分布式缓存.md.html">10 如何高效读取计费规则等热数据——分布式缓存</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/SpringCloud微服务实战(完)/11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html">11 多实例下的定时任务如何避免重复执行——分布式定时任务</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/12 同一套服务如何应对不同终端的需求——服务适配.md.html">12 同一套服务如何应对不同终端的需求——服务适配</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html">13 采用消息驱动方式处理扣费通知——集成消息中间件</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html">14 Spring Cloud 与 Dubbo 冲突吗——强强联合</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html">15 破解服务中共性问题的繁琐处理方式——接入 API 网关</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/16 服务压力大系统响应慢如何破——网关流量控制.md.html">16 服务压力大系统响应慢如何破——网关流量控制</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/17 集成网关后怎么做安全验证——统一鉴权.md.html">17 集成网关后怎么做安全验证——统一鉴权</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/18 多模块下的接口 API 如何统一管理——聚合 API.md.html">18 多模块下的接口 API 如何统一管理——聚合 API</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/19 数据分库后如何确保数据完整性——分布式事务.md.html">19 数据分库后如何确保数据完整性——分布式事务</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/20 优惠券如何避免超兑——引入分布式锁.md.html">20 优惠券如何避免超兑——引入分布式锁</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/21 如何查看各服务的健康状况——系统应用监控.md.html">21 如何查看各服务的健康状况——系统应用监控</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/22 如何确定一次完整的请求过程——服务链路跟踪.md.html">22 如何确定一次完整的请求过程——服务链路跟踪</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/SpringCloud微服务实战(完)/23 结束语.md.html">23 结束语</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>11 多实例下的定时任务如何避免重复执行——分布式定时任务</h1>
|
||
<p>前面的章节,用户通过绑定手机号的注册为会员,并可以补充完个人信息,比如姓名、生日等信息,拿到用户的生日信息之后,就可以通过会员生日信息进行营销,此处就涉及到定时任务执行营销信息推送的问题。本篇就带你走入微服务下的定时任务的构建问题。</p>
|
||
<h3>定时任务选型</h3>
|
||
<p>常见的定时任务的解决方案有以下几种:</p>
|
||
<p><img src="assets/2020-05-05-021607.jpg" alt="img" /></p>
|
||
<p>右半部分基于 Java 或 Spring 框架即可支持定时任务的开发运行,左侧部分需要引入第三方框架支持。针对不同方案,作个简单介绍</p>
|
||
<ul>
|
||
<li>XXL-JOB 是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。任务调度与任务执行分离,功能很丰富,在多家公司商业产品中已有应用。官方地址:<a href="https://www.xuxueli.com/xxl-job/">https://www.xuxueli.com/xxl-job/</a></li>
|
||
<li>Elastic-Job 是一个分布式调度解决方案,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成。Elastic-Job-Lite 定位为轻量级无中心化解决方案,依赖 Zookeeper ,使用 jar 包的形式提供分布式任务的协调服务,之前是当当网 Java 应用框架 ddframe 框架中的一部分,后分离出来独立发展。</li>
|
||
<li>Quartz 算是定时任务领域的老牌框架了,出自 OpenSymphony 开源组织,完全由 Java 编写,提供内存作业存储和数据库作业存储两种方式。在分布式任务调度时,数据库作业存储在服务器关闭或重启时,任务信息都不会丢失,在集群环境有很好的可用性。</li>
|
||
<li>淘宝出品的 TBSchedule 是一个简洁的分布式任务调度引擎,基于 Zookeeper 纯 Java 实现,调度与执行同样是分离的,调度端可以控制、监控任务执行状态,可以让任务能够被动态的分配到多个主机的 JVM 中的不同线程组中并行执行,保证任务能够不重复、不遗漏的执行。</li>
|
||
<li>Timer 和 TimerTask 是 Java 基础组件库的两个类,简单的任务尚可应用,但涉及到的复杂任务时,建议选择其它方案。</li>
|
||
<li>ScheduledExecutorService 在 ExecutorService 提供的功能之上再增加了延迟和定期执行任务的功能。虽然有定时执行的功能,但往往大家不选择它作为定时任务的选型方案。</li>
|
||
<li><a href="">@EnableScheduling </a> 以注解的形式开启定时任务,依赖 Spring 框架,使用简单,无须 xml 配置。特别是使用 Spring Boot 框架时,更加方便。</li>
|
||
</ul>
|
||
<p>引入第三方分布式框架会增加项目复杂度,Timer、TimerTask 比较简单无法符合复杂的分布式定时任务,本次选择基于 注解的 <a href="">@EnableScheduling </a> 来开启我们的定时任务之旅。</p>
|
||
<h3>建立定时任务项目</h3>
|
||
<p>在 parking-project 父项目中新增基于 Spring Boot 的定时任务项目,命名为 parking-schedule-job,将基本的项目配置完毕,如端口、项目名称等等。</p>
|
||
<p>新增项目启动类</p>
|
||
<pre><code class="language-java">@SpringBootApplication
|
||
@EnableScheduling
|
||
public class ParkingScheduleJobApplication {
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(ParkingScheduleJobApplication.class, args);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>新增任务执行类</p>
|
||
<pre><code class="language-java">@Component
|
||
@Slf4j
|
||
public class UserBirthdayBasedPushTask {
|
||
//每隔 5s 输出一次日志
|
||
@Scheduled(cron = " 0/5 * * * * ?")
|
||
public void scheduledTask() {
|
||
log.info("Task running at = " + LocalDateTime.now());
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>一个简单的定时任务项目就此完成,启动项目,日志每隔 5s 输出一次。单实例执行没有问题,但仔细想想似乎不符合我们的预期:微服务架构环境下,进行横向扩展部署多实例时,每隔 5s 每个实例都会执行一次,重复执行会导致数据的混乱或糟糕的用户体验,比如本次基于会员生日推送营销短信时,用户会被短信轰炸,这肯定不是我们想看到的。即使部署了多代码实例,任务在同一时刻应当也只有任务执行才是符合正常逻辑的,而不能因为实例的增多,导致执行次数增多。</p>
|
||
<h3>分布式定时任务</h3>
|
||
<p>保证任务在同一时刻只有执行,就需要每个实例执行前拿到一个令牌,谁拥有令牌谁有执行任务,其它没有令牌的不能执行任务,通过数据库记录就可以达到这个目的。</p>
|
||
<p><img src="assets/2020-05-05-021608.jpg" alt="img" /></p>
|
||
<p>有小伙伴给出的是 A 方案,但有一个漏洞:当 select 指定记录后,再去 update 时,存在时间间隙,会导致多个实例同时执行任务,建议采用直接 update 的方案 B 更为可靠, update 更新到记录时会返回 1 ,否则是 0 。</p>
|
||
<p>这种方案还需要编写数据更新操作方法,如果这些代码都不想写,有没有什么好办法?当然有,总会有"懒"程序员帮你省事,介绍一个组件 <a href="https://github.com/lukas-krecan/ShedLock">ShedLock</a>,可以使我们的定时任务在同一时刻,最多执行一次。</p>
|
||
<p>1、引入 ShedLock 相关的 jar ,这里依旧采用 MySQL 数据库的形式:</p>
|
||
<pre><code class="language-xml"><dependency>
|
||
<groupId>net.javacrumbs.shedlock</groupId>
|
||
<artifactId>shedlock-core</artifactId>
|
||
<version>4.5.0</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>net.javacrumbs.shedlock</groupId>
|
||
<artifactId>shedlock-spring</artifactId>
|
||
<version>4.5.0</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>net.javacrumbs.shedlock</groupId>
|
||
<artifactId>shedlock-provider-jdbc-template</artifactId>
|
||
<version>4.5.0</version>
|
||
</dependency>
|
||
</code></pre>
|
||
<p>2、变更项目启动类,增加 <a href="">@EnableSchedulerLock </a> 注解,打开 ShedLock 获取锁的支持。</p>
|
||
<pre><code class="language-java">@SpringBootApplication
|
||
@EnableScheduling
|
||
@EnableSchedulerLock(defaultLockAtMostFor = "30s")
|
||
public class ParkingScheduleJobApplication {
|
||
public static void main(String[] args) {
|
||
SpringApplication.run(ParkingScheduleJobApplication.class, args);
|
||
}
|
||
@Bean
|
||
//基于 Jdbc 的方式提供的锁机制
|
||
public LockProvider lockProvider(DataSource dataSource) {
|
||
return new JdbcTemplateLockProvider(dataSource);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>3、任务执行类的方法上,同样增加 <a href="">@SchedulerLock </a> 注解,并声明定时任务锁的名称,如果有多个定时任务,要确保名称的唯一性。</p>
|
||
<p>4、新增名为 shedlock 的数据库,并新建 shedlock 数据表,表结构如下:</p>
|
||
<pre><code class="language-sql">CREATE TABLE shedlock(
|
||
`NAME` varchar(64) NOT NULL DEFAULT '' COMMENT '任务名',
|
||
`lock_until` timestamp(3) NULL DEFAULT NULL COMMENT '释放时间',
|
||
`locked_at` timestamp(3) NULL DEFAULT NULL COMMENT '锁定时间',
|
||
`locked_by` varchar(255) DEFAULT NULL COMMENT '锁定实例',
|
||
PRIMARY KEY (name)
|
||
)
|
||
</code></pre>
|
||
<p>5、修改 application.properties 中数据库连接</p>
|
||
<pre><code class="language-properties">spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
|
||
spring.datasource.url = jdbc:mysql://localhost:3306/shedlock?useUnicode=true&characterEncoding=utf-8
|
||
spring.datasource.username = root
|
||
spring.datasource.password = root
|
||
</code></pre>
|
||
<p>6、完成以上步骤,基本配置已经完成,来测试一下,在多实例运行时,同一时刻是否只有一个实施在执行任务。</p>
|
||
<pre><code>//实例 1 的日志输出
|
||
2020-03-07 21:20:45.007 INFO 67479 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:20:45.007
|
||
2020-03-07 21:20:50.011 INFO 67479 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:20:50.011
|
||
2020-03-07 21:21:15.009 INFO 67479 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:15.009
|
||
2020-03-07 21:21:30.014 INFO 67479 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:30.014
|
||
2020-03-07 21:21:40.008 INFO 67479 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:40.008
|
||
//实例 2 的日志输出
|
||
2020-03-07 21:21:20.011 INFO 67476 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:20.011
|
||
2020-03-07 21:21:25.008 INFO 67476 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:25.008
|
||
2020-03-07 21:21:30.006 INFO 67476 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:30.006
|
||
2020-03-07 21:21:35.006 INFO 67476 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:35.006
|
||
2020-03-07 21:21:45.008 INFO 67476 --- [ scheduling-1] c.m.p.s.j.t.UserBirthdayBasedPushTask : Task running at = 2020-03-07T21:21:45.008
|
||
</code></pre>
|
||
<p>可以看出每 5s 执行一次,是分布在两个实例中,同一时刻只有一个任务在执行,这与我们的预期是一致。数据库表记录(有两个定时任务的情况下):</p>
|
||
<p><img src="assets/2020-05-05-021610.jpg" alt="img" /></p>
|
||
<h3>定时发送营销短信</h3>
|
||
<p>初步框架构建完成,现在填充据会员生日信息推送营销短信的功能。</p>
|
||
<blockquote>
|
||
<p>有小伙伴一听说定时任务,一定要找服务压力小的时间段来处理,索性放到凌晨。但凌晨让用户收到营销短信,真的好吗?所以还是要考虑产品的用户体验,不能盲目定时。</p>
|
||
</blockquote>
|
||
<p>前面服务调用章节我们已经学会了服务间的调用 ,这次是定时任务项目要调用会员服务里的方法,依旧采用 Feign 的方式进行。编写 MemberServiceClient 接口,与会员服务中的会员请求响应类保持一致</p>
|
||
<pre><code class="language-java">@FeignClient(value = "member-service", fallback = MemberServiceFallback.class)
|
||
public interface MemberServiceClient {
|
||
@RequestMapping(value = "/member/list", method = RequestMethod.POST)
|
||
public CommonResult<List<Member>> list() throws BusinessException;
|
||
@RequestMapping(value = "/member/getMember", method = RequestMethod.POST)
|
||
public CommonResult<Member> getMemberInfo(@RequestParam(value = "memberId") String memberId);
|
||
}
|
||
</code></pre>
|
||
<p>任务执行类编写业务逻辑,这里用到了 Member 实体,但这个实体是维护在会员服务中的,未对外公开。<code>*对于一些公用类,可以抽取到一个公共项目中,供各项目间相互引用,而不是维护多份。*</code></p>
|
||
<pre><code class="language-java">@Component
|
||
@Slf4j
|
||
public class UserBirthdayBasedPushTask {
|
||
@Autowired
|
||
MemberServiceClient memberService;
|
||
@Scheduled(cron = " 0/5 * * * * ?")
|
||
@SchedulerLock(name = "scheduledTaskName")
|
||
public void scheduledTask() {
|
||
CommonResult<List<Member>> members;
|
||
try {
|
||
members = memberService.list();
|
||
List<Member> resp = members.getRespData();
|
||
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||
LocalDateTime time = LocalDateTime.now();
|
||
String curTime = df.format(time);
|
||
for (Member one : resp) {
|
||
//当天生日的推送营销短信
|
||
if (curTime.equals(one.getBirth())) {
|
||
log.info(" send sms to " + one.getPhone() );
|
||
}
|
||
}
|
||
} catch (BusinessException e) {
|
||
log.error("catch exception " + e.getMessage());
|
||
}
|
||
log.info("Task running at = " + LocalDateTime.now());
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>启动会员服务、定时任务两个项目,测试业务逻辑的是否运行正常。定时任务执行时,发现出现异常:</p>
|
||
<pre><code class="language-shell">Caused by: org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `com.mall.parking.common.bean.CommonResult` out of START_ARRAY token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `com.mall.parking.common.bean.CommonResult` out of START_ARRAY token at [Source: (PushbackInputStream); line: 1, column: 1]
|
||
</code></pre>
|
||
<p>定位原因: CommonResult 对象中含有 Member List 对象集合,JSON 对象解析时的结构应该为 <code>{}</code>,但返回值是<code>[]</code>,肯定会解析异常。需要将 Feign 接口变更为原始的 JSON 字符串形式。</p>
|
||
<pre><code class="language-java">//MemberServiceClient 接口方法变更为此
|
||
@RequestMapping(value = "/member/list", method = RequestMethod.POST)
|
||
public String list() throws BusinessException;
|
||
</code></pre>
|
||
<p>任务执行类变更操作方式,如下</p>
|
||
<pre><code class="language-java"> @Scheduled(cron = " 0/5 * * * * ?")
|
||
@SchedulerLock(name = "scheduledTaskName")
|
||
public void scheduledTask() {
|
||
try {
|
||
String members = memberService.list();
|
||
List<Member> array = JSONArray.parseArray(members, Member.class);
|
||
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||
LocalDateTime time = LocalDateTime.now();
|
||
String curTime = df.format(time);
|
||
for (Member one : array) {
|
||
if (curTime.equals(one.getBirth())) {
|
||
log.info(" send sms to " + one.getPhone() );
|
||
}
|
||
}
|
||
} catch (BusinessException e) {
|
||
log.error("catch exception " + e.getMessage());
|
||
}
|
||
log.info("Task running at = " + LocalDateTime.now());
|
||
}
|
||
</code></pre>
|
||
<p>再重新启动两个项目,测试任务已经可以正常执行。如果你的项目中还需要更多的定时任务的话,参照这种方式编写相应代码即可。</p>
|
||
<p>本章节从定时任务入手,谈了几个定时任务的解决方案,接着引入分布式定时任务来完成我们的短信营销任务,完整的实施一次分布式定时任务。给你留下个动手题目吧:如果使用 elastic-job 组件,又当如何实现这个分布式定时任务呢?</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/SpringCloud微服务实战(完)/10 如何高效读取计费规则等热数据——分布式缓存.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/SpringCloud微服务实战(完)/12 同一套服务如何应对不同终端的需求——服务适配.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709975a1aac03cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|