注:文中的“Doctor”是精分對(duì)話者,另一個(gè)我族檬,道德約束者歪赢,話嘮,請(qǐng)無視他
前言:博主最近在做公司一個(gè)很重要的項(xiàng)目单料,有關(guān)于汽車物聯(lián)網(wǎng)金融的埋凯,作為剛工作一年的新人來說点楼,這是一個(gè)極好的學(xué)習(xí)機(jī)會(huì),而且被信任的感覺很好啊有木有白对。簡單說博主在為某實(shí)力國產(chǎn)汽車公司賣命掠廓,這個(gè)系統(tǒng)涉及到的用戶有幾百萬,每個(gè)用戶都會(huì)有很多的金融數(shù)據(jù)甩恼,金融總是要結(jié)算的蟀瞧,所以每個(gè)月到結(jié)算日會(huì)有千萬條數(shù)據(jù)需要操作,累計(jì)有幾十億次的計(jì)算条摸,那么面對(duì)這龐大的數(shù)據(jù)集悦污,我是瑟瑟發(fā)抖的,之前對(duì)于優(yōu)化只是建立在理論上的钉蒲,這次雖然是好的實(shí)踐機(jī)會(huì)切端,但是作為核心項(xiàng)目,一點(diǎn)也馬虎不得顷啼。
Doctor:你的廢話太多了踏枣。。钙蒙。
咳咳茵瀑,那么廢話不說了,現(xiàn)在開始進(jìn)入正題躬厌,本文章將會(huì)跟大家分享一下個(gè)人在優(yōu)化中的一些實(shí)踐和心得马昨,不足的地方希望大家指正。
關(guān)于針對(duì)該系統(tǒng)的優(yōu)化烤咧,我主要做了以下幾點(diǎn):
數(shù)據(jù)庫SQL語句調(diào)優(yōu)偏陪,mybatis批量插入,建立索引
適當(dāng)使用Redis存儲(chǔ)不常修改但是常用的數(shù)據(jù)
使用異步處理煮嫌,校驗(yàn)完核心數(shù)據(jù)即可返回前臺(tái)操作完成笛谦,并異步跑后續(xù)操作
多線程處理海量運(yùn)算
該系統(tǒng)的架構(gòu)是基于SpringCloud的微服務(wù)架構(gòu),Springboot+Mybatis+Maven+MySql整合昌阿,同時(shí)使用到Redis饥脑、kafka。
1懦冰、數(shù)據(jù)庫SQL語句調(diào)優(yōu)灶轰,mybatis批量插入,建立索引
關(guān)于SQL語句調(diào)優(yōu)我想最基本的就是在項(xiàng)目開始階段刷钢,根據(jù)業(yè)務(wù)需求抽象成數(shù)據(jù)庫表的時(shí)候要請(qǐng)專門的DBA來分析建表笋颤,本系統(tǒng)是由我跟另一位高級(jí)開發(fā)建的表,其實(shí)后期發(fā)現(xiàn)并不夠完美,也許跟業(yè)務(wù)變更有些大導(dǎo)致的吧伴澄,我認(rèn)為數(shù)據(jù)庫表的設(shè)計(jì)和代碼一樣也需要健壯性赋除,我們的系統(tǒng)使用的是基于SpringCloud的微服務(wù)架構(gòu),其實(shí)更像是把單體服務(wù)拆成多個(gè)springboot小項(xiàng)目非凌,既然使用敏捷迭代開發(fā)模式(我們現(xiàn)在的模式也可以勉強(qiáng)說是devops举农,只不過devops里面運(yùn)維要做的很多工作都由我們開發(fā)做了),就要針對(duì)變動(dòng)頻繁的需求設(shè)計(jì)出合理健壯的數(shù)據(jù)庫表結(jié)構(gòu)敞嗡。
首先颁糟,要按照三范式來建表,但是既然是范式就未必一定遵守喉悴,其實(shí)有些時(shí)候棱貌,比如某張表里面有很多需要頻繁加起來的字段,假設(shè)我們現(xiàn)在有一張結(jié)算表account粥惧,里面有三個(gè)字段:account_a键畴、account_b、account_c突雪,每次系統(tǒng)結(jié)算的時(shí)候都會(huì)從這個(gè)表里面取出這三個(gè)字段累加起來使用,那么我們可以違背范式規(guī)則涡贱,增加一個(gè)冗余字段:account_d咏删,存放前三個(gè)字段的和,這樣使用的時(shí)候就不需要取出前三個(gè)再計(jì)算问词,直接取最后一個(gè)字段即可督函。
在往數(shù)據(jù)庫批量插入數(shù)據(jù)的時(shí)候,有兩種方式激挪,一種是通過在接口實(shí)現(xiàn)類里對(duì)集合遍歷調(diào)用mapper辰狡,這樣每有一條數(shù)據(jù)就會(huì)調(diào)一次mapper插一次數(shù)據(jù)庫;另一種是使用mybatis的動(dòng)態(tài)語句foreach垄分,直接把一個(gè)集合插入宛篇,這樣只需要一次mybatis動(dòng)態(tài)代理,速度會(huì)比前一種快很多薄湿,但是需要注意的是mysql默認(rèn)接受sql的大小是1M叫倍,如果集合過大就會(huì)報(bào)異常,可以調(diào)整MySQL安裝目錄下的my.ini文件[mysqld段的"max_allowed_packet = 1M"來改變默認(rèn)接受的大小豺瘤。
關(guān)于索引的建立吆倦,這里的實(shí)踐真的是驚艷到我了,我測試將10萬條數(shù)據(jù)去和一張170萬條數(shù)據(jù)的表校驗(yàn)坐求,對(duì)比出這10萬條數(shù)據(jù)中哪些不在這表中蚕泽,粗略估計(jì)最差的情況就要計(jì)算10萬*170萬次,也就是1700億次桥嗤,當(dāng)然實(shí)際不可能這么多次须妻,我們只是假設(shè)每次校驗(yàn)都校驗(yàn)到最后一條數(shù)據(jù)才能找到派任。我第一次跑的時(shí)候走了70多秒:
然后我給where語句后面的字段加了索引,使用的是B+樹算法璧南,建立索引的過程也等了一小段時(shí)間掌逛,畢竟170萬條數(shù)據(jù),當(dāng)我建立完索引之后一運(yùn)行司倚,直接把我嚇到了:
不得不感嘆豆混,這索引的強(qiáng)大,不實(shí)踐真的沒有體會(huì)动知,以前只是停留在理論上皿伺,這里博主還發(fā)現(xiàn)了一件奇怪的事情,就是我測試完一遍得到上一張圖片的結(jié)果后又跑了一遍:
這里比上一次運(yùn)行又快了十倍盒粮,不知道這次的優(yōu)化是怎么出來的鸵鸥,是一個(gè)小謎題,如果有大神知道請(qǐng)?jiān)u論里指教丹皱。
2妒穴、適當(dāng)使用Redis存儲(chǔ)不常修改但是常用的數(shù)據(jù)
我們可以把常用且不常修改的信息暫存到Redis里面,比如系統(tǒng)維護(hù)的數(shù)據(jù)字典表里面的信息摊崭,這樣校驗(yàn)的時(shí)候直接從內(nèi)存中取讼油,避免了數(shù)據(jù)庫層的訪問,可以大大提高響應(yīng)速度呢簸,如果是大量信息校驗(yàn)的系統(tǒng)里面矮台,這種寫法將會(huì)很大限度的提升接口響應(yīng)速度。
下面是redis的部分配置內(nèi)容:
redis:
connect-timeout: 20s
database: 7
host: 127.0.0.1? ? #本地地址根时,實(shí)際項(xiàng)目看項(xiàng)目的地址
jedis:
pool:
max-active: 200
read-timeout: 20s
下面是具體使用的代碼:
/**
* 常用緩存鍵類
*
* @author Coder_gasenwell
* @version 1.0
* @createDate 2020/5/4
*/
public interface CacheKey {
/**
* 字典緩存前綴
*/
String PREFIX_TEST_SYS_DICT_TYPECODE = "test:sys_dict:typeCode:";
/**
* 字典緩存前綴
*/
String PREFIX_TEST_SYS_DEPT_USERS_TREE = "test:sys_dept_users:tree:";
}
@Autowired
private RedisUtils redisUtils;
//摘選一個(gè)方法作為參考瘦赫,只關(guān)注實(shí)現(xiàn)語句邏輯,每次先檢索redis里面是不是有緩存蛤迎,如果沒有就去數(shù)據(jù)庫取數(shù)據(jù)然后存緩存确虱,如果有的話就直接使用緩存。
@Override
public List getDictSetCache(String typeCode) {
List dictList = null;
// 確認(rèn)緩存中是否有字典忘苛,如果沒有有可能是第一次創(chuàng)建緩存
Set redisKeys = redisUtils.keys(CacheKey.PREFIX_TEST_SYS_DICT_TYPECODE + "*");
if (null != redisKeys && redisKeys.size() > 0) {
dictList = sysDictMapper.listByTypeCode(typeCode);
if (null != dictList && dictList.size() > 0) {
dictList.sort(new Comparator() {
@Override
//這里是對(duì)數(shù)據(jù)進(jìn)行一個(gè)排序蝉娜,可以忽略
public int compare(SysDict o1, SysDict o2) {
return o1.getSort() - o2.getSort();
}
});
redisUtils.set(CacheKey.PREFIX_TEST_SYS_DICT_TYPECODE + dictList.get(0).getTypeCode(), dictList);
}
3、使用異步處理扎唾,校驗(yàn)完核心數(shù)據(jù)即可返回前臺(tái)操作完成召川,并異步跑后續(xù)操作
雖然校驗(yàn)數(shù)據(jù)這里優(yōu)化速度很快了,但是校驗(yàn)完要把海量數(shù)據(jù)取出來進(jìn)行計(jì)算處理胸遇,所以為了避免從前端調(diào)一個(gè)接口讓用戶等太久荧呐,就把校驗(yàn)和后續(xù)計(jì)算分開,校驗(yàn)完成后把不存在的數(shù)據(jù)生產(chǎn)Excel表格導(dǎo)出,存在的數(shù)據(jù)開始走異步倍阐,此時(shí)返回前臺(tái)接口調(diào)用完成概疆。這時(shí)候前臺(tái)會(huì)告訴用戶校驗(yàn)完成,并下載到用戶本地一個(gè)Excel文件峰搪。
Spring里面內(nèi)置了@Async岔冀,所以這里開啟異步處理只需要在方法體上打上該注解即可,然后由主要方法調(diào)用該方法概耻。
4使套、多線程處理海量運(yùn)算
Spring是通過TaskExecutor來實(shí)現(xiàn)多線程的,使用Spring為我們提供的ThreadPoolTaskExecutor來實(shí)例化線程池鞠柄,因?yàn)槲覀兒芏鄷r(shí)候都是調(diào)用異步任務(wù)才會(huì)用到多線程侦高,所以往往都會(huì)配合在啟動(dòng)類上打上@EnableAsync注解,在實(shí)際方法上打@Async注解厌杜,就可以開啟多線程異步任務(wù)奉呛。
@SpringBootApplication(scanBasePackages = "cn.com.test")
@EnableFeignClients(basePackages = "cn.com.test")
@EnableAsync
@EnableDiscoveryClient
@EnableScheduling
@MapperScan("cn.com.test.mapper")
public class ThreadConfig implements AsyncConfigurer{
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
//核心線程數(shù)
threadPool.setCorePoolSize(8);
//最大線程數(shù)
threadPool.setMaxPoolSize(50);
//緩沖隊(duì)列
threadPool.setQueueCapacity(10);
threadPool.setAwaitTerminationSeconds(60);
threadPool.setThreadNamePrefix("TestThread:");
threadPool.initialize();
return threadPool;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
Doctor:我今天是不是很安靜,我的臺(tái)詞都沒了夯尽?
emmm瞧壮,寫的太專注,忘了給Doctor加戲了呐萌,下次一定馁痴。
好了,以上就是我的優(yōu)化實(shí)踐肺孤,這次實(shí)踐確實(shí)體會(huì)到性能優(yōu)化的強(qiáng)大之處,對(duì)于企業(yè)級(jí)開發(fā)的理解也更深了一步济欢,也頗有成就感赠堵,想起來我們畢業(yè)前院長說的一句話:希望你們隨著時(shí)間沉淀技術(shù)沉淀人格。技術(shù)和人格對(duì)于一個(gè)工程師來說法褥,都很重要茫叭。
歡迎批評(píng)指正。
原文作者:Coder_gasenwell
原文鏈接:https://blog.csdn.net/Coder_gasenwell/article/details/105905797