前面我们在数据库初始化时额外创建了一张任务表,用来模拟处理任务:
key | 模拟业务 |
---|---|
sendMail | 模拟用户注册后给用户发送邮件任务,多线程异步任务处理 |
analysisLog | 模拟每晚定时分析日志业务,定时任务处理 |
异步任务
异步任务通过方法上的@Async("taskExecutor")
和启动类的@EnableAsync
注解实现,@Async
中的参数指定了异步任务使用的的线程池。
首先,实现AsyncUserService服务接口:
接口:
package com.example.demospringboot.service;
public interface AsyncUserService {
void sendMailTask();
}
对应实现:
package com.example.demospringboot.service.impl;
import com.example.demospringboot.dao.JobMapper;
import com.example.demospringboot.task.AsyncTasks;
import com.example.demospringboot.service.AsyncUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AsyncUserServiceImpl implements AsyncUserService {
@Autowired
private AsyncTasks asyncTasks;
@Autowired
JobMapper jobMapper;
@Override
public void sendMailTask() {
if (jobMapper.getSendmail() > 0) {
asyncTasks.doAsyncTask("doSendMailTask");
jobMapper.setSendmail(0); // 发送结束标记
}
};
}
用到的task类如下:
package com.example.demospringboot.task;
import com.example.demospringboot.utils.ThreadUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class AsyncTasks {
public static Random random = new Random();
// @Async注解中的参数指向异步任务的线程池
@Async("taskExecutor")
public CompletableFuture<String> doAsyncTask(String taskNo){
log.info("start AsyncTask: {}", taskNo);
long start = System.currentTimeMillis();
ThreadUtils.sleepUtil(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("end:{}, task:{} ms", taskNo, end - start);
return CompletableFuture.completedFuture("finished");
}
}
(1)异步任务通过方法上的@Async("taskExecutor")
和启动类的@EnableAsync
注解实现,@Async
中的参数指定了异步任务使用的的线程池。调用异步方法时不会等待方法执行完,调用即过,被调用方法在自己的线程池中奔跑。
(2)多线程执行的返回值是Future类型或void。Future是非序列化的,微服务架构中有可能传递失败。spring boot推荐使用的CompletableFuture来返回异步调用的结果。
用到的thread工具类如下:
package com.example.demospringboot.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Repository;
import java.util.concurrent.ThreadPoolExecutor;
@Repository
@Slf4j
public class ThreadUtils {
public static final int MAX_POOL_SIZE = 2;
public static final String SCHED_EXECUTOR_POOL_PREFIX = "sched-exe-" + MAX_POOL_SIZE + "-";
public static final String ASYNC_EXECUTOR_POOL_PREFIX = "async-exe-" + MAX_POOL_SIZE + "-";
public static final String ASYNC_TASK_POOL_PREFIX = "async-task-" + MAX_POOL_SIZE + "-";
// 自定义AsyncTask线程池
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(MAX_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(MAX_POOL_SIZE);
executor.setKeepAliveSeconds(0);
executor.setThreadNamePrefix(ASYNC_TASK_POOL_PREFIX);
// 如果添加到线程池失败,那么主线程会自己去执行该任务
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
// 启动Executor的线程池
public static ThreadPoolTaskExecutor getThreadPool(String threadNamePrefix) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(MAX_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(MAX_POOL_SIZE);
executor.setKeepAliveSeconds(0);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
public static void sleepUtil(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
log.error("{}", e);
}
}
}
线程池用的是ThreadPoolTaskExecutor 。Executor 顾名思义是专门用来处理多线程相关的一个接口,所有线程相关的类都实现了这个接口,里面有一个execute()方法,用来执行线程,线程池主要提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。
ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理,是spring core包中提供的,而ThreadPoolExecutor是JDK中的JUC。
参数说明:
- corePoolSize:核心线程数
- queueCapacity:任务队列容量(阻塞队列)
- maxPoolSize:最大线程数
- keepAliveTime:线程空闲时间
- rejectedExecutionHandler:任务拒绝处理器
异步任务会先占用核心线程,核心线程满了其他任务进入队列等待;在缓冲队列也满了之后才会申请超过核心线程数的线程来进行处理。当线程数已经达到maxPoolSize,且队列已满,线程池可以调用这四个策略处理:- AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
- DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
- DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
- CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。
- 也可以自己实现RejectedExecutionHandler接口,可自定义处理器
为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。需考虑好默认线程池的配置和多任务情况下的线程池隔离。
上述服务我们就用不同线程池的JobManager进行管理:
package com.example.demospringboot.job;
import com.example.demospringboot.service.AsyncUserService;
import com.example.demospringboot.utils.ThreadUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AsyncJobManager {
private static final ThreadPoolTaskExecutor ASYNC_EXECUTOR_POOL =
ThreadUtils.getThreadPool(ThreadUtils.ASYNC_EXECUTOR_POOL_PREFIX);
@Autowired
private AsyncUserService asyncUserService;
public void startSyncExecutor() {
ASYNC_EXECUTOR_POOL.execute(new AsyncExecutor(asyncUserService));
}
static class AsyncExecutor implements Runnable {
private AsyncUserService asyncUserService;
public AsyncExecutor(AsyncUserService asyncUserService) {
this.asyncUserService = asyncUserService;
}
@Override
public void run() {
while (true) {
asyncUserService.sendMailTask();
// sleep 1s
ThreadUtils.sleepUtil(1000L);
}
}
}
}
定时任务
定时任务通过在主类家@EnableScheduling
注解,在任务类加@Scheduled
实现。
同样地,先实现SchedUserService服务接口:
接口:
package com.example.demospringboot.service;
public interface SchedUserService {
void AnalysisLogTask();
}
对应实现:
package com.example.demospringboot.service.impl;
import com.example.demospringboot.ScheduledTasks;
import com.example.demospringboot.dao.JobMapper;
import com.example.demospringboot.service.SchedUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SchedUserServiceImpl implements SchedUserService {
@Autowired
private ScheduledTasks scheduledTasks;
@Autowired
JobMapper jobMapper;
@Override
public void AnalysisLogTask() {
if (jobMapper.getAnalysisLog() > 0) {
scheduledTasks.AnalysisLogTask();
}
};
}
对应的task如下:
package com.example.demospringboot;
import com.example.demospringboot.utils.ThreadUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@Component
@AllArgsConstructor
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* 秒 分 时 日 月 周几
* 0 * * * * MON-FRI
*/
@Scheduled(cron = "10 0 0 * * ?")
public void AnalysisLogTask() {
log.info("AnalysisLogTask start:" + dateFormat.format(new Date()));
ThreadUtils.sleepUtil(10000);
log.info("AnalysisLogTask end:" + dateFormat.format(new Date()));
}
}
我们来详细了解下cron表达式:
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作业
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
SchedJobManager进行任务管理:
package com.example.demospringboot.job;
import com.example.demospringboot.service.SchedUserService;
import com.example.demospringboot.utils.ThreadUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SchedJobManager {
private static final ThreadPoolTaskExecutor SCHED_EXECUTOR_POOL =
ThreadUtils.getThreadPool(ThreadUtils.SCHED_EXECUTOR_POOL_PREFIX);
@Autowired
private SchedUserService schedUserService;
public void startSchedExecutor() {
SCHED_EXECUTOR_POOL.execute(new Executor(schedUserService));
}
static class Executor implements Runnable {
private SchedUserService schedUserService;
public Executor(SchedUserService schedUserService) {
this.schedUserService = schedUserService;
}
@Override
public void run() {
while (true) {
schedUserService.AnalysisLogTask();
// sleep 1s
ThreadUtils.sleepUtil(1000L);
}
}
}
}
主启动类
主类如下,同时开启任务:
package com.example.demospringboot;
import com.example.demospringboot.job.SchedJobManager;
import com.example.demospringboot.job.AsyncJobManager;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.cache.annotation.EnableCaching;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import com.spring4all.swagger.EnableSwagger2Doc;
@Slf4j
@EnableCaching
@EnableAsync
@SpringBootApplication
@EnableSwagger2Doc
// 需要指定扫描的类,并在配置文件指定mybatis.mapper-locations为对应的xml路径
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication implements CommandLineRunner {
@Autowired
private SchedJobManager schedJobManager;
@Autowired
private AsyncJobManager asyncJobManager;
@Autowired
private DataSource dataSource;
public static void main(String[] args) {
SpringApplication.run(DemospringbootApplication.class, args);
}
@Override
public void run(String... strings) throws SQLException {
initDatabase();
schedJobManager.startSchedExecutor();
asyncJobManager.startSyncExecutor();
}
private void initDatabase() throws SQLException {
log.info("======== 自动初始化数据库开始 ========");
Resource initData = new ClassPathResource("schema.sql");
Connection connection = null;
try {
connection = dataSource.getConnection();
ScriptUtils.executeSqlScript(connection, initData);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
if (connection != null) {
connection.close();
}
}
log.info("======== 自动初始化数据库结束 ========");
}
}
主启动类实现了CommandLineRunner 接口,会直接执行run方法。
我们在其中调用了对应JobManager的startExecutor方法,用线程池execute方法启动了对应线程类的run方法。文章来源:https://www.toymoban.com/news/detail-680707.html
github地址:https://github.com/JackyZhang888/springboot/tree/main/user-system-demo文章来源地址https://www.toymoban.com/news/detail-680707.html
到了这里,关于Spring Boot实践八--用户管理系统(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!