异步的八种实现方式
- 线程Thread
- Future
- 异步框架CompletableFuture
- Spring注解@Async
- Spring ApplicationEvent事件
- 消息队列
- 第三方异步框架,比如Hutool的ThreadUtil
- Guava异步
一、线程异步
public class AsyncThread extends Thread{
@Override
public void run(){
System.out.println("Current thread name:" + Thread.currentThread().getName() + " Send email success!");
}
public static void main(String[] args) {
AsyncThread asyncThread = new AsyncThread();
asyncThread.run();
}
}
当然如果每次都创建一个Thread线程,频繁的创建、销毁,浪费系统资源,我们可以采用线程池:
private ExecutorService executorService = Executors.newCachedThreadPool();
public void fun() {
//可以将业务逻辑封装到Runnable或Callable中,交由线程池来执行。
executorService.submit(new Runnable() {
@Override
public void run() {
log.info("执行业务逻辑...");
}
});
}
二、Future异步
@Slf4j
public class FutureManager{
public String execute()throws Exception{
//创建线程池
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future executor.submit(new Callable<String>(){
@Override
public String call()throws Exception{
System.out.println(" --- task start --- ");
Thread.sleep(3000);
System.out.println(" --- task finish ---");
return "this is future execute final result!!!";
}
});
//这里需要返回值时会阻塞主线程
String result = future.get();
log.info("Future get result: {}", result);
return result;
}
@SneakyThrows
public static void main(String[] args){
FutureManager manager = new FutureManager();
manager.execute();
}
}
Future的不足之处:
- 无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务执行结束之后,主线程无法得到任务完成与否的通知,它需要通过get方法主动获取任务执行的结果。
- Future件彼此孤立:有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的CompletableFuture,CompletableFuture就可以将多个Future串联起来形成任务流
- Futrue没有很好的错误处理机制:截止目前,如果某个异步任务在执行发的过程中发生了异常,调用者无法被动感知,必须通过捕获get方法的异常才知晓异步任务执行是否出现了错误,从而在做进一步的判断处理。
三、CompletableFuture实现异步
public class CompletableFutureCompose{
/**
* thenAccept子任务和父任务公用同一个线程
*/
@SneakyThrows
public static void thenRunAsync(){
CompletableFuture<Integer> cf1 = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread() + " cf1 do something....");
return 1;
});
CompletableFuture<Void> cf2 = cf1.thenRunAsync(() -> {
System.out.println(Thread.currentThread() + " cf2 do something...");
});
//等待任务1执行完成
System.out.println("cf1结果->" + cf1.get());
//等待任务2执行完成
System.out.println("cf2结果->" + cf2.get());
}
public static void main(String[] args) {
thenRunAsync();
}
}
不需要显式使用ExecutorService,CompletableFuture 内部使用了ForkJoinPool来处理异步任务,如果在某些业务场景我们想自定义自己的异步线程池也是可以的。
四、Spring的@Async异步
在实际项目中, 使用@Async调用线程池,推荐等方式是是使用自定义线程池的模式,不推荐直接使用@Async直接实现异步。
①、自定义异步线程池
/**
* 线程池参数配置,多个线程池实现线程池隔离,
* @Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName")
**/
@EnableAsync
@Configuration
public class TaskPoolConfig{
//自定义线程池
@Bean("taskExecutor")
public Executor taskExecutor(){
//返回可用处理器的Java虚拟机数量12
int i = Runtime.getRuntime().availableProcessors();
System.out.println("系统最大线程数:" + i);
//核心线程池大小
executor.setCorePoolSize(16);
//最大线程数
executor.setMaxPoolSize(20);
//配置队列容量,默认值为Integer.MAX_VALUE
executor.setQueueCapacity(99999);
//活跃时间
executor.setKeepAliveSeconds(60);
//线程名字前缀
executor.setThreadNamePrefix("asyncServiceExecutor -");
//设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行
executor.setAwaitTerminationSeconds(60);
//等待所有的任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
②、AsyncService:
public interface AsyncService{
MessageResult sendSms(String callPrefix, String mobile, String actionType, String content);
MessageResult sendEmail(String email, String subject, String content);
}
@Slf4j
@Service
public class AsyncServiceImpl implements AsyncService{
@Autowired
private IMessageHandler messageHandler;
@Override
@Async("taskExecutor")
public MessageResult sendSms(String callPrefix, String mobile, String actionType, String content){
try{
Thread.sleep(1000);
messageHandler.sendSms(callPrefix, mobile, actionType, content);
}catch(Exception e){
log.error("发送短信异常 -> ", e)
}
}
@Override
@Async("taskExecutor")
public sendEmail(String email, String subject, String content) {
try {
Thread.sleep(1000);
messageHandler.sendsendEmail(email, subject, content);
} catch (Exception e) {
log.error("发送email异常 -> ", e)
}
}
}
五、Spring ApplicationEvent事件实现异步
①、定义事件
public class AsyncSendEmailEvent extends ApplicationEvent{
/**
* 邮箱
**/
private String email;
/**
* 主题
**/
private String subject;
/**
* 内容
**/
private String content;
/**
* 接收者
**/
private String targetUserId;
}
②、定义事件处理器
@Slf4j
@Component
public class AsyncSendEmailEventHandler implements ApplicationListener<AsyncSendEmailEvent>{
@Autowired
private IMessageHandler messageHandler;
@Async(taskExecutor)
@Override
public void onApplicationEvent(AsyncSendEmailEvent event){
if(event == null){
return;
}
String email = event.getEmail();
String subject = event.getSubject();
String content = event.getContent();
String targetUserId = event.getTargetUserId();
mesageHandler.sendsendEmailSms(email, subject, content, targerUserId);
}
}
可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合Spring Retry重试来帮助我们避免这种异常造成数据不一致问题。
六、消息队列
①、回调事件消息生产者
@Slf4j
@Component
public class CallbackProducer{
@Autowired
AmqpTemplate amqpTemplate;
public void sendCallbackMessage(CallbackDTO allbackDTO, final long delayTimes){
log.info("生产者发送消息,callbackDTO,{}", callbackDTO);
amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(),
CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(),
JsonMapper.getInstance().toJson(genseeCallbackDTO),
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//给消息设置延迟毫秒值,通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间
message.getMessageProperties().setHeader("x-delay", delayTimes);
message.getMessageProperties().setCorrelationId(callbackDTO.getSdkId());
return message;
}
});
}
}
②、回调事件消息消费者
@Slf4j
@Component
@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory")
public class CallbackConsumer{
@Autowired
private IGlobalUserService globalUserService;
@RabbitHandler
public void handle(String json, Channel channel, @Headers Map<String, Object> map)throws Exception{
if (map.get("error") != null) {
//否认消息
channel.basicNack((Long) map.get(AmqpHeaders.DELIVERY_TAG), false, true);
return;
}
try {
CallbackDTO callbackDTO = JsonMapper.getInstance().fromJson(json, CallbackDTO.class);
//执行业务逻辑
globalUserService.execute(callbackDTO);
//消息消息成功手动确认,对应消息确认模式acknowledge-mode: manual
channel.basicAck((Long) map.get(AmqpHeaders.DELIVERY_TAG), false);
} catch (Exception e) {
log.error("回调失败 -> {}", e);
}
}
}
七、ThreadUtil异步工具类
@Slf4j
public class ThreadUtils{
public static void main(){
for (int i = 0; i < 3; i++) {
ThreadUtil.execAsync(() -> {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
int number = threadLocalRandom.nextInt(20) + 1;
System.out.println(number);
});
log.info("当前第:" + i + "个线程");
}
log.info("task finish!");
}
}
八、Guava异步
Guava的ListenableFuture顾名思义就是可以监听的Future,是对java原生Future的扩展增强。
使用Guava ListenableFuture可以帮我们检测Future是否完成了,不需要再通过get()方法苦苦等待异步的计算结果,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。
ListenableFuture是一个接口,它从jdk的Future接口继承,添加了void addListener(Runnable listener, Executor executor)方法。
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
final ListenableFuture<Integer> listenableFuture = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("callable execute...")
TimeUnit.SECONDS.sleep(1);
return 1;
}
});
首先通过MoreExecutors类的静态方法listeningDecorator方法初始化一个ListeningExecutorService的方法,然后使用此实例的submit方法即可初始化ListenableFuture对象。
ListenableFuture要做的工作,在Callable接口的实现类中定义,这里只是休眠了1秒钟然后返回一个数字1,有了ListenableFuture实例,可以执行此Future并执行Future完成之后的回调函数。
Futures.addCallback(listenableFuture, new FutureCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
//成功执行...
System.out.println("Get listenable future's result with callback " + result);
}
@Override
public void onFailure(Throwable t) {
//异常情况处理...
t.printStackTrace();
}
});
业务场景
老师登录教研平台,会看到课程列表,点击课程后,课程会以视频的形式展现出来。
- 读取课程视频信息:从缓存服务器 Redis 获取课程的视频信息 ,返回给前端,前端通过视频组件渲染。
- 写入课程观看行为记录:当教师观看视频的过程中,浏览器每隔3秒发起请求,教研服务将观看行为记录插入到数据库表中。而且随着用户在线人数越多,写操作的频率也会指数级增长。
上线初期,这种设计运行还算良好,但随着在线用户的增多,系统响应越来越慢,大量线程阻塞在写入视频观看进度表上的 Dao 方法上。
第一种方案:解决写入数据库的能力方案:
- 优化SQL语句
- 提升MySQL数据库硬件配置
- 分库分表
这种方案其实也可以满足我们的需求,但是通过扩容硬件并不便宜,另外写操作可以允许适当延迟和丢失少量数据,那这种方案更显得性价比不足。
第二种方案:“减少写动作的耗时,提升写动作的并发度”
它们的共同特点是:将写操作命令存储在一个池子后,立刻响应给前端,减少写动作的耗时。任务服务异步从池子里获取任务后执行。
- 线程池模式
- 本地内存+定时任务
- MQ模式
- Agent服务+MQ模式
需求:写操作,速度也比较快(20毫秒左右),接口的日请求量在2000万左右
①、线程池模式
一个粗糙的方案:“控制器收到请求后,将写操作放入到独立的线程池中后,立即返回给前端,而线程池会异步执行激活红包方法”。
- 线程数不宜过高,避免占用过多的数据库连接 ;
- 需要考虑评估线程池队列的大小,以免出现内存溢出的问题。
②、本地内存+定时任务
用户访问过一次文章、新闻、代码详情页面,访问次数字段加 1 , 在 oschina 上这个操作是异步的,访问的时候只是将数据在内存中保存,每隔固定时间将这些数据写入数据库。
- 控制器接收请求后,观看进度信息存储到本地内存 LinkedBlockingQueue 对象里
- 异步线程每隔1分钟从队列里获取数据 ,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库
- 批量写入主要是为了提升系统的整体吞吐量,每次批量写入的 List 大小也不宜过大
③、MQ模式
MQ 本身支持高可用和异步,发送消息效率高 , 也支持批量消费;
消息在 MQ 服务端会持久化,可靠性要比保存在本地内存高;
不过 MQ 模式需要引入新的组件,增加额外的复杂度。
- 控制器接收写请求,将观看视频行为记录转换成消息 ;
- 教研服务发送消息到 MQ ,将写操作成功信息返回给前端 ;
- 消费者服务从 MQ 中获取消息 ,批量操作数据库
④、Agent服务+MQ模式
- 教研服务器上部署 Agent 服务(独立的进程) , 教研服务接收写请求后,将请求按照固定的格式(比如 JSON )写入到本次磁盘中,然后给前端返回成功信息。
- Agent 服务会监听文件变动,将文件内容发送到消息队列 , 消费者服务获取观看行为记录,将其存储到 MySQL 数据库中。
还有一种演进,假设我们不想在应用中依赖消息队列,不生成本地文件,可以采用如下的方式:
这种方案最大的优点是:架构分层清晰,业务服务不需要引入 MQ 组件。
笔者原来接触过的性能监控平台,或者日志分析平台都使用这种模式。
=========================================
异步处理方案
一、数据库脚本
CREATE TABLE `async_req` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`application_name` varchar(100) NOT NULL DEFAULT '' COMMENT '应用名称',
`sign` varchar(50) NOT NULL DEFAULT '' COMMENT '方法签名',
`class_name` varchar(200) NOT NULL DEFAULT '' COMMENT '全路径类名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名称',
`async_type` varchar(50) NOT NULL DEFAULT '' COMMENT '异步策略类型',
`exec_status` tinyint NOT NULL DEFAULT '0' COMMENT '执行状态 0:初始化 1:执行失败 2:执行成功',
`exec_count` int NOT NULL DEFAULT '0' COMMENT '执行次数',
`param_json` longtext COMMENT '请求参数',
`remark` varchar(200) NOT NULL DEFAULT '' COMMENT '业务描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_applocation_name` (`application_name`) USING BTREE,
KEY `idx_exec_status` (`exec_status`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理请求';
CREATE TABLE `async_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`async_id` bigint NOT NULL DEFAULT '0' COMMENT '异步请求ID',
`error_data` longtext COMMENT '执行错误信息',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_async_id` (`async_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='异步处理日志';
二、异步策略
枚举字段
执行状态
三、流程图
四、apollo 配置
# 开关:默认关闭
async.enabled=true
# 应用名称
spring.application.name=xxx
# 数据源 druid
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/fc_async?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
spring.datasource.username=user
spring.datasource.password=xxxx
spring.datasource.filters=config
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=yyy
#静态地址
spring.resources.add-mappings=true
spring.resources.static-locations=classpath:/static/
# 以下配置都有默认值
# 核心线程数
async.executor.thread.corePoolSize=10
# 最大线程数
async.executor.thread.maxPoolSize=50
# 队列容量
async.executor.thread.queueCapacity=10000
# 活跃时间
async.executor.thread.keepAliveSeconds=600
# 执行成功是否删除记录:默认删除
async.exec.deleted=true
# 自定义队列名称前缀:默认应用名称
async.topic=${spring.application.name}
# 重试执行次数:默认5次
async.exec.count=5
# 重试最大查询数量
async.retry.limit=100
# 补偿最大查询数量
async.comp.limit=100
# 登录拦截:默认false
async.login=false
五、使用
①、异步开关
scm.async.enabled=true
②、在需要异步执行的方法注解(spring代理方法)
@AsyncExec(type = AsyncExecEnum.SAVE_ASYNC, remark = "数据字典")
③、人工处理地址
http://localhost:8004/async/index.html
效果展示:
https://github.com/xiongyanokok/fc-async.git