常見的異步方式:
創(chuàng)建異步線程
每個(gè)新創(chuàng)建一個(gè)線程來執(zhí)行異步任務(wù),任務(wù)結(jié)束線程也終止。
線程的創(chuàng)建成本比較大,不建議使用。
使用Queue,producer/consumer方式
在內(nèi)部創(chuàng)建一個(gè)Queue,worker線程直接將異步處理的任務(wù)放入queue,一個(gè)或多個(gè)異步線程從queue中消費(fèi)并執(zhí)行任務(wù)。
線程池
用線程池來替換每次創(chuàng)建線程,減少線程創(chuàng)建的成本,線程被復(fù)用,一次創(chuàng)建多處使用。
和使用Queue類似,也是通過BlockingQueue
實(shí)現(xiàn),但策略上更復(fù)雜,向線程池提交Callable&Runnable任務(wù),由線程池調(diào)度執(zhí)行。
參考:java.util.concurrent.ThreadPoolExecutor#execute
spring @Async注解
通過注解來來簡(jiǎn)化了異步編程,只需要在需要異步的方法上使用@Async
注解即可。
其本質(zhì)也是在線程池功能上擴(kuò)展的,將異步執(zhí)行方法封裝為一個(gè)Callable
,然后提交給線程池。
org.springframework.aop.interceptor.AsyncExecutionInterceptor:
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
if (executor == null) {
throw new IllegalStateException(
"No executor specified and no default executor set on AsyncExecutionInterceptor either");
}
Callable<Object> task = new Callable<Object>() {
@Override
public Object call() throws Exception {
try {
Object result = invocation.proceed();
if (result instanceof Future) {
return ((Future<?>) result).get();
}
}
catch (ExecutionException ex) {
handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
}
catch (Throwable ex) {
handleError(ex, userDeclaredMethod, invocation.getArguments());
}
return null;
}
};
return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
背景和場(chǎng)景
產(chǎn)生的背景
在項(xiàng)目中使用了@Async
來執(zhí)行異步任務(wù),但在線上運(yùn)行時(shí)出現(xiàn)了一次OOM的故障。通過分析發(fā)現(xiàn)是,線程池隊(duì)列設(shè)置的比較大,當(dāng)時(shí)的JVM內(nèi)存給的也比較少(2048M),異步任務(wù)方法參數(shù)中傳了大量的數(shù)據(jù),任務(wù)執(zhí)行被后端數(shù)據(jù)庫(kù)阻塞(后端數(shù)據(jù)庫(kù)變慢),最后導(dǎo)致緩存了大量的數(shù)據(jù)被放到線程池隊(duì)列。其實(shí)JVM內(nèi)存配置合適,線程池隊(duì)列數(shù)合適,并配置合適的RejectedExecutionHandler
策略。
產(chǎn)生這個(gè)組件,1) 旨在替換內(nèi)存隊(duì)列的異步方式 2) 用來方便擴(kuò)展集成分布式MQ
異步隔離
除了上面背景和場(chǎng)景,開發(fā)這個(gè)組件的另一個(gè)初衷就是有效異步隔離和作為一個(gè)降級(jí)備份方案。
也是主要實(shí)現(xiàn)了文件隊(duì)列方式的一個(gè)原因。
當(dāng)我們使用分布式MQ時(shí),難免分布式MQ宕機(jī)或者其他網(wǎng)絡(luò)等原因?qū)е虏荒苌a(chǎn)消息,或者阻塞影響到本身的業(yè)務(wù),出現(xiàn)這種情況時(shí)可以降級(jí)到本地文件隊(duì)列。
本地文件隊(duì)列的優(yōu)點(diǎn)是速度快,只要文件系統(tǒng)不出問題可以認(rèn)為不會(huì)被阻塞。缺點(diǎn)是本地文件隊(duì)列生產(chǎn)的消息必須自己來消費(fèi),出現(xiàn)故障時(shí)消息消費(fèi)會(huì)延遲,文件系統(tǒng)的損壞也會(huì)導(dǎo)致消息丟失。主要看使用的姿勢(shì),更看重哪一方面了。
基本架構(gòu)設(shè)計(jì)思路
采用producer/consumer生產(chǎn)消費(fèi)設(shè)計(jì)模式。
參考了@Async
思路,定義一個(gè)注解@AsyncExecutable
, 使用Spring攔截器攔截注解了@AsyncExecutable
的方法,可以使用AOP或者BeanPostProcessor來應(yīng)用攔截器。
producer
攔截器攔截到@AsyncExecutable
方法后,將該方法所有的參數(shù)和方法信息作為Message,并序列化Message,序列化采用Kryo或者Json,將序列化后的信息放入隊(duì)列。
class Message {
String beanName;
String klassName;
String methodName;
Class<?>[] argTypes;
Object[] args;
boolean hasTransactional = true;
}
consumer
有1個(gè)調(diào)度主線程和worker線程組成,主線程負(fù)責(zé)從隊(duì)列中拉取消息,并分發(fā)到worker線程,worker線程采用線程池,使用了spring提供的TaskExecutor。
worker線程反序列化消息為Message對(duì)象,并根據(jù)Message中的方法信息在spring ApplicationContext中查找到spring 管理的bean,并通過反射來調(diào)用。
隊(duì)列
隊(duì)列抽象了一個(gè)BlockableQueue
, 通過BlockableQueue
具體實(shí)現(xiàn)來擴(kuò)展,可以是內(nèi)存,文件,或分布式MQ。
public interface BlockableQueue<T> {
String DefaultQueueName = "fileQueue";
/**
* push一個(gè)消息到隊(duì)列
*
* @param t
* @return
*/
boolean offer(T t);
/**
* 從隊(duì)列pop一個(gè)消息,如果隊(duì)列中無可用消息,則阻塞
*
* @return
* @throws InterruptedException
*/
T take() throws InterruptedException;
/**
* 從隊(duì)列pop一個(gè)消息,如果隊(duì)列中無可用消息,則返回null
*
* @return
*/
T poll();
/**
* 隊(duì)列中消息數(shù)量
*
* @return
*/
int size();
}
通用的默認(rèn)實(shí)現(xiàn):
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DefaultBlockableQueue implements BlockableQueue<byte[]> {
final Lock lock = new ReentrantLock();
final Condition notEmpty = lock.newCondition();
private Queue<byte[]> queue = null;
public FileBlockableQueue(Queue<byte[]> queue) {
this.queue = queue;
}
@Override
public boolean offer(byte[] bytes) {
lock.lock();
try {
boolean v = queue.offer(bytes);
notEmpty.signal();
return v;
} finally {
lock.unlock();
}
}
@Override
public byte[] take() throws InterruptedException {
lock.lock();
try {
while (queue.size() == 0) {
notEmpty.await();
}
byte[] bytes = queue.poll();
return bytes;
} finally {
lock.unlock();
}
}
@Override
public byte[] poll() {
return queue.poll();
}
@Override
public int size() {
return queue.size();
}
}
本文實(shí)現(xiàn)了一個(gè)文件隊(duì)列,采用去哪兒文件隊(duì)列實(shí)現(xiàn),這是一個(gè)fork:https://github.com/tietang/fqueue
對(duì)編程模型來說不用關(guān)心異步細(xì)節(jié),只需要在需要異步的方法上注解@AsyncExecutable
即可。