自定義starter引發(fā)的線上事故記錄復(fù)盤
前言
本文素材來源于業(yè)務(wù)部門技術(shù)負責(zé)人某次線上事故復(fù)盤分享。故事的背景是這樣,該業(yè)務(wù)部門招了一個技術(shù)挺不錯的小伙子小張,由于小張技術(shù)能力在該部門比較突出,在入職不久后,他便成為這個部門某個項目組的team leader,同時也擁有review 該項目的權(quán)利。(注: 該項目為微服務(wù)項目),在某次小張review項目的時候,他發(fā)現(xiàn)好幾個項目,發(fā)現(xiàn)代碼有很多重復(fù),于是他就動了把這些重復(fù)代碼封裝成starter的念頭,然后也是因為這次的封裝,帶來一次線上事故。下面就以代碼示例的形式,模擬這次事故
代碼示例
注: 本文僅模擬出現(xiàn)事故的代碼片段,不涉及業(yè)務(wù)
1、模擬小張的封裝的starter
@Slf4j
public class HelloSevice {
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
public HelloSevice(ThreadPoolTaskExecutor threadPoolTaskExecutor){
this.threadPoolTaskExecutor = threadPoolTaskExecutor;
}
public String sayHello(String username){
threadPoolTaskExecutor.execute(()->{
log.info("hello: {} ",username);
});
return " hello : " + username;
}
}@Configuration
public class HelloServiceAutoConfiguration {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(4);
threadPoolTaskExecutor.setQueueCapacity(1);
threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("hello-pool-" + atomicInteger.getAndIncrement());
return thread;
}
});
return threadPoolTaskExecutor;
}
@Bean
public HelloSevice helloSevice(ThreadPoolTaskExecutor threadPoolTaskExecutor){
return new HelloSevice(threadPoolTaskExecutor);
}
}spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.github.lybgeek.thirdparty.autoconfigure.HelloServiceAutoConfiguration
2、模擬有引用小張封裝的starter的微服務(wù)項目
因為這些微服務(wù)中有一些耗時的任務(wù),因此使用了spring的異步。示例如下
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(5);
threadPoolTaskExecutor.setQueueCapacity(10);
threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("echo-pool-" + atomicInteger.getAndIncrement());
return thread;
}
});
threadPoolTaskExecutor.setRejectedExecutionHandler((r, executor) -> System.err.println("記錄日志。。。。"));
return threadPoolTaskExecutor;
}
}@Service
@Slf4j
public class EchoService {
@Async("threadPoolTaskExecutor")
public void echo(String content){
log.info("echo -> {} ",content);
try {
//模擬耗時操作
TimeUnit.MINUTES.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}3、和本文有關(guān)系的配置內(nèi)容
spring:
main:
allow-bean-definition-overriding: true4、模擬調(diào)用耗時業(yè)務(wù)代碼塊示例
@Component
public class BeanCommandRunner implements CommandLineRunner {
@Autowired
private EchoService echoService;
@Override
public void run(String... args) throws Exception {
for (int i = 0; i < 6; i++) {
echoService.echo("content:" + i);
}
}
}相關(guān)的代碼如上述內(nèi)容
大家可以思考一下上面的示例有沒有什么問題
我們啟動一下程序,觀察一下控制臺

報了一個線程池拒絕異常,而且通過這個異常信息,我們發(fā)現(xiàn)這個線程池走是小張封裝線程池,而非業(yè)務(wù)自己定義的線程池。這明顯是不正常的,正常的邏輯是業(yè)務(wù)代碼優(yōu)先級需比公共代碼優(yōu)先高才合理
那如何解決呢?
僅需利用springboot的條件注解即可,在小張封裝的starter下做如下改動
@Bean
@ConditionalOnMissingBean
public ThreadPoolTaskExecutor threadPoolTaskExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(4);
threadPoolTaskExecutor.setQueueCapacity(1);
threadPoolTaskExecutor.setThreadFactory(new ThreadFactory() {
private AtomicInteger atomicInteger = new AtomicInteger();
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("hello-pool-" + atomicInteger.getAndIncrement());
return thread;
}
});
return threadPoolTaskExecutor;
}修改后,我們在啟動一下程序,觀察控制臺

此時走就是業(yè)務(wù)自定義的線程池了
為什么加了一個 @ConditionalOnMissingBean就可以了
這就得從springboot的自動裝配說起了,springboot的自動裝配類繼承了org.springframework.context.annotation.DeferredImportSelector,這個接口具有懶加載的功能,當(dāng)項目啟動后,先加載業(yè)務(wù)自定義的bean,再來加載starter的bean,當(dāng)我們項目中沒有配置
spring:
main:
allow-bean-definition-overriding: true時,項目啟動就會直接報類似如下異常

當(dāng)時他們業(yè)務(wù)項目因為他們feign沒有指定contextId,導(dǎo)致報了上述的異常,業(yè)務(wù)開發(fā)為了省事就直接把
allow-bean-definition-overriding設(shè)置成true,這也為后續(xù)小張自定義的starter引發(fā)的事故埋下了很好的根基。那我們再切回主線,當(dāng)spring發(fā)現(xiàn)有兩個一樣的bean,且發(fā)現(xiàn)allow-bean-definition-overriding為true,后面加載的bean會把前面加載的bean覆蓋掉,這也是為啥小張starter的bean會生效。
當(dāng)我們在starter上的bean上加載 @ConditionalOnMissingBean后,因為業(yè)務(wù)項目的bean已經(jīng)存在了,starter的bean就不會加載進spring容器了。
我們從技術(shù)維度說明了解決方案,我們再從非技術(shù)的角度上復(fù)盤一下這次事故
復(fù)盤
不知道會不會有朋友說,你說那么多,不就加一個@ConditionalOnMissingBean就能解決這個問題,下次注意就好了啊。但據(jù)業(yè)務(wù)技術(shù)人反饋當(dāng)時他們排查了挺久,因為他們業(yè)務(wù)項目平時沒啥并發(fā)量,所以小張那個問題就被掩蓋住了,而有次他們業(yè)務(wù)搞了一個營銷活動,因為并發(fā)上去了,才把問題暴露出來。這側(cè)面也說明項目壓測的重要性,不能因為平時沒啥并發(fā),就掉以輕心
不懂大家的公司是否也有這樣的情況,在我們這邊,底下成員代碼只能merge request,只有team leader review后,再將代碼合并到主干,因為team leader擁有的權(quán)限比較大,他寫的代碼,只要他愿意,直接就能合并到主干了。這次也是因為小張直接將他寫的代碼推到主干發(fā)布,釀成事故。后面我們這邊提出了一個方法,就是team leader的代碼要由更高級的leader進行走查,但是這個方法我是感覺也不是很好,因為有不少項目組的team leader的老板基本上已經(jīng)脫離一線,不敲代碼了,也不懂能不能行。
其次因為小張入職不久,對業(yè)務(wù)其實沒有完全吃透,因為看到重復(fù)的代碼,出于技術(shù)潔癖,就想去改,出發(fā)點是好的,但有句話技術(shù)是為業(yè)務(wù)服務(wù),業(yè)務(wù)都沒搞懂,就去動,有時候會帶來意想不到的風(fēng)險
總結(jié)
對自己的不熟悉的項目或者開發(fā)公共組件,深思熟慮再動手是很重要的
以上就是自定義starter引發(fā)的線上事故記錄復(fù)盤的詳細內(nèi)容,更多關(guān)于自定義starter線上事故的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Idea的Generate Sources無法生成QueryDSL問題及解決方法
這篇文章主要介紹了解決Idea的Generate Sources無法生成QueryDSL問題,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02
java設(shè)計模式學(xué)習(xí)之簡單工廠模式
這篇文章主要為大家詳細介紹了java設(shè)計模式學(xué)習(xí)之簡單工廠模式,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10
Java使用JDBC驅(qū)動連接MySQL數(shù)據(jù)庫
這篇文章主要為大家詳細介紹了Java使用JDBC驅(qū)動連接MySQL數(shù)據(jù)庫的具體步驟,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12

