1. 
          

          1. 新聞動(dòng)態(tài)

            日常開(kāi)發(fā)中,如何減少bug?

            常見(jiàn)問(wèn)題 發(fā)布者:ou3377 2021-12-10 08:40 訪(fǎng)問(wèn)量:122

            本文將從數據庫、代碼層面、緩存使用篇3個(gè)大方向,總結出一共50多個(gè)注意點(diǎn),助大家成為開(kāi)發(fā)質(zhì)量之星。

            圖片

            1. 數據庫篇

            圖片慢查詢(xún)

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

            1.1 慢查詢(xún)

            圖片

            1.1.1 是否命中索引

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

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

            • 查詢(xún)條件包含or,可能導致索引失效
            • 如何字段類(lèi)型是字符串,where時(shí)一定用引號括起來(lái),否則索引失效
            • like通配符可能導致索引失效。
            • 聯(lián)合索引,查詢(xún)時(shí)的條件列不是聯(lián)合索引中的第一個(gè)列,索引失效。
            • 在索引列上使用mysql的內置函數,索引失效。
            • 對索引列運算(如,+、-、*、/),索引失效。
            • 索引字段上使用(!= 或者 < >,not in)時(shí),可能會(huì )導致索引失效。
            • 索引字段上使用is null, is not null,可能導致索引失效。
            • 左連接查詢(xún)或者右連接查詢(xún)查詢(xún)關(guān)聯(lián)的字段編碼格式不一樣,可能導致索引失效。
            • mysql估計使用全表掃描要比使用索引快,則不使用索引。

            1.1.2 數據量大,考慮分庫分表

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

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

            1.1.3 不合理的SQL

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

            1.2 數據庫字段注意點(diǎn)

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

            1.2.1 字段是否會(huì )超長(cháng)

            假設你的數據庫字段是:

            `name` varchar(255) DEFAULT NOT NULL

            如果請求參數來(lái)了變量name,字段長(cháng)度是300,那插入表的時(shí)候就報錯了。所以需要校驗參數,防止字段超長(cháng)。

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

            我們設計數據庫表字段的時(shí)候,盡量把字段設置為not null。

            • 如果是整形,我們一般使用0或者-1作為默認值。
            • 如果字符串,默認空字符串

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

            1.2.3 字段缺失

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

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

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

            1.2.5 謹慎使用text、blob字段

            如果你要用一個(gè)字段存儲文件,考慮存儲文件的路徑,而不是保存整個(gè)文件下去。使用text時(shí),涉及查詢(xún)條件時(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í)現的。spring事務(wù)自己在調用動(dòng)態(tài)代理之前,已經(jīng)對非public方法過(guò)濾了,所以非public方法,事務(wù)不生效。

            1.3.2 本地方法直接調用

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

            public class TransactionTest{
              public void A(){
                //插入一條數據
                //調用方法B (本地的類(lèi)調用,事務(wù)失效了)
                B();
              }
              
              @Transactional
              public void B(){
                //插入數據
              }
            }

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

            @Transactional
            public void method(){
              try{
                //插入一條數據
                insertA();
                //更改一條數據
                updateB();
              }catch(Exception e){
                logger.error("異常被捕獲了,那你的事務(wù)就失效咯",e);
              }
            }

            1.3.4 rollbackFor屬性設置錯誤

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

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

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

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

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

            @Transactional
            public void mothed() {
                new Thread() {
                  事務(wù)操作
                }.start();
            }

            1.4 死鎖

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

            圖片

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

            1.4.1 9種情況的SQL加鎖分析

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

            • 組合一:id列是主鍵,RC隔離級別
            • 組合二:id列是二級唯一索引,RC隔離級別
            • 組合三:id列是二級非唯一索引,RC隔離級別
            • 組合四:id列上沒(méi)有索引,RC隔離級別
            • 組合五:id列是主鍵,RR隔離級別
            • 組合六:id列是二級唯一索引,RR隔離級別
            • 組合七:id列是二級非唯一索引,RR隔離級別
            • 組合八:id列上沒(méi)有索引,RR隔離級別
            • 組合九:Serializable隔離級別

            1.4.2 如何分析解決死鎖?

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

            • 模擬死鎖場(chǎng)景
            • show engine innodb status;查看死鎖日志
            • 找出死鎖SQL
            • SQL加鎖分析,這個(gè)可以去官網(wǎng)看哈
            • 分析死鎖日志(持有什么鎖,等待什么鎖)
            • 熟悉鎖模式兼容矩陣,InnoDB存儲引擎中鎖的兼容性矩陣。

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

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

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

            圖片

            1.5.1 要求強一致性,考慮讀主庫

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

            1.5.2 不要求強一致性,讀從庫

            如果是一般業(yè)務(wù),可以接受短暫的數據不一致的話(huà),優(yōu)先考慮讀從庫。因為從庫可以分擔主庫的讀寫(xiě)壓力,提高系統吞吐。

            1.6 新老數據兼容

            1.6.1 新加的字段,考慮存量數據的默認值

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

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

            如果我們開(kāi)發(fā)中,需要沿用數據庫表的老字段,并且有存量數據,那就需要考慮老存量數據庫的值是否有坑。比如我們表有個(gè)user_role_code 的字段,老的數據中,它枚舉值是 01:超級管理員 02:管理員 03:一般用戶(hù)。假設業(yè)務(wù)需求是一般用戶(hù)拆分為03查詢(xún)用戶(hù)和04操作用戶(hù),那我們在開(kāi)發(fā)中,就要考慮老數據值的問(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種對應的解決方案

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

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

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

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

            方案三:  利用延遲關(guān)聯(lián)或者子查詢(xú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 修改、查詢(xún)數據量多時(shí),考慮分批進(jìn)行。

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

            正例:

            remoteBatchQuery(param);

            反例:


            for(int i=0;i<100000;i++){
              remoteSingleQuery(param)
            }

            2. 代碼層面篇

            圖片代碼層面

            2.1 編碼細節

            圖片

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

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

            • 包裝類(lèi)型的空指針問(wèn)題
            • 級聯(lián)調用的空指針問(wèn)題
            • Equals方法左邊的空指針問(wèn)題
            • ConcurrentHashMap 類(lèi)似容器不支持 k-v為 null。
            • 集合,數組直接獲取元素
            • 對象直接獲取屬性
            if(object!=null){
               String name = object.getName();
            }

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

            • 使用 Executors.newFixedThreadPool,可能會(huì )出現OOM問(wèn)題,因為它使用的是無(wú)界阻塞隊列
            • 建議使用自定義的線(xiàn)程池,最好給線(xiàn)程池一個(gè)清晰的命名,方便排查問(wèn)題
            • 不同的業(yè)務(wù),最好做線(xiàn)程池隔離,避免所有的業(yè)務(wù)公用一個(gè)線(xiàn)程池。
            • 線(xiàn)程池異常處理要考慮好

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

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

            • Hashmap、Arraylist、LinkedList、TreeMap等都是線(xiàn)性不安全的;
            • Vector、Hashtable、ConcurrentHashMap等都是線(xiàn)性安全的

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

            日常開(kāi)發(fā),經(jīng)常需要對日期格式化,但是呢,年份設置為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 轉 YYYY-MM-dd 格式后 " + dtf.format(testDate));

            運行結果:

            2019-12-31 轉 YYYY-MM-dd 格式后 2020-12-31

            還有金額計算也比較常見(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");
                    }
                }
            }

            運行結果:

            0.30000000000000004
            0.19999999999999996
            401.49999999999994
            1.2329999999999999

            2.1.5 大文件處理

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

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

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

            /*
             * 關(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()打印,可能導致字符串常量池內存空間占滿(mǎn)
            • catch了異常,使用log把它打印出來(lái)
            • 不要用一個(gè)Exception捕捉所有可能的異常
            • 不要把捕獲異常當做業(yè)務(wù)邏輯來(lái)處理

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

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

            if(selectIsAvailable(ticketId){ 
                1、deleteTicketById(ticketId) 
                2、給現金增加操作 
            }else
                return “沒(méi)有可用現金券” 
            }

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

            if(deleteAvailableTicketById(ticketId) == 1){ 
                1、給現金增加操作 
            }else
                return “沒(méi)有可用現金券” 
            }

            2.2 提供對外接口

            圖片

            2.2.1 校驗參數合法性

            我們提供對外的接口,不管是提供給客戶(hù)端、還是前端,又或是別的系統調用,都需要校驗一下入參的合法性。

            如果你的數據庫字段設置為varchar(16),對方傳了一個(gè)32位的字符串過(guò)來(lái),你不校驗參數長(cháng)度,插入數據庫直接異常了。

            圖片

            2.2.2 新老接口兼容

            很多bug都是因為修改了對外老接口,但是卻不做兼容導致的。關(guān)鍵這個(gè)問(wèn)題多數是比較嚴重的,可能直接導致系統發(fā)版失敗的。新手程序員很容易犯這個(gè)錯誤哦~

            比如我們有個(gè)dubbo的分布式接口,本次你修改了入參,就需要考慮新老接口兼容。原本是只接收A,B參數,現在你加了一個(gè)參數C,就可以考慮這樣處理。

            //老接口
            void oldService(A,B){
              //兼容新接口,傳個(gè)null代替C
              newService(A,B,null);
            }

            //新接口,暫時(shí)不能刪掉老接口,需要做兼容。
            void newService(A,B,C);

            2.2.3 限流,防止大流量壓垮系統

            如果瞬間的大流量請求過(guò)來(lái),容易壓垮系統。所以為了保護我們的系統,一般要做限流處理??梢允褂?strong style=";padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;color: rgb(14, 136, 235)">guava ratelimiter 組件做限流,也可以用阿里開(kāi)源的Sentinel

            2.2.4 接口安全性,加簽驗簽,鑒權

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

            2.2.5 考慮接口冪等性

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

            1. 冪等(idempotent、idempotence)是一個(gè)數學(xué)與計算機學(xué)概念,常見(jiàn)于抽象代數中。
            2. 在編程中.一個(gè)冪等操作的特點(diǎn)是其任意多次執行所產(chǎn)生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。

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

            1. 查詢(xún)操作
            2. 唯一索引
            3. token機制,防止重復提交
            4. 數據庫的delete刪除操作
            5. 樂(lè )觀(guān)鎖
            6. 悲觀(guān)鎖
            7. Redis、zookeeper 分布式鎖(以前搶紅包需求,用了Redis分布式鎖)
            8. 狀態(tài)機冪等

            圖片

            2.3 調用第三方接口

            圖片

            2.3.1 超時(shí)處理

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

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

            2.3.2 考慮重試機制

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

            2.3.3 考慮是否降級處理

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

            圖片

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

            2.3.4 考慮是否異步處理

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

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

            圖片

            2.3.5 調接口異常處理

            如果我們調用一個(gè)遠程接口,一般需要思考以下:如果別人接口異常,我們要怎么處理,怎么兜底,是重試還是當做失???怎么保證數據的最終一致性等等。

            3. 緩存篇

            圖片

            3.1 數據庫與緩存一致性

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

            3.1.1 幾種緩存使用模式

            • Cache-Aside Pattern,旁路緩存模式
            • Read-Through/Write-Through(讀寫(xiě)穿透)
            • Write- behind (異步緩存寫(xiě)入)

            一般我們使用緩存,都是旁路緩存模式,讀請求流程如下:

            圖片

            • 讀的時(shí)候,先讀緩存,緩存命中的話(huà),直接返回數據
            • 緩存沒(méi)有命中的話(huà),就去讀數據庫,從數據庫取出數據,放入緩存后,同時(shí)返回響應。

            旁路緩存模式的寫(xiě)流程:圖片

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

            我們在操作緩存的時(shí)候,到底應該刪除緩存還是更新緩存呢?我們先來(lái)看個(gè)例子:

            圖片

            1. 線(xiàn)程A先發(fā)起一個(gè)寫(xiě)操作,第一步先更新數據庫
            2. 線(xiàn)程B再發(fā)起一個(gè)寫(xiě)操作,第二步更新了數據庫
            3. 由于網(wǎng)絡(luò )等原因,線(xiàn)程B先更新了緩存
            4. 線(xiàn)程A更新緩存。

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

            3.1.3 先操作數據庫還是先操作緩存

            雙寫(xiě)的情況下,先操作數據庫還是先操作緩存?我們再來(lái)看一個(gè)例子:假設有A、B兩個(gè)請求,請求A做更新操作,請求B做查詢(xún)讀取操作。

            圖片image.png

            1. 線(xiàn)程A發(fā)起一個(gè)寫(xiě)操作,第一步del cache
            2. 此時(shí)線(xiàn)程B發(fā)起一個(gè)讀操作,cache miss
            3. 線(xiàn)程B繼續讀DB,讀出來(lái)一個(gè)老數據
            4. 然后線(xiàn)程B把老數據設置入cache
            5. 線(xiàn)程A寫(xiě)入DB最新的數據

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

            3.1.4 如何保證最終一致性

            • 緩存延時(shí)雙刪
            • 刪除緩存重試機制
            • 讀取biglog異步刪除緩存

            3.2 緩存穿透

            緩存穿透:指查詢(xún)一個(gè)一定不存在的數據,由于緩存不命中時(shí),需要從數據庫查詢(xún),查不到數據則不寫(xiě)入緩存,這將導致這個(gè)不存在的數據每次請求都要到數據庫去查詢(xún),進(jìn)而給數據庫帶來(lái)壓力。

            緩存穿透一般都是這幾種情況產(chǎn)生的:業(yè)務(wù)不合理的設計、業(yè)務(wù)/運維/開(kāi)發(fā)失誤的操作、黑客非法請求攻擊。如何避免緩存穿透呢?一般有三種方法。

            • 如果是非法請求,我們在A(yíng)PI入口,對參數進(jìn)行校驗,過(guò)濾非法值。
            • 如果查詢(xún)數據庫為空,我們可以給緩存設置個(gè)空值,或者默認值。但是如有有寫(xiě)請求進(jìn)來(lái)的話(huà),需要更新緩存哈,以保證緩存一致性,同時(shí),最后給緩存設置適當的過(guò)期時(shí)間。(業(yè)務(wù)上比較常用,簡(jiǎn)單有效)
            • 使用布隆過(guò)濾器快速判斷數據是否存在。即一個(gè)查詢(xún)請求過(guò)來(lái)時(shí),先通過(guò)布隆過(guò)濾器判斷值是否存在,存在才繼續往下查。

            3.3 緩存雪崩

            緩存雪崩:指緩存中數據大批量到過(guò)期時(shí)間,而查詢(xún)數據量巨大,引起數據庫壓力過(guò)大甚至down機。

            • 緩存雪奔一般是由于大量數據同時(shí)過(guò)期造成的,對于這個(gè)原因,可通過(guò)均勻設置過(guò)期時(shí)間解決,即讓過(guò)期時(shí)間相對離散一點(diǎn)。如采用一個(gè)較大固定值+一個(gè)較小的隨機值,5小時(shí)+0到1800秒醬紫。
            • Redis 故障宕機也可能引起緩存雪奔。這就需要構造Redis高可用集群啦。

            3.4  緩存機擊穿

            緩存擊穿:指熱點(diǎn)key在某個(gè)時(shí)間點(diǎn)過(guò)期的時(shí)候,而恰好在這個(gè)時(shí)間點(diǎn)對這個(gè)Key有大量的并發(fā)請求過(guò)來(lái),從而大量的請求打到db。

            緩存擊穿看著(zhù)有點(diǎn)像緩存雪崩,其實(shí)它兩區別是,緩存雪奔是指數據庫壓力過(guò)大甚至down機,緩存擊穿只是大量并發(fā)請求到了DB數據庫層面??梢哉J為擊穿是緩存雪奔的一個(gè)子集吧。有些文章認為它倆區別,是在于擊穿針對某一熱點(diǎn)key緩存,雪奔則是很多key。

            解決方案就有兩種:

            1. 使用互斥鎖方案。緩存失效時(shí),不是立即去加載db數據,而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時(shí)候,再去加載db數據庫數據和設置緩存。否則就去重試獲取緩存。
            2. “永不過(guò)期”,是指沒(méi)有設置過(guò)期時(shí)間,但是熱點(diǎn)數據快要過(guò)期時(shí),異步線(xiàn)程去更新和設置過(guò)期時(shí)間。

            3.5 緩存熱Key

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

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

            • Redis集群擴容:增加分片副本,均衡讀流量;
            • 對熱key進(jìn)行hash散列,比如將一個(gè)key備份為key1,key2……keyN,同樣的數據N個(gè)備份,N個(gè)備份分布到不同分片,訪(fǎng)問(wèn)時(shí)可隨機訪(fǎng)問(wèn)N個(gè)備份中的一個(gè),進(jìn)一步分擔讀流量;
            • 使用二級緩存,即JVM本地緩存,減少Redis的讀請求。

            3.6 緩存容量?jì)却婵紤]

            3.6.1 評估容量,合理利用

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

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

            3.6.2 Redis的八種內存淘汰機制

            為了避免Redis內存不夠用,Redis用8種內存淘汰策略保護自己~

            • volatile-lru:當內存不足以容納新寫(xiě)入數據時(shí),從設置了過(guò)期時(shí)間的key中使用LRU(最近最少使用)算法進(jìn)行淘汰;
            • allkeys-lru:當內存不足以容納新寫(xiě)入數據時(shí),從所有key中使用LRU(最近最少使用)算法進(jìn)行淘汰。
            • volatile-lfu:4.0版本新增,當內存不足以容納新寫(xiě)入數據時(shí),在過(guò)期的key中,使用LFU算法進(jìn)行刪除key。
            • allkeys-lfu:4.0版本新增,當內存不足以容納新寫(xiě)入數據時(shí),從所有key中使用LFU算法進(jìn)行淘汰;
            • volatile-random:當內存不足以容納新寫(xiě)入數據時(shí),從設置了過(guò)期時(shí)間的key中,隨機淘汰數據;。
            • allkeys-random:當內存不足以容納新寫(xiě)入數據時(shí),從所有key中隨機淘汰數據。
            • volatile-ttl:當內存不足以容納新寫(xiě)入數據時(shí),在設置了過(guò)期時(shí)間的key中,根據過(guò)期時(shí)間進(jìn)行淘汰,越早過(guò)期的優(yōu)先被淘汰;
            • noeviction:默認策略,當內存不足以容納新寫(xiě)入數據時(shí),新寫(xiě)入操作會(huì )報錯。

            3.6.3 不同的業(yè)務(wù)場(chǎng)景,Redis選擇適合的數據結構

            • 排行榜適合用zset
            • 緩存用戶(hù)信息一般用hash
            • 消息隊列,文章列表適用用list
            • 用戶(hù)標簽、社交需求一般用set
            • 計數器、分布式鎖等一般用String類(lèi)型

            3.7 Redis一些有坑的命令

            1. 不能使用 keys指令
            2. 慎用O(n)復雜度命令,如hgetall等
            3. 慎用Redis的monitor命令
            4. 禁止使用flushall、flushdb
            5. 注意使用del命令

            文章源自撿田螺的小男孩


            關(guān)鍵字: BUG 數據庫 代碼

            文章連接: http://www.gostscript.com/cjwt/795.html

            版權聲明:文章由 晨展科技 整理收集,來(lái)源于互聯(lián)網(wǎng)或者用戶(hù)投稿,如有侵權,請聯(lián)系我們,我們會(huì )立即刪除。如轉載請保留

            双腿国产亚洲精品无码不卡|国产91精品无码麻豆|97久久久久久久极品|无码人妻少妇久久中文字幕
                1.