0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

教大家怎么樣才能在日常開(kāi)發(fā)中減少bug

上海磐啟微電子有限公司 ? 來(lái)源:撿田螺的小男孩 ? 作者:撿田螺的小男孩 ? 2021-07-19 09:19 ? 次閱讀

1. 數(shù)據(jù)庫(kù)篇

數(shù)據(jù)庫(kù)篇的話,哪些地方容易導(dǎo)致bug出現(xiàn)呢?我總結(jié)了7個(gè)方面:慢查詢、數(shù)據(jù)庫(kù)字段注意點(diǎn)、事務(wù)失效的場(chǎng)景、死鎖、主從延遲、新老數(shù)據(jù)兼容、一些SQL經(jīng)典注意點(diǎn)。

1.1 慢查詢

1.1.1 是否命中索引

提起慢查詢,我們馬上就會(huì)想到加索引。如果一條SQL沒(méi)加索引,或者沒(méi)有命中索引的話,就會(huì)產(chǎn)生慢查詢。

索引哪些情況會(huì)失效?

查詢條件包含or,可能導(dǎo)致索引失效

如何字段類(lèi)型是字符串,where時(shí)一定用引號(hào)括起來(lái),否則索引失效

like通配符可能導(dǎo)致索引失效。

聯(lián)合索引,查詢時(shí)的條件列不是聯(lián)合索引中的第一個(gè)列,索引失效。

在索引列上使用mysql的內(nèi)置函數(shù),索引失效。

對(duì)索引列運(yùn)算(如,+、-、*、/),索引失效。

索引字段上使用(!= 或者 《 》,not in)時(shí),可能會(huì)導(dǎo)致索引失效。

索引字段上使用is null, is not null,可能導(dǎo)致索引失效。

左連接查詢或者右連接查詢查詢關(guān)聯(lián)的字段編碼格式不一樣,可能導(dǎo)致索引失效。

mysql估計(jì)使用全表掃描要比使用索引快,則不使用索引。

1.1.2 數(shù)據(jù)量大,考慮分庫(kù)分表

單表數(shù)據(jù)量太大,就會(huì)影響SQL執(zhí)行性能。我們知道索引數(shù)據(jù)結(jié)構(gòu)一般是B+樹(shù),一棵高度為3的B+樹(shù),大概可以存儲(chǔ)兩千萬(wàn)的數(shù)據(jù)。超過(guò)這個(gè)數(shù)的話,B+樹(shù)要變高,查詢性能會(huì)下降。

因此,數(shù)據(jù)量大的時(shí)候,建議分庫(kù)分表。分庫(kù)分表的中間件有mycat、sharding-jdbc

1.1.3 不合理的SQL

日常開(kāi)發(fā)中,筆者見(jiàn)過(guò)很多不合理的SQL:比如一個(gè)SQL居然用了6個(gè)表連接,連表太多會(huì)影響查詢性能;再比如一個(gè)表,居然加了10個(gè)索引等等。索引是會(huì)降低了插入和更新SQL性能,所以索引一般不建議太多,一般不能超過(guò)五個(gè)。

1.2 數(shù)據(jù)庫(kù)字段注意點(diǎn)

數(shù)據(jù)庫(kù)字段這塊內(nèi)容,很容易出bug。比如,你測(cè)試環(huán)境修改了表結(jié)構(gòu),加了某個(gè)字段,忘記把腳本帶到生產(chǎn)環(huán)境,那發(fā)版肯定有問(wèn)題了。

1.2.1 字段是否會(huì)超長(zhǎng)

假設(shè)你的數(shù)據(jù)庫(kù)字段是:

`name` varchar(255) DEFAULT NOT NULL

如果請(qǐng)求參數(shù)來(lái)了變量name,字段長(zhǎng)度是300,那插入表的時(shí)候就報(bào)錯(cuò)了。所以需要校驗(yàn)參數(shù),防止字段超長(zhǎng)。

1.2.2 字段為空,是否會(huì)導(dǎo)致空指針等

我們?cè)O(shè)計(jì)數(shù)據(jù)庫(kù)表字段的時(shí)候,盡量把字段設(shè)置為not null。

如果是整形,我們一般使用0或者-1作為默認(rèn)值。

如果字符串,默認(rèn)空字符串

如果數(shù)據(jù)庫(kù)字段設(shè)置為NULL值,容易導(dǎo)致程序空指針;如果數(shù)據(jù)庫(kù)字段設(shè)置為NULL值,需要注意count(具體列) 的使用,會(huì)有坑。

1.2.3 字段缺失

我們的日常開(kāi)發(fā)任務(wù),如果在測(cè)試環(huán)境,對(duì)表進(jìn)行修改,比如添加了一個(gè)新字段,必須要把SQL腳本帶到生產(chǎn)環(huán)境,否則字段缺失,發(fā)版就有問(wèn)題啦。

1.2.4 字段類(lèi)型是否支持表情

如果一個(gè)表字段需要支持表情存儲(chǔ),使用utf8mb4。

1.2.5 謹(jǐn)慎使用text、blob字段

如果你要用一個(gè)字段存儲(chǔ)文件,考慮存儲(chǔ)文件的路徑,而不是保存整個(gè)文件下去。使用text時(shí),涉及查詢條件時(shí),注意創(chuàng)建前綴索引。

1.3 事務(wù)失效的場(chǎng)景

1.3.1 @Transactional 在非public修飾的方法上失效

@Transactional注解,加在非public修飾的方法上,事務(wù)是不會(huì)生效的。spring事務(wù)是借鑒了AOP的思想,也是通過(guò)動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)的。spring事務(wù)自己在調(diào)用動(dòng)態(tài)代理之前,已經(jīng)對(duì)非public方法過(guò)濾了,所以非public方法,事務(wù)不生效。

1.3.2 本地方法直接調(diào)用

以下這個(gè)場(chǎng)景,@Transactional事務(wù)也是無(wú)效的

public class TransactionTest{

public void A(){

//插入一條數(shù)據(jù)

//調(diào)用方法B (本地的類(lèi)調(diào)用,事務(wù)失效了)

B();

}

@Transactional

public void B(){

//插入數(shù)據(jù)

}

}

1.3.3 異常被try.。.catch吃了,導(dǎo)致事務(wù)失效。

@Transactional

public void method(){

try{

//插入一條數(shù)據(jù)

insertA();

//更改一條數(shù)據(jù)

updateB();

}catch(Exception e){

logger.error(“異常被捕獲了,那你的事務(wù)就失效咯”,e);

}

}

1.3.4 rollbackFor屬性設(shè)置錯(cuò)誤

Spring默認(rèn)拋出了未檢查unchecked異常(繼承自RuntimeException 的異常)或者Error才回滾事務(wù);其他異常不會(huì)觸發(fā)回滾事務(wù)。如果在事務(wù)中拋出其他類(lèi)型的異常,就需要指定rollbackFor屬性。

1.3.5 底層數(shù)據(jù)庫(kù)引擎不支持事務(wù)

MyISAM存儲(chǔ)引擎不支持事務(wù),InnoDb就支持事務(wù)

1.3.6 spring事務(wù)和業(yè)務(wù)邏輯代碼必須在一個(gè)線程中

業(yè)務(wù)代碼要和spring事務(wù)的源碼在同一個(gè)線程中,才會(huì)受spring事務(wù)的控制。比如下面代碼,方法mothed的子線程,內(nèi)部執(zhí)行的事務(wù)操作,將不受mothed方法上spring事務(wù)的控制,這一點(diǎn)大家要注意。這是因?yàn)閟pring事務(wù)實(shí)現(xiàn)中使用了ThreadLocal,實(shí)現(xiàn)同一個(gè)線程中數(shù)據(jù)共享。

@Transactional

public void mothed() {

new Thread() {

事務(wù)操作

}.start();

}

1.4 死鎖

死鎖是指兩個(gè)或多個(gè)事務(wù)在同一資源上相互占用,并請(qǐng)求鎖定對(duì)方的資源,從而導(dǎo)致惡性循環(huán)的現(xiàn)象。

MySQL內(nèi)部有一套死鎖檢測(cè)機(jī)制,一旦發(fā)生死鎖會(huì)立即回滾一個(gè)事務(wù),讓另一個(gè)事務(wù)執(zhí)行下去。但死鎖有資源的利用率降低、進(jìn)程得不到正確結(jié)果等危害。

1.4.1 9種情況的SQL加鎖分析

要避免死鎖,需要學(xué)會(huì)分析:一條SQL的加鎖是如何進(jìn)行的?一條SQL加鎖,可以分9種情況進(jìn)行探討:

組合一:id列是主鍵,RC隔離級(jí)別

組合二:id列是二級(jí)唯一索引,RC隔離級(jí)別

組合三:id列是二級(jí)非唯一索引,RC隔離級(jí)別

組合四:id列上沒(méi)有索引,RC隔離級(jí)別

組合五:id列是主鍵,RR隔離級(jí)別

組合六:id列是二級(jí)唯一索引,RR隔離級(jí)別

組合七:id列是二級(jí)非唯一索引,RR隔離級(jí)別

組合八:id列上沒(méi)有索引,RR隔離級(jí)別

組合九:Serializable隔離級(jí)別

1.4.2 如何分析解決死鎖?

分析解決死鎖的步驟如下:

模擬死鎖場(chǎng)景

show engine innodb status;查看死鎖日志

找出死鎖SQL

SQL加鎖分析,這個(gè)可以去官網(wǎng)看哈

分析死鎖日志(持有什么鎖,等待什么鎖)

熟悉鎖模式兼容矩陣,InnoDB存儲(chǔ)引擎中鎖的兼容性矩陣。

有興趣的小伙伴,可以看下我之前寫(xiě)的這篇文章:手把手教你分析Mysql死鎖問(wèn)題

1.5 主從延遲問(wèn)題考慮

先插入,接著就去查詢,這類(lèi)代碼邏輯比較常見(jiàn),這可能會(huì)有問(wèn)題的。一般數(shù)據(jù)庫(kù)都是有主庫(kù),從庫(kù)的。寫(xiě)入的話是寫(xiě)主庫(kù),讀一般是讀從庫(kù)。如果發(fā)生主從延遲,,很可能出現(xiàn)你插入成功了,但是查詢不到的情況。

1.5.1 要求強(qiáng)一致性,考慮讀主庫(kù)

如果是重要業(yè)務(wù),要求強(qiáng)一致性,考慮直接讀主庫(kù)

1.5.2 不要求強(qiáng)一致性,讀從庫(kù)

如果是一般業(yè)務(wù),可以接受短暫的數(shù)據(jù)不一致的話,優(yōu)先考慮讀從庫(kù)。因?yàn)閺膸?kù)可以分擔(dān)主庫(kù)的讀寫(xiě)壓力,提高系統(tǒng)吞吐。

1.6 新老數(shù)據(jù)兼容

1.6.1 新加的字段,考慮存量數(shù)據(jù)的默認(rèn)值

我們?nèi)粘i_(kāi)發(fā)中,隨著業(yè)務(wù)需求變更,經(jīng)常需要給某個(gè)數(shù)據(jù)庫(kù)表添加個(gè)字段。比如在某個(gè)APP配置表,需要添加個(gè)場(chǎng)景號(hào)字段,如scene_type,它的枚舉值是 01、02、03,那我們就要跟業(yè)務(wù)對(duì)齊,新添加的字段,老數(shù)據(jù)是什么默認(rèn)值,是為空還是默認(rèn)01,如果是為NULL的話,程序代碼就要做好空指針處理。

1.6.2 如果新業(yè)務(wù)用老的字段,考慮老數(shù)據(jù)的值是否有坑

如果我們開(kāi)發(fā)中,需要沿用數(shù)據(jù)庫(kù)表的老字段,并且有存量數(shù)據(jù),那就需要考慮老存量數(shù)據(jù)庫(kù)的值是否有坑。比如我們表有個(gè)user_role_code 的字段,老的數(shù)據(jù)中,它枚舉值是 01:超級(jí)管理員 02:管理員 03:一般用戶。假設(shè)業(yè)務(wù)需求是一般用戶拆分為03查詢用戶和04操作用戶,那我們?cè)陂_(kāi)發(fā)中,就要考慮老數(shù)據(jù)值的問(wèn)題啦。

1.7 一些SQL的經(jīng)典注意點(diǎn)

1.7.1 limit大分頁(yè)問(wèn)題

limit大分頁(yè)是一個(gè)非常經(jīng)典的SQL問(wèn)題,我們一般有這3種對(duì)應(yīng)的解決方案

方案一: 如果id是連續(xù)的,可以這樣,返回上次查詢的最大記錄(偏移量),再往下limit

select id,name from employee where id》1000000 limit 10.

方案二: 在業(yè)務(wù)允許的情況下限制頁(yè)數(shù):

建議跟業(yè)務(wù)討論,有沒(méi)有必要查這么后的分頁(yè)啦。因?yàn)榻^大多數(shù)用戶都不會(huì)往后翻太多頁(yè)。谷歌搜索頁(yè)也是限制了頁(yè)數(shù),因此不存在limit大分頁(yè)問(wèn)題。

方案三: 利用延遲關(guān)聯(lián)或者子查詢優(yōu)化超多分頁(yè)場(chǎng)景。(先快速定位需要獲取的id段,然后再關(guān)聯(lián))

SELECT a.* FROM employee a, (select id from employee where 條件 LIMIT 1000000,10 ) b where a.id=b.id

1.7.2 修改、查詢數(shù)據(jù)量多時(shí),考慮分批進(jìn)行。

我們更新或者查詢數(shù)據(jù)庫(kù)數(shù)據(jù)時(shí),盡量避免循環(huán)去操作數(shù)據(jù)庫(kù),可以考慮分批進(jìn)行。比如你要插入10萬(wàn)數(shù)據(jù)的話,可以一次插入500條,執(zhí)行200次。

正例:

remoteBatchQuery(param);

反例:

for(int i=0;i《100000;i++){

remoteSingleQuery(param)

}

2. 代碼層面篇

2.1 編碼細(xì)節(jié)

2.1.1 六大典型空指針問(wèn)題

我們編碼的時(shí)候,需要注意這六種類(lèi)型的空指針問(wèn)題

包裝類(lèi)型的空指針問(wèn)題

級(jí)聯(lián)調(diào)用的空指針問(wèn)題

Equals方法左邊的空指針問(wèn)題

ConcurrentHashMap 類(lèi)似容器不支持 k-v為 null。

集合,數(shù)組直接獲取元素

對(duì)象直接獲取屬性

if(object!=null){

String name = object.getName();

}

2.1.2 線程池使用注意點(diǎn)

使用 Executors.newFixedThreadPool,可能會(huì)出現(xiàn)OOM問(wèn)題,因?yàn)樗褂玫氖菬o(wú)界阻塞隊(duì)列

建議使用自定義的線程池,最好給線程池一個(gè)清晰的命名,方便排查問(wèn)題

不同的業(yè)務(wù),最好做線程池隔離,避免所有的業(yè)務(wù)公用一個(gè)線程池。

線程池異常處理要考慮好

2.1.3 線性安全的集合、類(lèi)

在高并發(fā)場(chǎng)景下,HashMap可能會(huì)出現(xiàn)死循環(huán)。因?yàn)樗欠蔷€性安全的,可以考慮使用ConcurrentHashMap。所以我們使用這些集合的時(shí)候,需要注意是不是線性安全的。

Hashmap、Arraylist、LinkedList、TreeMap等都是線性不安全的;

Vector、Hashtable、ConcurrentHashMap等都是線性安全的

2.1.4 日期格式,金額處理精度等

日常開(kāi)發(fā),經(jīng)常需要對(duì)日期格式化,但是呢,年份設(shè)置為YYYY大寫(xiě)的時(shí)候,是有坑的哦。

Calendar calendar = Calendar.getInstance();

calendar.set(2019, Calendar.DECEMBER, 31);

Date testDate = calendar.getTime();

SimpleDateFormat dtf = new SimpleDateFormat(“YYYY-MM-dd”);

System.out.println(“2019-12-31 轉(zhuǎn) YYYY-MM-dd 格式后 ” + dtf.format(testDate));

運(yùn)行結(jié)果:

2019-12-31 轉(zhuǎn) YYYY-MM-dd 格式后 2020-12-31

還有金額計(jì)算也比較常見(jiàn),我們要注意精度問(wèn)題:

public class DoubleTest {

public static void main(String[] args) {

System.out.println(0.1+0.2);

System.out.println(1.0-0.8);

System.out.println(4.015*100);

System.out.println(123.3/100);

double amount1 = 3.15;

double amount2 = 2.10;

if (amount1 - amount2 == 1.05){

System.out.println(“OK”);

}

}

}

運(yùn)行結(jié)果:

0.30000000000000004

0.19999999999999996

401.49999999999994

1.2329999999999999

2.1.5 大文件處理

讀取大文件的時(shí)候,不要Files.readAllBytes直接讀到內(nèi)存,會(huì)OOM的,建議使用BufferedReader一行一行來(lái),或者使用NIO

2.1.6 使用完IO資源流,需要關(guān)閉

使用try-with-resource,讀寫(xiě)完文件,需要關(guān)閉流

/*

*/

try (FileInputStream inputStream = new FileInputStream(new File(“jay.txt”)) {

// use resources

} catch (FileNotFoundException e) {

log.error(e);

} catch (IOException e) {

log.error(e);

}

2.1.7 try.。.catch異常使用的一些坑

盡量不要使用e.printStackTrace()打印,可能導(dǎo)致字符串常量池內(nèi)存空間占滿

catch了異常,使用log把它打印出來(lái)

不要用一個(gè)Exception捕捉所有可能的異常

不要把捕獲異常當(dāng)做業(yè)務(wù)邏輯來(lái)處理

2.1.8 先查詢,再更新/刪除的并發(fā)一致性

日常開(kāi)發(fā)中,這種代碼實(shí)現(xiàn)經(jīng)??梢?jiàn):先查詢是否有剩余可用的票,再去更新票余量。

if(selectIsAvailable(ticketId){

1、deleteTicketById(ticketId)

2、給現(xiàn)金增加操作

}else{

return “沒(méi)有可用現(xiàn)金券”

}

如果是并發(fā)執(zhí)行,很可能有問(wèn)題的,應(yīng)該利用數(shù)據(jù)庫(kù)更新/刪除的原子性,正解如下:

if(deleteAvailableTicketById(ticketId) == 1){

1、給現(xiàn)金增加操作

}else{

return “沒(méi)有可用現(xiàn)金券”

}

2.2 提供對(duì)外接口

2.2.1 校驗(yàn)參數(shù)合法性

我們提供對(duì)外的接口,不管是提供給客戶端、還是前端,又或是別的系統(tǒng)調(diào)用,都需要校驗(yàn)一下入?yún)⒌暮戏ㄐ浴?/p>

如果你的數(shù)據(jù)庫(kù)字段設(shè)置為varchar(16),對(duì)方傳了一個(gè)32位的字符串過(guò)來(lái),你不校驗(yàn)參數(shù)長(zhǎng)度,插入數(shù)據(jù)庫(kù)直接異常了?!?.2.2 新老接口兼容

很多bug都是因?yàn)樾薷牧藢?duì)外老接口,但是卻不做兼容導(dǎo)致的。關(guān)鍵這個(gè)問(wèn)題多數(shù)是比較嚴(yán)重的,可能直接導(dǎo)致系統(tǒng)發(fā)版失敗的。新手程序員很容易犯這個(gè)錯(cuò)誤哦~

比如我們有個(gè)dubbo的分布式接口,本次你修改了入?yún)ⅲ托枰紤]新老接口兼容。原本是只接收A,B參數(shù),現(xiàn)在你加了一個(gè)參數(shù)C,就可以考慮這樣處理。

//老接口

void oldService(A,B){

//兼容新接口,傳個(gè)null代替C

newService(A,B,null);

}

//新接口,暫時(shí)不能刪掉老接口,需要做兼容。

void newService(A,B,C);

2.2.3 限流,防止大流量壓垮系統(tǒng)

如果瞬間的大流量請(qǐng)求過(guò)來(lái),容易壓垮系統(tǒng)。所以為了保護(hù)我們的系統(tǒng),一般要做限流處理??梢允褂胓uava ratelimiter 組件做限流,也可以用阿里開(kāi)源的Sentinel

2.2.4 接口安全性,加簽驗(yàn)簽,鑒權(quán)

我們轉(zhuǎn)賬等類(lèi)型的接口,一定要注意安全性。一定要鑒權(quán),加簽驗(yàn)簽,為用戶交易保駕護(hù)航。

2.2.5 考慮接口冪等性

接口是需要考慮冪等性的,尤其搶紅包、轉(zhuǎn)賬這些重要接口。最直觀的業(yè)務(wù)場(chǎng)景,就是用戶連著點(diǎn)擊兩次,你的接口有沒(méi)有hold住。

冪等(idempotent、idempotence)是一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念,常見(jiàn)于抽象代數(shù)中。

編程中。一個(gè)冪等操作的特點(diǎn)是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)。

”一般冪等技術(shù)方案有這幾種:

查詢操作

唯一索引

token機(jī)制,防止重復(fù)提交

數(shù)據(jù)庫(kù)的delete刪除操作

樂(lè)觀鎖

悲觀鎖

Redis、zookeeper 分布式鎖(以前搶紅包需求,用了Redis分布式鎖)

狀態(tài)機(jī)冪等

2.3 調(diào)用第三方接口

2.3.1 超時(shí)處理

我們調(diào)用別人的接口,如果超時(shí)了怎么辦呢?

舉個(gè)例子,我們調(diào)用一個(gè)遠(yuǎn)程轉(zhuǎn)賬接口,A客戶給B客戶轉(zhuǎn)100萬(wàn),成功的時(shí)候就把本地轉(zhuǎn)賬流水置為成功,失敗的時(shí)候就把本地流水置為失敗。如果調(diào)用轉(zhuǎn)賬系統(tǒng)超時(shí)了呢,我們?cè)趺刺幚砟??置為成功還是失敗呢?這個(gè)超時(shí)處理可要考慮好,要不然就資金損失了。這種場(chǎng)景下,調(diào)接口超時(shí),我們就可以先不更新本地轉(zhuǎn)賬流水狀態(tài),而是重新發(fā)起查詢遠(yuǎn)程轉(zhuǎn)賬請(qǐng)求,查詢到轉(zhuǎn)賬成功的記錄,再更新本地狀態(tài)狀態(tài)”2.3.2 考慮重試機(jī)制

如果我們調(diào)用一個(gè)遠(yuǎn)程http或者dubbo接口,調(diào)用失敗了,我們可以考慮引入重試機(jī)制。有時(shí)候網(wǎng)路抖動(dòng)一下,接口就調(diào)失敗了,引入重試機(jī)制可以提高用戶體驗(yàn)。但是這個(gè)重試機(jī)制需要評(píng)估次數(shù),或者有些接口不支持冪等,就不適合重試的。

2.3.3 考慮是否降級(jí)處理

假設(shè)我們系統(tǒng)是一個(gè)提供注冊(cè)的服務(wù):用戶注冊(cè)成功之后,調(diào)遠(yuǎn)程A接口發(fā)短信,調(diào)遠(yuǎn)程B接口發(fā)郵件,最后更新注冊(cè)狀態(tài)為成功。

如果調(diào)用接口B發(fā)郵件失敗,那用戶就注冊(cè)失敗,業(yè)務(wù)可能就不會(huì)同意了。這時(shí)候我們可以考慮給B接口降級(jí)處理,提供有損服務(wù)。也就是說(shuō),如果調(diào)用B接口失敗,那先不發(fā)郵件,而是先讓用戶注冊(cè)成功,后面搞個(gè)定時(shí)補(bǔ)發(fā)郵件就好啦。

2.3.4 考慮是否異步處理

我還是使用上個(gè)小節(jié)的用戶注冊(cè)的例子。我們可以開(kāi)個(gè)異步線程去調(diào)A接口發(fā)短信,異步調(diào)B接口發(fā)郵件,那即使A或者B接口調(diào)失敗,我們還是可以保證用戶先注冊(cè)成功。

把發(fā)短信這些通知類(lèi)接口,放到異步線程處理,可以降低接口耗時(shí),提升用戶體驗(yàn)哦。

2.3.5 調(diào)接口異常處理

如果我們調(diào)用一個(gè)遠(yuǎn)程接口,一般需要思考以下:如果別人接口異常,我們要怎么處理,怎么兜底,是重試還是當(dāng)做失?。吭趺幢WC數(shù)據(jù)的最終一致性等等。

3. 緩存篇3.1 數(shù)據(jù)庫(kù)與緩存一致性

使用緩存,可以降低耗時(shí),提供系統(tǒng)吞吐性能。但是,使用緩存,會(huì)存在數(shù)據(jù)一致性的問(wèn)題。

3.1.1 幾種緩存使用模式

Cache-Aside Pattern,旁路緩存模式

Read-Through/Write-Through(讀寫(xiě)穿透)

Write- behind (異步緩存寫(xiě)入)

一般我們使用緩存,都是旁路緩存模式。

讀的時(shí)候,先讀緩存,緩存命中的話,直接返回?cái)?shù)據(jù)

緩存沒(méi)有命中的話,就去讀數(shù)據(jù)庫(kù),從數(shù)據(jù)庫(kù)取出數(shù)據(jù),放入緩存后,同時(shí)返回響應(yīng)。

3.1.2 刪除緩存呢,還是更新緩存?

我們?cè)诓僮骶彺娴臅r(shí)候,到底應(yīng)該刪除緩存還是更新緩存呢?

線程A先發(fā)起一個(gè)寫(xiě)操作,第一步先更新數(shù)據(jù)庫(kù)

線程B再發(fā)起一個(gè)寫(xiě)操作,第二步更新了數(shù)據(jù)庫(kù)

由于網(wǎng)絡(luò)等原因,線程B先更新了緩存

線程A更新緩存。

這時(shí)候,緩存保存的是A的數(shù)據(jù)(老數(shù)據(jù)),數(shù)據(jù)庫(kù)保存的是B的數(shù)據(jù)(新數(shù)據(jù)),數(shù)據(jù)不一致了,臟數(shù)據(jù)出現(xiàn)啦。如果是刪除緩存取代更新緩存則不會(huì)出現(xiàn)這個(gè)臟數(shù)據(jù)問(wèn)題。

3.1.3 先操作數(shù)據(jù)庫(kù)還是先操作緩存

雙寫(xiě)的情況下,先操作數(shù)據(jù)庫(kù)還是先操作緩存?我們?cè)賮?lái)看一個(gè)例子:假設(shè)有A、B兩個(gè)請(qǐng)求,請(qǐng)求A做更新操作,請(qǐng)求B做查詢讀取操作。

線程A發(fā)起一個(gè)寫(xiě)操作,第一步del cache

此時(shí)線程B發(fā)起一個(gè)讀操作,cache miss

線程B繼續(xù)讀DB,讀出來(lái)一個(gè)老數(shù)據(jù)

然后線程B把老數(shù)據(jù)設(shè)置入cache

線程A寫(xiě)入DB最新的數(shù)據(jù)

醬紫就有問(wèn)題啦,緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致了。緩存保存的是老數(shù)據(jù),數(shù)據(jù)庫(kù)保存的是新數(shù)據(jù)。因此,Cache-Aside緩存模式,選擇了先操作數(shù)據(jù)庫(kù)而不是先操作緩存。

3.1.4 如何保證最終一致性

緩存延時(shí)雙刪

刪除緩存重試機(jī)制

讀取biglog異步刪除緩存

3.2 緩存穿透

緩存穿透:指查詢一個(gè)一定不存在的數(shù)據(jù),由于緩存不命中時(shí),需要從數(shù)據(jù)庫(kù)查詢,查不到數(shù)據(jù)則不寫(xiě)入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到數(shù)據(jù)庫(kù)去查詢,進(jìn)而給數(shù)據(jù)庫(kù)帶來(lái)壓力?!本彺娲┩敢话愣际沁@幾種情況產(chǎn)生的:業(yè)務(wù)不合理的設(shè)計(jì)、業(yè)務(wù)/運(yùn)維/開(kāi)發(fā)失誤的操作、黑客非法請(qǐng)求攻擊。如何避免緩存穿透呢?一般有三種方法。

如果是非法請(qǐng)求,我們?cè)贏PI入口,對(duì)參數(shù)進(jìn)行校驗(yàn),過(guò)濾非法值。

如果查詢數(shù)據(jù)庫(kù)為空,我們可以給緩存設(shè)置個(gè)空值,或者默認(rèn)值。但是如有有寫(xiě)請(qǐng)求進(jìn)來(lái)的話,需要更新緩存哈,以保證緩存一致性,同時(shí),最后給緩存設(shè)置適當(dāng)?shù)倪^(guò)期時(shí)間。(業(yè)務(wù)上比較常用,簡(jiǎn)單有效)

使用布隆過(guò)濾器快速判斷數(shù)據(jù)是否存在。即一個(gè)查詢請(qǐng)求過(guò)來(lái)時(shí),先通過(guò)布隆過(guò)濾器判斷值是否存在,存在才繼續(xù)往下查。

3.3 緩存雪崩

緩存雪崩:指緩存中數(shù)據(jù)大批量到過(guò)期時(shí)間,而查詢數(shù)據(jù)量巨大,引起數(shù)據(jù)庫(kù)壓力過(guò)大甚至down機(jī)?!本彺嫜┍家话闶怯捎诖罅繑?shù)據(jù)同時(shí)過(guò)期造成的,對(duì)于這個(gè)原因,可通過(guò)均勻設(shè)置過(guò)期時(shí)間解決,即讓過(guò)期時(shí)間相對(duì)離散一點(diǎn)。如采用一個(gè)較大固定值+一個(gè)較小的隨機(jī)值,5小時(shí)+0到1800秒醬紫。

Redis 故障宕機(jī)也可能引起緩存雪奔。這就需要構(gòu)造Redis高可用集群啦。

3.4 緩存機(jī)擊穿

緩存擊穿:指熱點(diǎn)key在某個(gè)時(shí)間點(diǎn)過(guò)期的時(shí)候,而恰好在這個(gè)時(shí)間點(diǎn)對(duì)這個(gè)Key有大量的并發(fā)請(qǐng)求過(guò)來(lái),從而大量的請(qǐng)求打到db?!本彺鎿舸┛粗悬c(diǎn)像緩存雪崩,其實(shí)它兩區(qū)別是,緩存雪奔是指數(shù)據(jù)庫(kù)壓力過(guò)大甚至down機(jī),緩存擊穿只是大量并發(fā)請(qǐng)求到了DB數(shù)據(jù)庫(kù)層面??梢哉J(rèn)為擊穿是緩存雪奔的一個(gè)子集吧。有些文章認(rèn)為它倆區(qū)別,是在于擊穿針對(duì)某一熱點(diǎn)key緩存,雪奔則是很多key。

解決方案就有兩種:

使用互斥鎖方案。緩存失效時(shí),不是立即去加載db數(shù)據(jù),而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時(shí)候,再去加載db數(shù)據(jù)庫(kù)數(shù)據(jù)和設(shè)置緩存。否則就去重試獲取緩存。

“永不過(guò)期”,是指沒(méi)有設(shè)置過(guò)期時(shí)間,但是熱點(diǎn)數(shù)據(jù)快要過(guò)期時(shí),異步線程去更新和設(shè)置過(guò)期時(shí)間。

3.5 緩存熱Key

在Redis中,我們把訪問(wèn)頻率高的key,稱為熱點(diǎn)key。如果某一熱點(diǎn)key的請(qǐng)求到服務(wù)器主機(jī)時(shí),由于請(qǐng)求量特別大,可能會(huì)導(dǎo)致主機(jī)資源不足,甚至宕機(jī),從而影響正常的服務(wù)。

如何解決熱key問(wèn)題?

Redis集群擴(kuò)容:增加分片副本,均衡讀流量;

對(duì)熱key進(jìn)行hash散列,比如將一個(gè)key備份為key1,key2……keyN,同樣的數(shù)據(jù)N個(gè)備份,N個(gè)備份分布到不同分片,訪問(wèn)時(shí)可隨機(jī)訪問(wèn)N個(gè)備份中的一個(gè),進(jìn)一步分擔(dān)讀流量;

使用二級(jí)緩存,即JVM本地緩存,減少Redis的讀請(qǐng)求。

3.6 緩存容量?jī)?nèi)存考慮

3.6.1 評(píng)估容量,合理利用

如果我們使用的是Redis,而Redis的內(nèi)存是比較昂貴的,我們不要什么數(shù)據(jù)都往Redis里面塞,一般Redis只緩存查詢比較頻繁的數(shù)據(jù)。同時(shí),我們要合理評(píng)估Redis的容量,也避免頻繁set覆蓋,導(dǎo)致設(shè)置了過(guò)期時(shí)間的key失效。

如果我們使用的是本地緩存,如guava的本地緩存,也要評(píng)估下容量。避免容量不夠。

3.6.2 Redis的八種內(nèi)存淘汰機(jī)制

為了避免Redis內(nèi)存不夠用,Redis用8種內(nèi)存淘汰策略保護(hù)自己~

volatile-lru:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),從設(shè)置了過(guò)期時(shí)間的key中使用LRU(最近最少使用)算法進(jìn)行淘汰;

allkeys-lru:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),從所有key中使用LRU(最近最少使用)算法進(jìn)行淘汰。

volatile-lfu:4.0版本新增,當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),在過(guò)期的key中,使用LFU算法進(jìn)行刪除key。

allkeys-lfu:4.0版本新增,當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),從所有key中使用LFU算法進(jìn)行淘汰;

volatile-random:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),從設(shè)置了過(guò)期時(shí)間的key中,隨機(jī)淘汰數(shù)據(jù);。

allkeys-random:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),從所有key中隨機(jī)淘汰數(shù)據(jù)。

volatile-ttl:當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),在設(shè)置了過(guò)期時(shí)間的key中,根據(jù)過(guò)期時(shí)間進(jìn)行淘汰,越早過(guò)期的優(yōu)先被淘汰;

noeviction:默認(rèn)策略,當(dāng)內(nèi)存不足以容納新寫(xiě)入數(shù)據(jù)時(shí),新寫(xiě)入操作會(huì)報(bào)錯(cuò)。

”3.6.3 不同的業(yè)務(wù)場(chǎng)景,Redis選擇適合的數(shù)據(jù)結(jié)構(gòu)

排行榜適合用zset

緩存用戶信息一般用hash

消息隊(duì)列,文章列表適用用list

用戶標(biāo)簽、社交需求一般用set

計(jì)數(shù)器、分布式鎖等一般用String類(lèi)型

3.7 Redis一些有坑的命令

不能使用 keys指令

慎用O(n)復(fù)雜度命令,如hgetall等

慎用Redis的monitor命令

禁止使用flushall、flushdb

注意使用del命令

編輯:jq

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • BUG
    BUG
    +關(guān)注

    關(guān)注

    0

    文章

    155

    瀏覽量

    15628

原文標(biāo)題:聊聊日常開(kāi)發(fā)中,如何減少 bug 呢?

文章出處:【微信號(hào):gh_6a53af9e8109,微信公眾號(hào):上海磐啟微電子有限公司】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    熱繼電器故障是常開(kāi)還是常閉

    熱繼電器的故障表現(xiàn)并不直接等同于常開(kāi)或常閉的狀態(tài),而是與其內(nèi)部觸點(diǎn)的正常與異常工作狀態(tài)有關(guān)。熱繼電器常開(kāi)點(diǎn)和常閉點(diǎn)是兩種不同類(lèi)型的觸點(diǎn),它們各自具有特定的功能和行為。 常開(kāi)點(diǎn)(No
    的頭像 發(fā)表于 08-27 11:16 ?262次閱讀

    磁性開(kāi)關(guān)是常開(kāi)還是常閉

    磁性開(kāi)關(guān)可以是常開(kāi)型,也可以是常閉型,這取決于其設(shè)計(jì)和應(yīng)用需求。 磁性開(kāi)關(guān)的常開(kāi)與常閉類(lèi)型 常開(kāi)型磁性開(kāi)關(guān) :在常態(tài)(即無(wú)磁場(chǎng)作用時(shí)),開(kāi)關(guān)觸點(diǎn)處于斷開(kāi)狀態(tài)。當(dāng)磁場(chǎng)靠近并作用于開(kāi)關(guān)時(shí),觸點(diǎn)會(huì)閉合
    的頭像 發(fā)表于 08-19 10:37 ?427次閱讀

    小型繼電器的常開(kāi)常閉點(diǎn)是什么

    小型繼電器是一種常見(jiàn)的電子元件,廣泛應(yīng)用于自動(dòng)控制、電力系統(tǒng)、通信設(shè)備等領(lǐng)域。在繼電器的工作原理,常開(kāi)常閉點(diǎn)是其核心組成部分,對(duì)于繼電器的正常工作至關(guān)重要。本文將詳細(xì)介紹小型繼電器的常開(kāi)常閉點(diǎn)
    的頭像 發(fā)表于 06-29 10:20 ?408次閱讀

    三菱plc常開(kāi)指令是什么

    菱PLC,常開(kāi)指令是一種基本的邏輯指令,用于實(shí)現(xiàn)電路的接通和斷開(kāi)。 常開(kāi)指令的定義 常開(kāi)指令,也稱為常開(kāi)觸點(diǎn)或
    的頭像 發(fā)表于 06-20 10:44 ?843次閱讀

    esp32c6怎么才能在rcp里面把字符串信息發(fā)送gateway呢?

    剛接觸esp32c6,zigbee有g(shù)ateway和rcp例子,該怎么才能在rcp里面把字符串信息發(fā)送gateway呢,有佬給例子看看嗎
    發(fā)表于 06-05 06:02

    BUG收集】為昕原理圖設(shè)計(jì)EDA軟件(Jupiter)免費(fèi)評(píng)測(cè)活動(dòng)常見(jiàn)問(wèn)題及BUG收集

    產(chǎn)出文件 試用例. 【BUG問(wèn)題反饋要求】 BUG問(wèn)題反饋要求模板*附件:BUG提交表-模版.xlsx 編號(hào)模塊BUG標(biāo)題
    發(fā)表于 04-25 18:23

    AUIR3241STR SPICE模型如何處理才能在LTspice運(yùn)行?

    官網(wǎng)上提供是加密SPICE模型,在LTspice運(yùn)行不了。如何處理才能在LTspice運(yùn)行
    發(fā)表于 02-20 06:47

    程序的R地址都是什么意思?怎么才能看懂?

    程序的R地址都是什么意思?怎么才能看懂? 在程序,R地址通常指的是寄存器地址,它是用來(lái)存儲(chǔ)和訪問(wèn)計(jì)算機(jī)的數(shù)據(jù)的硬件部件。寄存器是計(jì)算
    的頭像 發(fā)表于 02-18 10:49 ?820次閱讀

    什么表示常開(kāi)觸點(diǎn)并聯(lián)連接指令

    常開(kāi)觸點(diǎn)并聯(lián)連接指令是指將電路的多個(gè)常開(kāi)觸點(diǎn)連接在一起,使它們同時(shí)閉合或斷開(kāi)的一種電氣連接方式。在電路中有許多應(yīng)用需要同時(shí)激活或關(guān)閉多個(gè)設(shè)備或功能,這時(shí)就可以使用常開(kāi)觸點(diǎn)并聯(lián)連接來(lái)實(shí)
    的頭像 發(fā)表于 01-16 09:44 ?841次閱讀

    繼電控制四大動(dòng)作狀態(tài):常開(kāi)、常閉、自鎖、互鎖

    繼電控制四大動(dòng)作狀態(tài):常開(kāi)、常閉、自鎖、互鎖 繼電器是一種常用的電氣控制元件,廣泛應(yīng)用于工業(yè)、家庭和商業(yè)領(lǐng)域。繼電器的作用是通過(guò)控制電流來(lái)實(shí)現(xiàn)開(kāi)關(guān)的閉合與斷開(kāi),從而控制電路的設(shè)備或電器的工作狀態(tài)
    的頭像 發(fā)表于 11-17 12:59 ?2586次閱讀

    怎樣才能在有限的容量下發(fā)揮電池的極限續(xù)航能力

    電子發(fā)燒友網(wǎng)站提供《怎樣才能在有限的容量下發(fā)揮電池的極限續(xù)航能力.doc》資料免費(fèi)下載
    發(fā)表于 11-14 14:38 ?0次下載
    怎樣<b class='flag-5'>才能在</b>有限的容量下發(fā)揮電池的極限續(xù)航能力

    PLC壓線鼻子的正確接法 PLC程序要用常開(kāi)的原因

    在PLC編程,一般把停止按鈕物理接點(diǎn)接成常閉,程序寫(xiě)成常開(kāi),這樣的停止按鈕在這套設(shè)備只起到信號(hào)作用。
    發(fā)表于 11-07 11:00 ?765次閱讀
    PLC壓線鼻子的正確接法 PLC程序<b class='flag-5'>中</b>要用<b class='flag-5'>常開(kāi)</b>的原因

    常見(jiàn)的PLC系統(tǒng)BUG有哪些?如何減少這些BUG的產(chǎn)生?

    PLC系統(tǒng)可能會(huì)遇到各種不同類(lèi)型的BUG,以下是一些常見(jiàn)的PLC系統(tǒng)BUG以及如何減少這些BUG的產(chǎn)生的建議: (1)邏輯錯(cuò)誤:邏輯錯(cuò)誤是最常見(jiàn)的PLC系統(tǒng)
    的頭像 發(fā)表于 10-31 11:30 ?813次閱讀
    常見(jiàn)的PLC系統(tǒng)<b class='flag-5'>BUG</b>有哪些?如何<b class='flag-5'>減少</b>這些<b class='flag-5'>BUG</b>的產(chǎn)生?

    常見(jiàn)的PLC系統(tǒng)BUG有哪些?如何減少BUG的產(chǎn)生呢?

    PLC系統(tǒng)可能會(huì)遇到各種不同類(lèi)型的BUG,以下是一些常見(jiàn)的PLC系統(tǒng)BUG以及如何減少這些BUG的產(chǎn)生的建議
    的頭像 發(fā)表于 10-31 11:29 ?760次閱讀

    當(dāng)物聯(lián)網(wǎng)端側(cè)開(kāi)始擁抱AI,什么的MCU才能堪重任?

    當(dāng)物聯(lián)網(wǎng)端側(cè)開(kāi)始擁抱AI,什么的MCU才能堪重任?
    的頭像 發(fā)表于 10-17 17:54 ?503次閱讀
    當(dāng)物聯(lián)網(wǎng)端側(cè)開(kāi)始擁抱AI,什么<b class='flag-5'>樣</b>的MCU<b class='flag-5'>才能</b>堪重任?