1. 數據庫篇
數據庫篇的話,哪些地方容易導致bug出現呢?我總結了7個方面:慢查詢、數據庫字段注意點、事務失效的場景、死鎖、主從延遲、新老數據兼容、一些SQL經典注意點。
1.1 慢查詢
1.1.1 是否命中索引
提起慢查詢,我們馬上就會想到加索引。如果一條SQL沒加索引,或者沒有命中索引的話,就會產生慢查詢。
索引哪些情況會失效?
查詢條件包含or,可能導致索引失效
如何字段類型是字符串,where時一定用引號括起來,否則索引失效
like通配符可能導致索引失效。
聯合索引,查詢時的條件列不是聯合索引中的第一個列,索引失效。
在索引列上使用mysql的內置函數,索引失效。
對索引列運算(如,+、-、*、/),索引失效。
索引字段上使用(!= 或者 《 》,not in)時,可能會導致索引失效。
索引字段上使用is null, is not null,可能導致索引失效。
左連接查詢或者右連接查詢查詢關聯的字段編碼格式不一樣,可能導致索引失效。
mysql估計使用全表掃描要比使用索引快,則不使用索引。
1.1.2 數據量大,考慮分庫分表
單表數據量太大,就會影響SQL執行性能。我們知道索引數據結構一般是B+樹,一棵高度為3的B+樹,大概可以存儲兩千萬的數據。超過這個數的話,B+樹要變高,查詢性能會下降。
因此,數據量大的時候,建議分庫分表。分庫分表的中間件有mycat、sharding-jdbc
1.1.3 不合理的SQL
日常開發中,筆者見過很多不合理的SQL:比如一個SQL居然用了6個表連接,連表太多會影響查詢性能;再比如一個表,居然加了10個索引等等。索引是會降低了插入和更新SQL性能,所以索引一般不建議太多,一般不能超過五個。
1.2 數據庫字段注意點
數據庫字段這塊內容,很容易出bug。比如,你測試環境修改了表結構,加了某個字段,忘記把腳本帶到生產環境,那發版肯定有問題了。
1.2.1 字段是否會超長
假設你的數據庫字段是:
`name` varchar(255) DEFAULT NOT NULL
如果請求參數來了變量name,字段長度是300,那插入表的時候就報錯了。所以需要校驗參數,防止字段超長。
1.2.2 字段為空,是否會導致空指針等
我們設計數據庫表字段的時候,盡量把字段設置為not null。
如果是整形,我們一般使用0或者-1作為默認值。
如果字符串,默認空字符串
如果數據庫字段設置為NULL值,容易導致程序空指針;如果數據庫字段設置為NULL值,需要注意count(具體列) 的使用,會有坑。
1.2.3 字段缺失
我們的日常開發任務,如果在測試環境,對表進行修改,比如添加了一個新字段,必須要把SQL腳本帶到生產環境,否則字段缺失,發版就有問題啦。
1.2.4 字段類型是否支持表情
如果一個表字段需要支持表情存儲,使用utf8mb4。
1.2.5 謹慎使用text、blob字段
如果你要用一個字段存儲文件,考慮存儲文件的路徑,而不是保存整個文件下去。使用text時,涉及查詢條件時,注意創建前綴索引。
1.3 事務失效的場景
1.3.1 @Transactional 在非public修飾的方法上失效
@Transactional注解,加在非public修飾的方法上,事務是不會生效的。spring事務是借鑒了AOP的思想,也是通過動態代理來實現的。spring事務自己在調用動態代理之前,已經對非public方法過濾了,所以非public方法,事務不生效。
1.3.2 本地方法直接調用
以下這個場景,@Transactional事務也是無效的
public class TransactionTest{
public void A(){
//插入一條數據
//調用方法B (本地的類調用,事務失效了)
B();
}
@Transactional
public void B(){
//插入數據
}
}
1.3.3 異常被try.。.catch吃了,導致事務失效。
@Transactional
public void method(){
try{
//插入一條數據
insertA();
//更改一條數據
updateB();
}catch(Exception e){
logger.error(“異常被捕獲了,那你的事務就失效咯”,e);
}
}
1.3.4 rollbackFor屬性設置錯誤
Spring默認拋出了未檢查unchecked異常(繼承自RuntimeException 的異常)或者Error才回滾事務;其他異常不會觸發回滾事務。如果在事務中拋出其他類型的異常,就需要指定rollbackFor屬性。
1.3.5 底層數據庫引擎不支持事務
MyISAM存儲引擎不支持事務,InnoDb就支持事務
1.3.6 spring事務和業務邏輯代碼必須在一個線程中
業務代碼要和spring事務的源碼在同一個線程中,才會受spring事務的控制。比如下面代碼,方法mothed的子線程,內部執行的事務操作,將不受mothed方法上spring事務的控制,這一點大家要注意。這是因為spring事務實現中使用了ThreadLocal,實現同一個線程中數據共享。
@Transactional
public void mothed() {
new Thread() {
事務操作
}.start();
}
1.4 死鎖
死鎖是指兩個或多個事務在同一資源上相互占用,并請求鎖定對方的資源,從而導致惡性循環的現象。
MySQL內部有一套死鎖檢測機制,一旦發生死鎖會立即回滾一個事務,讓另一個事務執行下去。但死鎖有資源的利用率降低、進程得不到正確結果等危害。
1.4.1 9種情況的SQL加鎖分析
要避免死鎖,需要學會分析:一條SQL的加鎖是如何進行的?一條SQL加鎖,可以分9種情況進行探討:
組合一:id列是主鍵,RC隔離級別
組合二:id列是二級唯一索引,RC隔離級別
組合三:id列是二級非唯一索引,RC隔離級別
組合四:id列上沒有索引,RC隔離級別
組合五:id列是主鍵,RR隔離級別
組合六:id列是二級唯一索引,RR隔離級別
組合七:id列是二級非唯一索引,RR隔離級別
組合八:id列上沒有索引,RR隔離級別
組合九:Serializable隔離級別
1.4.2 如何分析解決死鎖?
分析解決死鎖的步驟如下:
模擬死鎖場景
show engine innodb status;查看死鎖日志
找出死鎖SQL
SQL加鎖分析,這個可以去官網看哈
分析死鎖日志(持有什么鎖,等待什么鎖)
熟悉鎖模式兼容矩陣,InnoDB存儲引擎中鎖的兼容性矩陣。
有興趣的小伙伴,可以看下我之前寫的這篇文章:手把手教你分析Mysql死鎖問題
1.5 主從延遲問題考慮
先插入,接著就去查詢,這類代碼邏輯比較常見,這可能會有問題的。一般數據庫都是有主庫,從庫的。寫入的話是寫主庫,讀一般是讀從庫。如果發生主從延遲,,很可能出現你插入成功了,但是查詢不到的情況。
1.5.1 要求強一致性,考慮讀主庫
如果是重要業務,要求強一致性,考慮直接讀主庫
1.5.2 不要求強一致性,讀從庫
如果是一般業務,可以接受短暫的數據不一致的話,優先考慮讀從庫。因為從庫可以分擔主庫的讀寫壓力,提高系統吞吐。
1.6 新老數據兼容
1.6.1 新加的字段,考慮存量數據的默認值
我們日常開發中,隨著業務需求變更,經常需要給某個數據庫表添加個字段。比如在某個APP配置表,需要添加個場景號字段,如scene_type,它的枚舉值是 01、02、03,那我們就要跟業務對齊,新添加的字段,老數據是什么默認值,是為空還是默認01,如果是為NULL的話,程序代碼就要做好空指針處理。
1.6.2 如果新業務用老的字段,考慮老數據的值是否有坑
如果我們開發中,需要沿用數據庫表的老字段,并且有存量數據,那就需要考慮老存量數據庫的值是否有坑。比如我們表有個user_role_code 的字段,老的數據中,它枚舉值是 01:超級管理員 02:管理員 03:一般用戶。假設業務需求是一般用戶拆分為03查詢用戶和04操作用戶,那我們在開發中,就要考慮老數據值的問題啦。
1.7 一些SQL的經典注意點
1.7.1 limit大分頁問題
limit大分頁是一個非常經典的SQL問題,我們一般有這3種對應的解決方案
方案一: 如果id是連續的,可以這樣,返回上次查詢的最大記錄(偏移量),再往下limit
select id,name from employee where id》1000000 limit 10.
方案二: 在業務允許的情況下限制頁數:
建議跟業務討論,有沒有必要查這么后的分頁啦。因為絕大多數用戶都不會往后翻太多頁。谷歌搜索頁也是限制了頁數,因此不存在limit大分頁問題。
方案三: 利用延遲關聯或者子查詢優化超多分頁場景。(先快速定位需要獲取的id段,然后再關聯)
SELECT a.* FROM employee a, (select id from employee where 條件 LIMIT 1000000,10 ) b where a.id=b.id
1.7.2 修改、查詢數據量多時,考慮分批進行。
我們更新或者查詢數據庫數據時,盡量避免循環去操作數據庫,可以考慮分批進行。比如你要插入10萬數據的話,可以一次插入500條,執行200次。
正例:
remoteBatchQuery(param);
反例:
for(int i=0;i《100000;i++){
remoteSingleQuery(param)
}
2. 代碼層面篇
2.1 編碼細節
2.1.1 六大典型空指針問題
我們編碼的時候,需要注意這六種類型的空指針問題
包裝類型的空指針問題
級聯調用的空指針問題
Equals方法左邊的空指針問題
ConcurrentHashMap 類似容器不支持 k-v為 null。
集合,數組直接獲取元素
對象直接獲取屬性
if(object!=null){
String name = object.getName();
}
2.1.2 線程池使用注意點
使用 Executors.newFixedThreadPool,可能會出現OOM問題,因為它使用的是無界阻塞隊列
建議使用自定義的線程池,最好給線程池一個清晰的命名,方便排查問題
不同的業務,最好做線程池隔離,避免所有的業務公用一個線程池。
線程池異常處理要考慮好
2.1.3 線性安全的集合、類
在高并發場景下,HashMap可能會出現死循環。因為它是非線性安全的,可以考慮使用ConcurrentHashMap。所以我們使用這些集合的時候,需要注意是不是線性安全的。
Hashmap、Arraylist、LinkedList、TreeMap等都是線性不安全的;
Vector、Hashtable、ConcurrentHashMap等都是線性安全的
2.1.4 日期格式,金額處理精度等
日常開發,經常需要對日期格式化,但是呢,年份設置為YYYY大寫的時候,是有坑的哦。
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 轉 YYYY-MM-dd 格式后 ” + dtf.format(testDate));
運行結果:
2019-12-31 轉 YYYY-MM-dd 格式后 2020-12-31
還有金額計算也比較常見,我們要注意精度問題:
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”);
}
}
}
運行結果:
0.30000000000000004
0.19999999999999996
401.49999999999994
1.2329999999999999
2.1.5 大文件處理
讀取大文件的時候,不要Files.readAllBytes直接讀到內存,會OOM的,建議使用BufferedReader一行一行來,或者使用NIO
2.1.6 使用完IO資源流,需要關閉
使用try-with-resource,讀寫完文件,需要關閉流
/*
*/
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()打印,可能導致字符串常量池內存空間占滿
catch了異常,使用log把它打印出來
不要用一個Exception捕捉所有可能的異常
不要把捕獲異常當做業務邏輯來處理
2.1.8 先查詢,再更新/刪除的并發一致性
日常開發中,這種代碼實現經常可見:先查詢是否有剩余可用的票,再去更新票余量。
if(selectIsAvailable(ticketId){
1、deleteTicketById(ticketId)
2、給現金增加操作
}else{
return “沒有可用現金券”
}
如果是并發執行,很可能有問題的,應該利用數據庫更新/刪除的原子性,正解如下:
if(deleteAvailableTicketById(ticketId) == 1){
1、給現金增加操作
}else{
return “沒有可用現金券”
}
2.2 提供對外接口
2.2.1 校驗參數合法性
我們提供對外的接口,不管是提供給客戶端、還是前端,又或是別的系統調用,都需要校驗一下入參的合法性。
如果你的數據庫字段設置為varchar(16),對方傳了一個32位的字符串過來,你不校驗參數長度,插入數據庫直接異常了?!?.2.2 新老接口兼容
很多bug都是因為修改了對外老接口,但是卻不做兼容導致的。關鍵這個問題多數是比較嚴重的,可能直接導致系統發版失敗的。新手程序員很容易犯這個錯誤哦~
比如我們有個dubbo的分布式接口,本次你修改了入參,就需要考慮新老接口兼容。原本是只接收A,B參數,現在你加了一個參數C,就可以考慮這樣處理。
//老接口
void oldService(A,B){
//兼容新接口,傳個null代替C
newService(A,B,null);
}
//新接口,暫時不能刪掉老接口,需要做兼容。
void newService(A,B,C);
2.2.3 限流,防止大流量壓垮系統
如果瞬間的大流量請求過來,容易壓垮系統。所以為了保護我們的系統,一般要做限流處理??梢允褂胓uava ratelimiter 組件做限流,也可以用阿里開源的Sentinel
2.2.4 接口安全性,加簽驗簽,鑒權
我們轉賬等類型的接口,一定要注意安全性。一定要鑒權,加簽驗簽,為用戶交易保駕護航。
2.2.5 考慮接口冪等性
接口是需要考慮冪等性的,尤其搶紅包、轉賬這些重要接口。最直觀的業務場景,就是用戶連著點擊兩次,你的接口有沒有hold住。
冪等(idempotent、idempotence)是一個數學與計算機學概念,常見于抽象代數中。
在編程中。一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。
”一般冪等技術方案有這幾種:
查詢操作
唯一索引
token機制,防止重復提交
數據庫的delete刪除操作
樂觀鎖
悲觀鎖
Redis、zookeeper 分布式鎖(以前搶紅包需求,用了Redis分布式鎖)
狀態機冪等
2.3 調用第三方接口
2.3.1 超時處理
我們調用別人的接口,如果超時了怎么辦呢?
舉個例子,我們調用一個遠程轉賬接口,A客戶給B客戶轉100萬,成功的時候就把本地轉賬流水置為成功,失敗的時候就把本地流水置為失敗。如果調用轉賬系統超時了呢,我們怎么處理呢?置為成功還是失敗呢?這個超時處理可要考慮好,要不然就資金損失了。這種場景下,調接口超時,我們就可以先不更新本地轉賬流水狀態,而是重新發起查詢遠程轉賬請求,查詢到轉賬成功的記錄,再更新本地狀態狀態”2.3.2 考慮重試機制
如果我們調用一個遠程http或者dubbo接口,調用失敗了,我們可以考慮引入重試機制。有時候網路抖動一下,接口就調失敗了,引入重試機制可以提高用戶體驗。但是這個重試機制需要評估次數,或者有些接口不支持冪等,就不適合重試的。
2.3.3 考慮是否降級處理
假設我們系統是一個提供注冊的服務:用戶注冊成功之后,調遠程A接口發短信,調遠程B接口發郵件,最后更新注冊狀態為成功。
如果調用接口B發郵件失敗,那用戶就注冊失敗,業務可能就不會同意了。這時候我們可以考慮給B接口降級處理,提供有損服務。也就是說,如果調用B接口失敗,那先不發郵件,而是先讓用戶注冊成功,后面搞個定時補發郵件就好啦。
2.3.4 考慮是否異步處理
我還是使用上個小節的用戶注冊的例子。我們可以開個異步線程去調A接口發短信,異步調B接口發郵件,那即使A或者B接口調失敗,我們還是可以保證用戶先注冊成功。
把發短信這些通知類接口,放到異步線程處理,可以降低接口耗時,提升用戶體驗哦。
2.3.5 調接口異常處理
如果我們調用一個遠程接口,一般需要思考以下:如果別人接口異常,我們要怎么處理,怎么兜底,是重試還是當做失???怎么保證數據的最終一致性等等。
3. 緩存篇3.1 數據庫與緩存一致性
使用緩存,可以降低耗時,提供系統吞吐性能。但是,使用緩存,會存在數據一致性的問題。
3.1.1 幾種緩存使用模式
Cache-Aside Pattern,旁路緩存模式
Read-Through/Write-Through(讀寫穿透)
Write- behind (異步緩存寫入)
一般我們使用緩存,都是旁路緩存模式。
讀的時候,先讀緩存,緩存命中的話,直接返回數據
緩存沒有命中的話,就去讀數據庫,從數據庫取出數據,放入緩存后,同時返回響應。
3.1.2 刪除緩存呢,還是更新緩存?
我們在操作緩存的時候,到底應該刪除緩存還是更新緩存呢?
線程A先發起一個寫操作,第一步先更新數據庫
線程B再發起一個寫操作,第二步更新了數據庫
由于網絡等原因,線程B先更新了緩存
線程A更新緩存。
這時候,緩存保存的是A的數據(老數據),數據庫保存的是B的數據(新數據),數據不一致了,臟數據出現啦。如果是刪除緩存取代更新緩存則不會出現這個臟數據問題。
3.1.3 先操作數據庫還是先操作緩存
雙寫的情況下,先操作數據庫還是先操作緩存?我們再來看一個例子:假設有A、B兩個請求,請求A做更新操作,請求B做查詢讀取操作。
線程A發起一個寫操作,第一步del cache
此時線程B發起一個讀操作,cache miss
線程B繼續讀DB,讀出來一個老數據
然后線程B把老數據設置入cache
線程A寫入DB最新的數據
醬紫就有問題啦,緩存和數據庫的數據不一致了。緩存保存的是老數據,數據庫保存的是新數據。因此,Cache-Aside緩存模式,選擇了先操作數據庫而不是先操作緩存。
3.1.4 如何保證最終一致性
緩存延時雙刪
刪除緩存重試機制
讀取biglog異步刪除緩存
3.2 緩存穿透
緩存穿透:指查詢一個一定不存在的數據,由于緩存不命中時,需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,進而給數據庫帶來壓力?!本彺娲┩敢话愣际沁@幾種情況產生的:業務不合理的設計、業務/運維/開發失誤的操作、黑客非法請求攻擊。如何避免緩存穿透呢?一般有三種方法。
如果是非法請求,我們在API入口,對參數進行校驗,過濾非法值。
如果查詢數據庫為空,我們可以給緩存設置個空值,或者默認值。但是如有有寫請求進來的話,需要更新緩存哈,以保證緩存一致性,同時,最后給緩存設置適當的過期時間。(業務上比較常用,簡單有效)
使用布隆過濾器快速判斷數據是否存在。即一個查詢請求過來時,先通過布隆過濾器判斷值是否存在,存在才繼續往下查。
3.3 緩存雪崩
緩存雪崩:指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機?!本彺嫜┍家话闶怯捎诖罅繑祿瑫r過期造成的,對于這個原因,可通過均勻設置過期時間解決,即讓過期時間相對離散一點。如采用一個較大固定值+一個較小的隨機值,5小時+0到1800秒醬紫。
Redis 故障宕機也可能引起緩存雪奔。這就需要構造Redis高可用集群啦。
3.4 緩存機擊穿
緩存擊穿:指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的并發請求過來,從而大量的請求打到db?!本彺鎿舸┛粗悬c像緩存雪崩,其實它兩區別是,緩存雪奔是指數據庫壓力過大甚至down機,緩存擊穿只是大量并發請求到了DB數據庫層面??梢哉J為擊穿是緩存雪奔的一個子集吧。有些文章認為它倆區別,是在于擊穿針對某一熱點key緩存,雪奔則是很多key。
解決方案就有兩種:
使用互斥鎖方案。緩存失效時,不是立即去加載db數據,而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時候,再去加載db數據庫數據和設置緩存。否則就去重試獲取緩存。
“永不過期”,是指沒有設置過期時間,但是熱點數據快要過期時,異步線程去更新和設置過期時間。
3.5 緩存熱Key
在Redis中,我們把訪問頻率高的key,稱為熱點key。如果某一熱點key的請求到服務器主機時,由于請求量特別大,可能會導致主機資源不足,甚至宕機,從而影響正常的服務。
如何解決熱key問題?
Redis集群擴容:增加分片副本,均衡讀流量;
對熱key進行hash散列,比如將一個key備份為key1,key2……keyN,同樣的數據N個備份,N個備份分布到不同分片,訪問時可隨機訪問N個備份中的一個,進一步分擔讀流量;
使用二級緩存,即JVM本地緩存,減少Redis的讀請求。
3.6 緩存容量內存考慮
3.6.1 評估容量,合理利用
如果我們使用的是Redis,而Redis的內存是比較昂貴的,我們不要什么數據都往Redis里面塞,一般Redis只緩存查詢比較頻繁的數據。同時,我們要合理評估Redis的容量,也避免頻繁set覆蓋,導致設置了過期時間的key失效。
如果我們使用的是本地緩存,如guava的本地緩存,也要評估下容量。避免容量不夠。
3.6.2 Redis的八種內存淘汰機制
為了避免Redis內存不夠用,Redis用8種內存淘汰策略保護自己~
volatile-lru:當內存不足以容納新寫入數據時,從設置了過期時間的key中使用LRU(最近最少使用)算法進行淘汰;
allkeys-lru:當內存不足以容納新寫入數據時,從所有key中使用LRU(最近最少使用)算法進行淘汰。
volatile-lfu:4.0版本新增,當內存不足以容納新寫入數據時,在過期的key中,使用LFU算法進行刪除key。
allkeys-lfu:4.0版本新增,當內存不足以容納新寫入數據時,從所有key中使用LFU算法進行淘汰;
volatile-random:當內存不足以容納新寫入數據時,從設置了過期時間的key中,隨機淘汰數據;。
allkeys-random:當內存不足以容納新寫入數據時,從所有key中隨機淘汰數據。
volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的key中,根據過期時間進行淘汰,越早過期的優先被淘汰;
noeviction:默認策略,當內存不足以容納新寫入數據時,新寫入操作會報錯。
”3.6.3 不同的業務場景,Redis選擇適合的數據結構
排行榜適合用zset
緩存用戶信息一般用hash
消息隊列,文章列表適用用list
用戶標簽、社交需求一般用set
計數器、分布式鎖等一般用String類型
3.7 Redis一些有坑的命令
不能使用 keys指令
慎用O(n)復雜度命令,如hgetall等
慎用Redis的monitor命令
禁止使用flushall、flushdb
注意使用del命令
編輯:jq
-
BUG
+關注
關注
0文章
155瀏覽量
15653
原文標題:聊聊日常開發中,如何減少 bug 呢?
文章出處:【微信號:gh_6a53af9e8109,微信公眾號:上海磐啟微電子有限公司】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論