最近做一個新項目時,有這么一個場景:使用TDDL數(shù)據(jù)庫分庫分表,且按SQL讀寫分離。在為系統(tǒng)做緩存層時,考慮到并發(fā)讀寫,可能會出現(xiàn)這么個問題:
緩存未設(shè)置或已過期時,寫數(shù)據(jù),主庫已完成但未拉起緩存,從庫也未完成同步,此時有查詢請求,將會訪問到從庫,把舊值查詢出來,如果此時寫數(shù)據(jù)線程已經(jīng)用最新數(shù)據(jù)拉起緩存,那讀數(shù)據(jù)線程將會用舊值將緩存覆蓋,導(dǎo)致緩存與數(shù)據(jù)庫不一致,在緩存失效或下次修改之前,緩存數(shù)據(jù)將一直是過期的。
這種情況雖然比較少見,但還是有一定概率出現(xiàn)的。為了屏蔽主從同步可能帶來的問題,一種比較簡單的解決方法是延遲更新緩存,給數(shù)據(jù)庫一定時間完成同步再進(jìn)行緩存設(shè)置,這樣就能保證最后緩存的是新數(shù)據(jù)。當(dāng)然這種方案只能解決數(shù)據(jù)庫同步延遲不大的普通情況。
如果是一些讀多寫少的數(shù)據(jù),也可以在修改完成改一遍緩存,再放到延遲任務(wù)改一遍。
為此實現(xiàn)了一個簡單的延遲任務(wù)處理器,主要思路是將任務(wù)放入DelayQueue,由一個專用線程從DelayQueue中取出到期的任務(wù),放入執(zhí)行線程池中執(zhí)行。整體比較簡單。
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.collections.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
/**
* 延遲任務(wù)處理器
* @author jerryli
* @date 2018/3/27
*/
public class DelayedTaskExecutor implements InitializingBean, DisposableBean{
private static final Logger logger = LoggerFactory.getLogger(DelayedTaskExecutor.class);
private String name;
private ThreadPoolExecutor executor;
private volatile boolean running;
private ThreadPoolExecutor putExecutor ;
private DelayQueue<Task> delayQueue = new DelayQueue<>();
public DelayedTaskExecutor(String name){
this.name = name;
}
public void submit(Runnable runnable, long delayedTime){
Task task = new Task(runnable,delayedTime);
delayQueue.offer(task);
logger.info("{}添加延遲任務(wù) {} 延遲{}ms",this.name,task,delayedTime);
}
@Override
public void destroy() throws Exception {
running = false;
if(putExecutor != null){
putExecutor.shutdown();
boolean shutdown = putExecutor.awaitTermination(8000, TimeUnit.MILLISECONDS);
if(!shutdown){
List<Runnable> notRunList = putExecutor.shutdownNow();
if(CollectionUtils.isNotEmpty(notRunList)){
logger.warn("強制關(guān)閉延遲任務(wù)轉(zhuǎn)移線程...未執(zhí)行任務(wù)數(shù)量{}",notRunList.size());
}
}
}
if(executor != null){
executor.shutdown();
boolean shutdown = executor.awaitTermination(8000, TimeUnit.MILLISECONDS);
if(!shutdown){
List<Runnable> notRunList = executor.shutdownNow();
if(CollectionUtils.isNotEmpty(notRunList)){
logger.warn("強制關(guān)閉延遲任務(wù)執(zhí)行線程池...未執(zhí)行任務(wù)數(shù)量{}",notRunList.size());
}
}
}
}
@Override
public void afterPropertiesSet() throws Exception {
running = true;
executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() + 1,
Runtime.getRuntime().availableProcessors() * 2,0,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setNameFormat(this.name + "-pool-%d")
.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("延遲任務(wù)執(zhí)行線程異常,thread={}",t,e);
}
})
.build());
putExecutor = new ThreadPoolExecutor(1,1,0,TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setNameFormat(this.name + "-submiter")
.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
logger.error("延遲任務(wù)轉(zhuǎn)移線程異常,thread={}",t,e);
}
})
.build());
putExecutor.execute(new Runnable() {
@Override
public void run() {
while(running || delayQueue.size() > 0){
try {
Task task = delayQueue.poll(1000, TimeUnit.MILLISECONDS);
if(task == null){
continue;
}
logger.info("開始執(zhí)行延遲任務(wù) {}",task);
executor.execute(task.getRunnable());
} catch (InterruptedException e) {
logger.error("延遲任務(wù)轉(zhuǎn)移線程異常interrupted",e);
} catch (Throwable e){
logger.error("延遲任務(wù)轉(zhuǎn)移線程異常",e);
}
}
logger.info("延遲任務(wù)轉(zhuǎn)移線程退出");
}
});
}
class Task implements Delayed{
// 提交時間
private long submitTime;
private long runningTime;
private Runnable runnable;
Task(Runnable runnable,long delayTime){
this.runnable = runnable;
this.submitTime = System.currentTimeMillis();
this.runningTime = submitTime + delayTime;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(runningTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if(o == null || !(o instanceof Task)){
return 1;
}
if(o == this){
return 0;
}
Task otherTask = (Task) o;
if(this.runningTime > otherTask.runningTime){
return 1;
}else if(this.runningTime == otherTask.runningTime){
return 0;
}else{
return -1;
}
}
public Runnable getRunnable() {
return runnable;
}
}
}
測試代碼:
public static void main(String[] args) throws Exception {
// 使用Spring可不用調(diào)用afterPropertiesSet和destroy方法
DelayedTaskExecutor delayedTaskExecutor = new DelayedTaskExecutor("cache-delay");
delayedTaskExecutor.afterPropertiesSet();
delayedTaskExecutor.submit(() -> System.out.println("hello"), 2000);
delayedTaskExecutor.destroy();
}