SpringBoot實(shí)現(xiàn)模塊日志入庫(kù)的項(xiàng)目實(shí)踐
模塊調(diào)用之后,記錄模塊的相關(guān)日志,看似簡(jiǎn)單,其實(shí)暗藏玄機(jī)。
1.簡(jiǎn)述
模塊日志的實(shí)現(xiàn)方式大致有三種:
- AOP + 自定義注解實(shí)現(xiàn)
- 輸出指定格式日志 + 日志掃描實(shí)現(xiàn)
- 在接口中通過(guò)代碼侵入的方式,在業(yè)務(wù)邏輯處理之后,調(diào)用方法記錄日志。
這里我們主要討論下第3種實(shí)現(xiàn)方式。
假設(shè)我們需要實(shí)現(xiàn)一個(gè)用戶登錄之后記錄登錄日志的操作。
調(diào)用關(guān)系如下:
這里的核心代碼是在 LoginService.login() 方法中設(shè)置了在事務(wù)結(jié)束后執(zhí)行:
// 指定事務(wù)提交后執(zhí)行 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { ? ? // 不需要事務(wù)提交前的操作,可以不用重寫這個(gè)方法 ? ? @Override ? ? public void beforeCommit(boolean readOnly) { ? ? ? ? System.out.println("事務(wù)提交前執(zhí)行"); ? ? } ? ? @Override ? ? public void afterCommit() { ? ? ? ? System.out.println("事務(wù)提交后執(zhí)行"); ? ? } });
在這里,我們把這段代碼封裝成了工具類,參考:4.TransactionUtils。
如果在 LoginService.login() 方法中開啟了事務(wù),不指定事務(wù)提交后指定的話,日志處理的方法做異步和做新事務(wù)都會(huì)有問(wèn)題:
- 做異步:由于主事務(wù)可能沒有執(zhí)行完畢,導(dǎo)致可能讀取不到主事務(wù)中新增或修改的數(shù)據(jù)信息;
- 做新事物:可以通過(guò) Propagation.REQUIRES_NEW 事務(wù)傳播行為來(lái)創(chuàng)建新事務(wù),在新事務(wù)中執(zhí)行記錄日志的操作,可能會(huì)導(dǎo)致如下問(wèn)題:
- 由于數(shù)據(jù)庫(kù)默認(rèn)事務(wù)隔離級(jí)別是可重復(fù)讀,意味著事物之間讀取不到未提交的內(nèi)容,所以也會(huì)導(dǎo)致讀取不到主事務(wù)中新增或修改的數(shù)據(jù)信息;
- 如果開啟的新事務(wù)和之前的事務(wù)操作了同一個(gè)表,就會(huì)導(dǎo)致鎖表。
- 什么都不做,直接同步調(diào)用:?jiǎn)栴}最多,可能導(dǎo)致如下幾個(gè)問(wèn)題:
- 不捕獲異常,直接導(dǎo)致接口所有操作回滾;
- 捕獲異常,部分?jǐn)?shù)據(jù)庫(kù),如:PostgreSQL,同一事務(wù)中,只要有一次執(zhí)行失敗,就算捕獲異常,剩余的數(shù)據(jù)庫(kù)操作也會(huì)全部失敗,拋出異常;
- 日志記錄耗時(shí)增加接口響應(yīng)時(shí)間,影響用戶體驗(yàn)。
2.LoginController
@RestController public class LoginController { ? ? @Autowired ? ? private LoginService loginService; ? ? @RequestMapping("/login") ? ? public String login(String username, String pwd) { ? ? ? ? loginService.login(username, pwd); ? ? ? ? return "succeed"; ? ? } }
3.Action
/** ?* <p> @Title Action ?* <p> @Description 自定義動(dòng)作函數(shù)式接口 ?* ?* @author ACGkaka ?* @date 2023/4/26 13:55 ?*/ public interface Action { ? ? ? ? /** ? ? ? ? * 執(zhí)行動(dòng)作 ? ? ? ? */ ? ? ? ? void doSomething(); }
4.TransactionUtils
import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; /** ?* <p> @Title TransactionUtils ?* <p> @Description 事務(wù)同步工具類 ?* ?* @author ACGkaka ?* @date 2023/4/26 13:45 ?*/ public class TransactionUtils { ? ? /** ? ? ?* 提交事務(wù)前執(zhí)行 ? ? ?*/ ? ? public static void beforeTransactionCommit(Action action) { ? ? ? ? TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { ? ? ? ? ? ? @Override ? ? ? ? ? ? public void beforeCommit(boolean readOnly) { ? ? ? ? ? ? ? ? // 異步執(zhí)行 ? ? ? ? ? ? ? ? action.doSomething(); ? ? ? ? ? ? } ? ? ? ? }); ? ? } ? ? /** ? ? ?* 提交事務(wù)后異步執(zhí)行 ? ? ?*/ ? ? public static void afterTransactionCommit(Action action) { ? ? ? ? TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { ? ? ? ? ? ? @Override ? ? ? ? ? ? public void afterCommit() { ? ? ? ? ? ? ? ? // 異步執(zhí)行 ? ? ? ? ? ? ? ? action.doSomething(); ? ? ? ? ? ? } ? ? ? ? }); ? ? } }
5.LoginService
@Service public class LoginService { ? ? @Autowired ? ? private LoginLogService loginLogService; ? ? /** 登錄 */ ? ? @Transactional(rollbackFor = Exception.class) ? ? public void login(String username, String pwd) { ? ? ? ? // 用戶登錄 ? ? ? ? // TODO: 實(shí)現(xiàn)登錄邏輯.. ? ? ? ? // 事務(wù)提交后執(zhí)行 ? ? ? ? TransactionUtil.afterTransactionCommit(() -> { ? ? ? ? ? ? // 異步執(zhí)行 ? ? ? ? ? ? taskExecutor.execute(() -> { ? ? ? ? ?? ??? ?// 記錄日志 ? ? ? ? ?? ??? ?loginLogService.recordLog(username); ? ? ? ? ? ? }); ? ? ? ? }); ? ? } }
6.LoginLogService
6.1 @Async實(shí)現(xiàn)異步
@Service public class LoginLogService { /** 記錄日志 */ @Async @Transactional(rollbackFor = Exception.class) public void recordLog(String username) { // TODO: 實(shí)現(xiàn)記錄日志邏輯... } }
注意:@Async 需要配合 @EnableAsync 使用,@EnableAsync 添加到啟動(dòng)類、配置類、自定義線程池類上均可。
補(bǔ)充:由于 @Async 注解會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)繼承類來(lái)擴(kuò)展方法的實(shí)現(xiàn),所以可能會(huì)導(dǎo)致當(dāng)前類注入Bean容器失敗 BeanCurrentlyInCreationException,可以使用如下方式:自定義線程池 + @Autowired
6.2 自定義線程池實(shí)現(xiàn)異步
1)自定義線程池
AsyncTaskExecutorConfig.java
import com.demo.async.ContextCopyingDecorator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; /** ?* <p> @Title AsyncTaskExecutorConfig ?* <p> @Description 異步線程池配置 ?* ?* @author ACGkaka ?* @date 2023/4/24 19:48 ?*/ @EnableAsync @Configuration public class AsyncTaskExecutorConfig { ? ? /** ? ? ?* 核心線程數(shù)(線程池維護(hù)線程的最小數(shù)量) ? ? ?*/ ? ? private int corePoolSize = 10; ? ? /** ? ? ?* 最大線程數(shù)(線程池維護(hù)線程的最大數(shù)量) ? ? ?*/ ? ? private int maxPoolSize = 200; ? ? /** ? ? ?* 隊(duì)列最大長(zhǎng)度 ? ? ?*/ ? ? private int queueCapacity = 10; ? ? @Bean ? ? public TaskExecutor taskExecutor() { ? ? ? ? ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ? ? ? ? executor.setCorePoolSize(corePoolSize); ? ? ? ? executor.setMaxPoolSize(maxPoolSize); ? ? ? ? executor.setQueueCapacity(queueCapacity); ? ? ? ? executor.setThreadNamePrefix("MyExecutor-"); ? ? ? ? // for passing in request scope context 轉(zhuǎn)換請(qǐng)求范圍的上下文 ? ? ? ? executor.setTaskDecorator(new ContextCopyingDecorator()); ? ? ? ? // rejection-policy:當(dāng)pool已經(jīng)達(dá)到max size的時(shí)候,如何處理新任務(wù) ? ? ? ? // CALLER_RUNS:不在新線程中執(zhí)行任務(wù),而是有調(diào)用者所在的線程來(lái)執(zhí)行 ? ? ? ? executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); ? ? ? ? executor.setWaitForTasksToCompleteOnShutdown(true); ? ? ? ? executor.initialize(); ? ? ? ? return executor; ? ? } }
2)復(fù)制上下文請(qǐng)求
ContextCopyingDecorator.java
import org.slf4j.MDC; import org.springframework.core.task.TaskDecorator; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import java.util.Map; /** ?* <p> @Title ContextCopyingDecorator ?* <p> @Description 上下文拷貝裝飾者模式 ?* ?* @author ACGkaka ?* @date 2023/4/24 20:20 ?*/ public class ContextCopyingDecorator implements TaskDecorator { ? ? @Override ? ? public Runnable decorate(Runnable runnable) { ? ? ? ? try { ? ? ? ? ? ? // 從父線程中獲取上下文,然后應(yīng)用到子線程中 ? ? ? ? ? ? RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); ? ? ? ? ? ? Map<String, String> previous = MDC.getCopyOfContextMap(); ? ? ? ? ? ? SecurityContext securityContext = SecurityContextHolder.getContext(); ? ? ? ? ? ? return () -> { ? ? ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ? ? if (previous == null) { ? ? ? ? ? ? ? ? ? ? ? ? MDC.clear(); ? ? ? ? ? ? ? ? ? ? } else { ? ? ? ? ? ? ? ? ? ? ? ? MDC.setContextMap(previous); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? RequestContextHolder.setRequestAttributes(requestAttributes); ? ? ? ? ? ? ? ? ? ? SecurityContextHolder.setContext(securityContext); ? ? ? ? ? ? ? ? ? ? runnable.run(); ? ? ? ? ? ? ? ? } finally { ? ? ? ? ? ? ? ? ? ? // 清除請(qǐng)求數(shù)據(jù) ? ? ? ? ? ? ? ? ? ? MDC.clear(); ? ? ? ? ? ? ? ? ? ? RequestContextHolder.resetRequestAttributes(); ? ? ? ? ? ? ? ? ? ? SecurityContextHolder.clearContext(); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? }; ? ? ? ? } catch (IllegalStateException e) { ? ? ? ? ? ? return runnable; ? ? ? ? } ? ? } }
3)自定義線程池實(shí)現(xiàn)異步 LoginService
import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @Service public class LoginService { ? ? @Autowired ? ? private LoginLogService loginLogService; ? ? @Qualifier("taskExecutor") ? ? @Autowired ? ? private TaskExecutor taskExecutor; ? ? /** 登錄 */ ? ? @Transactional(rollbackFor = Exception.class) ? ? public void login(String username, String pwd) { ? ? ? ? // 用戶登錄 ? ? ? ? // TODO: 實(shí)現(xiàn)登錄邏輯.. ? ? ? ? // 事務(wù)提交后執(zhí)行 ? ? ? ? TransactionUtil.afterTransactionCommit(() -> { ? ? ? ? ? ? // 異步執(zhí)行 ? ? ? ? ? ? taskExecutor.execute(() -> { ? ? ? ? ?? ??? ?// 記錄日志 ? ? ? ? ?? ??? ?loginLogService.recordLog(username); ? ? ? ? ? ? }); ? ? ? ? }); ? ? } }
7.其他解決方案
7.1 使用編程式事務(wù)來(lái)代替@Transactional
我們還可以使用TransactionTemplate來(lái)代替 @Transactional 注解:
import org.springframework.transaction.support.TransactionTemplate; @Service public class LoginService { ? ? @Autowired ? ? private LoginLogService loginLogService; ? ? @Autowired ? ? private TransactionTemplate transactionTemplate; ? ? /** 登錄 */ ? ? public void login(String username, String pwd) { ? ? ? ? // 用戶登錄 ? ? ? ? transactionTemplate.execute(status->{ ?? ??? ??? ?// TODO: 實(shí)現(xiàn)登錄邏輯.. ? ? ? ? }); ? ? ? ? // 事務(wù)提交后異步執(zhí)行 ? ? ? ? taskExecutor.execute(() -> { ? ? ?? ??? ?// 記錄日志 ? ? ?? ??? ?loginLogService.recordLog(username); ? ? ? ? }); ? ? } }
經(jīng)測(cè)試:
這種實(shí)現(xiàn)方式拋出異常后,事務(wù)也可以正?;貪L
正常執(zhí)行之后也可以讀取到事務(wù)執(zhí)行后的內(nèi)容,可行。
別看日志記錄好實(shí)現(xiàn),坑是真的多,這里記錄的只是目前遇到的問(wèn)題。
參考地址:
1.SpringBoot 關(guān)于異步與事務(wù)一起使用的問(wèn)題
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)模塊日志入庫(kù)的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)SpringBoot 模塊日志入庫(kù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java模擬死鎖發(fā)生之演繹哲學(xué)家進(jìn)餐問(wèn)題案例詳解
這篇文章主要介紹了Java模擬死鎖發(fā)生之演繹哲學(xué)家進(jìn)餐問(wèn)題,結(jié)合具體演繹哲學(xué)家進(jìn)餐問(wèn)題的案例形式詳細(xì)分析了死鎖機(jī)制與原理,需要的朋友可以參考下2019-10-10SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實(shí)現(xiàn)方法詳解
這篇文章主要介紹了SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-12-12SpringCloud Config連接git與數(shù)據(jù)庫(kù)流程分析講解
springcloud config是一個(gè)解決分布式系統(tǒng)的配置管理方案。它包含了 client和server兩個(gè)部分,server端提供配置文件的存儲(chǔ)、以接口的形式將配置文件的內(nèi)容提供出去,client端通過(guò)接口獲取數(shù)據(jù)、并依據(jù)此數(shù)據(jù)初始化自己的應(yīng)用2022-12-12JDBC實(shí)現(xiàn)Mysql自動(dòng)重連機(jī)制的方法詳解
最近在工作中發(fā)現(xiàn)了一個(gè)問(wèn)題,通過(guò)查找相關(guān)的資料終于解決了,下面這篇文章主要給大家介紹了關(guān)于JDBC實(shí)現(xiàn)Mysql自動(dòng)重連機(jī)制的相關(guān)資料,文中給出多種解決的方法,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-07-07Java實(shí)現(xiàn)經(jīng)典游戲復(fù)雜迷宮
這篇文章主要介紹了如何利用java語(yǔ)言實(shí)現(xiàn)經(jīng)典《復(fù)雜迷宮》游戲,文中采用了swing技術(shù)進(jìn)行了界面化處理,感興趣的小伙伴可以動(dòng)手試一試2022-02-02