苏三说技术

「苏三说技术」 维护者目前就任于某出名互联网公司,从事开发、架构和局部管理工作。实战经验丰盛,对jdk、spring、springboot、springcloud、mybatis等开源框架源码有肯定钻研,欢送关注,和我一起交换。

30篇原创内容

公众号

大家好我是苏三,又跟大家见面了。

===================

前言

最近有几个读者私信给我,问我他们的业务场景,要用什么样的定时工作。的确,在不必的业务场景下要用不同的定时工作,其实咱们的抉择还是挺多的。我明天给大家总结10种十分实用的定时工作,总有一种是适宜你的。

一. linux自带的定时工作

crontab

不晓得你有没有遇到过这种场景:有时须要长期统计线上的数据,而后导出到excel表格中。这种需要有时较为简单,光靠写sql语句是无奈满足需要的,这就须要写java代码了。而后将该程序打成一个jar包,在线上环境执行,最初将生成的excel文件下载到本地。

为了减小对线上环境的影响,咱们个别会抉择在凌晨1-2点,趁用户量少的时候,执行统计程序。(其实凌晨4点左右,用户才是起码的)

因为工夫太晚了,咱们齐全没必要守在那里等执行后果,一个定时工作就能能够搞定。

那么,这种状况用哪种定时工作更适合呢?

答案是:linux零碎的crontab。(不过也不排除有些我的项目没部署在linux零碎中)

运行crontab -e,能够编辑定时器,而后退出如下命令:

`0 2 * * * /usr/local/java/jdk1.8/bin/java -jar /data/app/tool.jar > /logs/tool.log &`

就能够在每天凌晨2点,定时执行tool.jar程序,并且把日志输入到tool.log文件中。当然你也能够把前面的执行java程序的命令写成shell脚本,更不便保护。

应用这种定时工作反对不便批改定时规定,有界面能够对立治理配置的各种定时脚本。

crontab命令的根本格局如下:

`crontab [参数] [文件名]`

如果没有指定文件名,则接管键盘上输出的命令,并将它载入到crontab

参数性能对照表如下:

以上参数,如果没有应用-u指定用户,则默认应用的以后用户。

通过crontab -e命令编辑文件内容,具体语法如下:

`[分] [小时] [日期] [月] [星期] 具体任务`

其中:

  • 分,示意多少分钟,范畴:0-59

  • 小时,示意多少小时,范畴:0-23

  • 日期,示意具体在哪一天,范畴:1-31

  • 月,示意多少月,范畴:1-12

  • 星期,示意多少周,范畴:0-7,0和7都代表星期日

还有一些特殊字符,比方:

  • *代表如何工夫,比方:*1*** 示意每天凌晨1点执行。

  • /代表每隔多久执行一次,比方:*/5 **** 示意每隔5分钟执行一次。

  • ,代表反对多个,比方:10 7,9,12 *** 示意在每天的7、9、12点10分各执行一次。

  • -代表反对一个范畴,比方:10 7-9 *** 示意在每天的7、8、9点10分各执行一次。

此外,顺便说一下crontab须要crond服务反对,crondlinux下用来周期地执行某种工作的一个守护过程,在装置linux操作系统后,默认会装置crond服务工具,且crond服务默认就是自启动的。crond过程每分钟会定期检查是否有要执行的工作,如果有,则会主动执行该工作。

能够通过以下命令操作相干服务:

`service crond status // 查看运行状态``service crond start //启动服务``service crond stop //敞开服务``service crond restart //重启服务``service crond reload //从新载入配置`

应用crontab的优缺点:

  • 长处:不便批改定时规定,反对一些较简单的定时规定,通过文件能够对立治理配好的各种定时脚本。

  • 毛病:如果定时工作十分多,不太好找,而且必须要求操作系统是linux,否则无奈执行。

二. jdk自带的定时工作

1.Thread

各位敬爱的敌人,你没看错,Thread类真的能做定时工作。如果你看过一些定时工作框架的源码,你最初会发现,它们的底层也会应用Thread类。

实现这种定时工作的具体代码如下:

`public static void init() {` `new Thread(() -> {` `while (true) {` `try {` `System.out.println("doSameThing");` `Thread.sleep(1000 * 60 * 5);` `} catch (Exception e) {` `log.error(e);` `}` `}` `}).start();``}`

应用Thread类能够做最简略的定时工作,在run办法中有个while的死循环(当然还有其余形式),执行咱们本人的工作。有个须要特地留神的中央是,须要用try...catch捕捉异样,否则如果出现异常,就间接退出循环,下次将无奈继续执行了。

这种形式做的定时工作,只能周期性执行,不能反对定时在某个工夫点执行。

此外,该线程能够定义成守护线程,在后盾默默执行就好。

应用场景:比方我的项目中有时须要每隔10分钟去下载某个文件,或者每隔5分钟去读取模板文件生成动态html页面等等,一些简略的周期性工作场景。

应用Thread类的优缺点:

  • 长处:这种定时工作非常简单,学习成本低,容易动手,对于那些简略的周期性工作,是个不错的抉择。

  • 毛病:不反对指定某个工夫点执行工作,不反对提早执行等操作,性能过于繁多,无奈应答一些较为简单的场景。

2.Timer

Timer类是jdk专门提供的定时器工具,用来在后盾线程打算执行指定工作,在java.util包下,要跟TimerTask一起配合应用。

Timer类其实是一个任务调度器,它外面蕴含了一个TimerThread线程,在这个线程中有限循环从TaskQueue中获取TimerTask(该类实现了Runnable接口),调用其run办法,就能异步执行定时工作。咱们须要继承TimerTask类,实现它的run办法,在该办法中加上本人的业务逻辑。

实现这种定时工作的具体代码如下:

`public class TimerTest {` `public static void main(String[] args) {` `Timer timer = new Timer();` `timer.schedule(new TimerTask() {` `@Override` `public void run() {` `System.out.println("doSomething");` `}` `},2000,1000);` `}``}`

先实例化一个Timer类,而后调用它的schedule办法,在该办法中实例化TimerTask类,业务逻辑写在run办法中。schedule办法最初的两次参数别离示意:延迟时间间隔时间,单位是毫秒。下面例子中,设置的定时工作是每隔1秒执行一次,提早2秒执行。

次要蕴含6个办法:

  • schedule(TimerTask task, Date time), 指定工作task在指定工夫time执行

  • schedule(TimerTask task, long delay), 指定工作task在指定提早delay后执行

  • schedule(TimerTask task, Date firstTime,long period),指定工作task在指定工夫firstTime执行后,进行反复固定提早频率peroid的执行

  • schedule(TimerTask task, long delay, long period), 指定工作task 在指定提早delay 后,进行反复固定提早频率peroid的执行

  • scheduleAtFixedRate(TimerTask task,Date firstTime,long period), 指定工作task在指定工夫firstTime执行后,进行反复固定提早频率peroid的执行

  • scheduleAtFixedRate(TimerTask task, long delay, long period), 指定工作task 在指定提早delay 后,进行反复固定提早频率peroid的执行

不过应用Timer实现定时工作有以下问题:

  1. 因为Timer是单线程执行工作,如果其中一个工作耗时十分长,会影响其余工作的执行。

  2. 如果TimerTask抛出RuntimeException,Timer会进行所有工作的运行。

应用Timer类的优缺点:

  • 长处:十分不便实现多个周期性的定时工作,并且反对提早执行,还反对在指定工夫之后反对,性能还算弱小。

  • 毛病:如果其中一个工作耗时十分长,会影响其余工作的执行。并且如果TimerTask抛出RuntimeExceptionTimer会进行所有工作的运行,所以阿里巴巴开发者标准中不倡议应用它。

3.ScheduledExecutorService

ScheduledExecutorService是JDK1.5+版本引进的定时工作,该类位于java.util.concurrent并发包下。

ScheduledExecutorService是基于多线程的,设计的初衷是为了解决Timer单线程执行,多个工作之间会相互影响的问题。

它次要蕴含4个办法:

  • schedule(Runnable command,long delay,TimeUnit unit),带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至工作执行结束。

  • schedule(Callable<V> callable,long delay,TimeUnit unit),带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至工作执行结束,并且能够获取执行后果。

  • scheduleAtFixedRate,示意以固定频率执行的工作,如果当前任务耗时较多,超过定时周期period,则当前任务完结后会立刻执行。

  • scheduleWithFixedDelay,示意以固定延时执行工作,延时是绝对当前任务完结为终点计算开始工夫。

实现这种定时工作的具体代码如下:

`public class ScheduleExecutorTest {` `public static void main(String[] args) {` `ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);` `scheduledExecutorService.scheduleAtFixedRate(() -> {` `System.out.println("doSomething");` `},1000,1000, TimeUnit.MILLISECONDS);` `}``}`

调用ScheduledExecutorService类的scheduleAtFixedRate办法实现周期性工作,每隔1秒钟执行一次,每次提早1秒再执行。

这种定时工作是阿里巴巴开发者标准中用来代替Timer类的计划,对于多线程执行周期性工作,是个不错的抉择。

ScheduledExecutorService的优缺点:

  • 长处:基于多线程的定时工作,多个工作之间不会相干影响,反对周期性的执行工作,并且带提早性能。

  • 毛病:不反对一些较简单的定时规定。

三. spring反对的定时工作

1.spring task

spring taskspring3以上版本自带的定时工作,实现定时工作的性能时,须要引入spring-context包,目前它反对:xml注解 两种形式。

1. 我的项目实战

因为xml形式太古老了,咱们以springboot我的项目中注解形式为例。

第一步,在pom.xml文件中引入spring-context相干依赖。

`<dependency>` `<groupId>org.springframework</groupId>` `<artifactId>spring-context</artifactId>``</dependency>`

第二步,在springboot启动类上加上@EnableScheduling注解。

`@EnableScheduling``@SpringBootApplication``public class Application {` `public static void main(String[] args) {` `new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);` `}``}`

第三步,应用@Scheduled注解定义定时规定。

`@Service``public class SpringTaskTest {` `@Scheduled(cron = "${sue.spring.task.cron}")` `public void fun() {` `System.out.println("doSomething");` `}``}`

第四步,在applicationContext.properties文件中配置参数:

`sue.spring.task.cron=*/10 * * * * ?`

这样就能每隔10秒执行一次fun办法了。

2. cron规定

spring4以上的版本中,cron表达式蕴含6个参数:

`[秒] [分] [时] [日期] [月] [星期]`

还反对几个罕用的特殊符号:

  • *:示意任何工夫触发工作

  • ,:示意指定的工夫触发工作

  • -:示意一段时间内触发工作

  • /:示意从哪一个时刻开始,每隔多长时间触发一次工作。

  • ?:示意用于月中的天和周中的天两个子表达式,示意不指定值。

cron表达式参数具体含意:

  1. 秒,取值范畴:0-59,反对*,-/

  2. 分,取值范畴:0-59,反对*,-/

  3. 时,取值范畴:0-23,反对*,-/

  4. 日期,取值范畴:1-31,反对*,-/。比秒多了?,示意如果指定的星期触发了,则配置的日期变成有效。

  5. 月,取值范畴:1-12,反对*,-/

  6. 星期,取值范畴:1~7,1代表星期天,6代表星期六,其余的以此类推。反对*,-/?。比秒多了?,示意如果指定的日期触发了,则配置的星期变成有效。

常见cron表达式应用举例:

  • 0 0 0 1 * ?  每月1号零点执行

  • 0 0 2 * * ?  每天凌晨2点执行

  • 0 0 2 * * ?  每天凌晨2点执行

  • 0 0/5 11 * * ? 每天11点-11点55分,每隔5分钟执行一次

  • 0 0 18 ? * WED 每周三下午6点执行

spring task先通过ScheduledAnnotationBeanPostProcessor类的processScheduled办法,解析和收集Scheduled注解中的参数,蕴含:cron表达式。

而后在ScheduledTaskRegistrar类的afterPropertiesSet办法中,默认初始化一个单线程的ThreadPoolExecutor执行工作。

对spring task感兴趣的小伙伴,能够加我微信找我私聊。

应用spring task的优缺点:

  • 长处:spring框架自带的定时性能,springboot做了十分好的封装,开启和定义定时工作非常容易,反对简单的cron表达式,能够满足绝大多数单机版的业务场景。单个工作时,以后次的调度实现后,再执行下一次任务调度。

  • 毛病:默认单线程,如果后面的工作执行工夫太长,对前面工作的执行有影响。不反对集群形式部署,不能做数据存储型定时工作。

2.spring quartz

quartzOpenSymphony开源组织在Job scheduling畛域的开源我的项目,是由java开发的一个开源的工作日程管理系统。

quartz能做什么?

  • 作业调度:调用各种框架的作业脚本,例如shell,hive等。

  • 定时工作:在某一预约的时刻,执行你想要执行的工作。

架构图如下:

quartz蕴含的次要接口如下:

  • Scheduler 代表调度容器,一个调度容器中能够注册多个JobDetail和Trigger。

  • Job 代表工作,即要执行的具体内容。

  • JobDetail 代表具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容。

  • JobBuilder 用于定义或构建JobDetail实例。

  • Trigger 代表调度触发器,决定什么时候去调。

  • TriggerBuilder 用于定义或构建触发器。

  • JobStore 用于存储作业和任务调度期间的状态。

1. 我的项目实战

咱们还是以springboot集成quartz为例。

第一步,在pom.xml文件中引入quartz相干依赖。

`<dependency>` `<groupId>org.springframework.boot</groupId>` `<artifactId>spring-boot-starter-quartz</artifactId>``</dependency>`

第二步,创立真正的定时工作执行类,该类继承QuartzJobBean

`public class QuartzTestJob extends QuartzJobBean {` `@Override` `protected void executeInternal(JobExecutionContext context) throws JobExecutionException {` `String userName = (String) context.getJobDetail().getJobDataMap().get("userName");` `System.out.println("userName:" + userName);` `}``}`

第三步,创立调度程序JobDetail和调度器Trigger

`@Configuration``public class QuartzConfig {` `@Value("${sue.spring.quartz.cron}")` `private String testCron;` `/**` `* 创立定时工作` `*/` `@Bean` `public JobDetail quartzTestDetail() {` `JobDetail jobDetail = JobBuilder.newJob(QuartzTestJob.class)` `.withIdentity("quartzTestDetail", "QUARTZ_TEST")` `.usingJobData("userName", "susan")` `.storeDurably()` `.build();` `return jobDetail;` `}` `/**` `* 创立触发器` `*/` `@Bean` `public Trigger quartzTestJobTrigger() {` `//每隔5秒执行一次` `CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(testCron);` `//创立触发器` `Trigger trigger = TriggerBuilder.newTrigger()` `.forJob(quartzTestDetail())` `.withIdentity("quartzTestJobTrigger", "QUARTZ_TEST_JOB_TRIGGER")` `.withSchedule(cronScheduleBuilder)` `.build();` `return trigger;` `}``}`

第四步,在applicationContext.properties文件中配置参数:

`sue.spring.quartz.cron=*/5 * * * * ?`

这样就能每隔5秒执行一次QuartzTestJob类的executeInternal办法了。

CronTrigger配置格局:

`[秒] [分] [小时] [日] [月] [周] [年]`

spring quartzspring taskcron表达式规定基本一致,只是spring4以上的版本去掉了前面的,而quartzCronTrigger是非必填的,这里我就不做过多介绍了。

应用spring quartz的优缺点:

  • 长处:默认是多线程异步执行,单个工作时,在上一个调度未实现时,下一个调度工夫到时,会另起一个线程开始新的调度,多个工作之间互不影响。反对简单的cron表达式,它能被集群实例化,反对分布式部署。

  • 毛病:绝对于spring task实现定时工作老本更高,须要手动配置QuartzJobBeanJobDetailTrigger等。须要引入了第三方的quartz包,有肯定的学习老本。不反对并行调度,不反对失败解决策略和动静分片的策略等。

四. 分布式定时工作

1.xxl-job

xxl-job是公众点评(许雪里)开发的一个分布式任务调度平台,其外围设计指标是开发迅速、学习简略、轻量级、易扩大。现已凋谢源代码并接入多家公司线上产品线,开箱即用。

xxl-job框架对quartz进行了扩大,应用mysql数据库存储数据,并且内置jetty作为RPC服务调用。

次要特点如下:

  1. 有界面保护定时工作和触发规定,非常容易治理。

  2. 能动静启动或进行工作

  3. 反对弹性扩容缩容

  4. 反对工作失败报警

  5. 反对动静分片

  6. 反对故障转移

  7. Rolling实时日志

  8. 反对用户和权限治理

治理界面:

整体架构图如下:

应用quartz架构图如下:

我的项目实战

xxl-admin治理后盾部署和mysql脚本执行等这些后期筹备工作,我就不过多介绍了,有需要的敌人能够找我私聊,这些更偏差于运维的事件。

假如前期工作曾经OK了,接下来咱们须要:

第一步,在pom.xml文件中引入xxl-job相干依赖。

`<dependency>` `<groupId>com.xuxueli</groupId>` `<artifactId>xxl-job-core</artifactId>``</dependency>`

第二步,在applicationContext.properties文件中配置参数:

`xxl.job.admin.address: http://localhost:8088/xxl-job-admin/``xxl.job.executor.appname: xxl-job-executor-sample``xxl.job.executor.port: 8888``xxl.job.executor.logpath: /data/applogs/xxl-job/`

第三步,创立HelloJobHandler类继承IJobHandler类:

`@JobHandler(value = "helloJobHandler")``@Component``public class HelloJobHandler extends IJobHandler {` `@Override` `public ReturnT<String> execute(String param) {` `System.out.println("XXL-JOB, Hello World.");` `return SUCCESS;` `}``}`

这样定时工作就配置好了。

倡议把定时工作独自部署到另外一个服务中,跟api服务离开。依据我以往的教训,job大部分状况下,会对数据做批量操作,如果操作的数据量太大,可能会对服务的内存和cpu资源造成肯定的影响。

应用xxl-job的优缺点:

  • 长处:有界面治理定时工作,反对弹性扩容缩容、动静分片、故障转移、失败报警等性能。它的性能十分弱小,很多大厂在用,能够满足绝大多数业务场景。

  • 毛病:和quartz一样,通过数据库分布式锁,来管制工作不能反复执行。在工作十分多的状况下,有一些性能问题。

2.elastic-job

elastic-job是当当网开发的弹性分布式任务调度零碎,功能丰富弱小,采纳zookeeper实现分布式协调,实现工作高可用以及分片。它是专门为高并发和简单业务场景开发。

elastic-job目前是apacheshardingsphere我的项目下的一个子项目,官网地址:http://shardingsphere.apache....

elastic-job在2.x之后,出了两个产品线:Elastic-Job-LiteElastic-Job-Cloud,而咱们个别应用Elastic-Job-Lite就可能满足需要。Elastic-Job-Lite定位为轻量级无中心化解决方案,应用jar包的模式提供分布式工作的协调服务,内部仅依赖于Zookeeper。。

次要特点如下:

  • 散布式调度协调

  • 弹性扩容缩容

  • 生效转移

  • 错过执行作业重触发

  • 作业分片一致性,保障同一分片在分布式环境中仅一个执行实例

  • 自诊断并修复分布式不稳固造成的问题

  • 反对并行调度

整体架构图:

我的项目实战

第一步,在pom.xml文件中引入elastic-job相干依赖。

`<dependency>` `<groupId>com.dangdang</groupId>` `<artifactId>elastic-job-lite-core</artifactId>``</dependency>``<dependency>` `<groupId>com.dangdang</groupId>` `<artifactId>elastic-job-lite-spring</artifactId>``</dependency>`

第二步,减少ZKConfig类,配置zookeeper

`@Configuration``@ConditionalOnExpression("'${zk.serverList}'.length() > 0")``public class ZKConfig {` `@Bean` `public ZookeeperRegistryCenter registry(@Value("${zk.serverList}") String serverList,` `@Value("${zk.namespace}") String namespace) {` `return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));` `}``}`

第三步,定义一个类实现SimpleJob接口:

`public class TestJob implements SimpleJob {` `@Override` `public void execute(ShardingContext shardingContext){` `System.out.println("ShardingTotalCount:"+shardingContext.getShardingTotalCount());` `System.out.println("ShardingItem:"+shardingContext.getShardingItem());` `}``}`

第四步,减少JobConfig配置工作:

`@Configuration``public class JobConfig {` `@Value("${sue.spring.elatisc.cron}")` `private String testCron;` `@Value("${sue.spring.elatisc.itemParameters}")` `private  String shardingItemParameters;` `@Value("${sue.spring.elatisc.jobParameters}")` `private String jobParameters =;` `@Value("${sue.spring.elatisc.shardingTotalCount}")` `private int shardingTotalCount;`  `@Autowired` `private ZookeeperRegistryCenter registryCenter;` `@Bean` `public SimpleJob testJob() {` `return new TestJob();` `}` `@Bean` `public JobScheduler simpleJobScheduler(final SimpleJob simpleJob) {` `return new SpringJobScheduler(simpleJob, registryCenter, getConfiguration(simpleJob.getClass(),` `cron, shardingTotalCount, shardingItemParameters, jobParameters));` `}` `private geConfiguration getConfiguration(Class<? extends SimpleJob> jobClass,String cron,int shardingTotalCount,String shardingItemParameters,String jobParameters) {` `JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), testCron, shardingTotalCount).` `shardingItemParameters(shardingItemParameters).jobParameter(jobParameters).build();` `SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());` `LiteJobConfiguration jobConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();` `return jobConfig;` `}``}`

其中:

  • cron:cron表达式,定义触发规定。

  • shardingTotalCount:定义作业分片总数

  • shardingItemParameters:定义调配项参数,个别用分片序列号和参数用等号分隔,多个键值对用逗号分隔,分片序列号从0开始,不可大于或等于作业分片总数。

  • jobParameters:作业自定义参数

第五步,在applicationContext.properties文件中配置参数:

`spring.application.name=elasticjobDemo``zk.serverList=localhost:2181``zk.namespace=elasticjobDemo``sue.spring.elatisc.cron=0/5 * * * * ?``sue.spring.elatisc.itemParameters=0=A,1=B,2=C,3=D``sue.spring.elatisc.jobParameters=test``sue.spring.elatisc.shardingTotalCount=4`

这样定时工作就配置好了,创立定时工作的步骤,绝对于xxl-job来说要繁琐一些。

应用elastic-job的优缺点:

  • 长处:反对散布式调度协调,反对分片,适宜高并发,和一些业务相对来说较简单的场景。

  • 毛病:须要依赖于zookeeper,实现定时工作绝对于xxl-job要简单一些,要对分片规定十分相熟。

3.其余分布式定时工作

1. Saturn

Saturn是唯品会开源的一个分布式任务调度平台。取代传统的Linux Cron/Spring Batch Job的形式,做到全域对立配置,对立监控,工作高可用以及分片并发解决。

Saturn是在当当开源的Elastic-Job根底上,联合各方需要和咱们的实际见解改进而成。应用案例:唯品会、酷狗音乐、新网银行、海融易、航美在线、量富征信等。 

github地址:https://github.com/vipshop/Sa...

2. TBSchedule

 TBSchedule是阿里开发的一款分布式任务调度平台,旨在将调度作业从业务零碎中分离出来,升高或者是打消和业务零碎的耦合度,进行高效异步工作解决。

目前被广泛应用在阿里巴巴、淘宝、支付宝、京东、聚美、汽车之家、国美等很多互联网企业的流程调度零碎中。

github地址:https://github.com/taobao/TBS...

诚实说优良的定时工作还是挺多的,不是说哪种定时工作牛逼咱们就肯定要用哪种,而是要依据理论业务需要抉择。每种定时工作都有优缺点,正当抉择既能满足业务需要,又能防止资源节约,才是上上策。当然在理论的业务场景,通常会多种定时工作一起配合应用。

顺便说一句,欢送敬爱的小伙伴们,找我一起聊聊:你用过哪些定时工作,遇到过哪些问题,以及如何解决问题的。如果有相干问题也能够问我。

心愿咱们可能共同进步,一起成长。

最初说一句(求关注,别白嫖我)

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多BAT大厂的前辈交换和学习。

 集体公众号

 集体微信