Redis系列--內存淘汰機制(含單機版內存優化建議)


https://blog.csdn.net/Jack__Frost/article/details/72478400?locationNum=13&fps=1

每台redis的服務器的內存都是有限的,而且也不是所有的內存都用來存儲信息。而且redis的實現並沒有在內存這塊做太多的優化,所以實現者為了防止內存過於飽和,采取了一些措施來管控內存。

文章結構:(1)內存策略;(2)內存釋放機制原理;(3)項目中如何合理應用淘汰策略;(4)單機版Redis內存優化注意點。

一、內存策略:先來吃份官方文檔

最大內存的設置是通過設置maxmemory來完成的,格式為maxmemory bytes ,當目前使用的內存超過了設置的最大內存,就要進行內存釋放了, 當需要進行內存釋放的時候,需要用某種策略對保存的的對象進行刪除。Redis有六種策略(默認的策略是volatile-lru。)

 redis中當內存超過限制時,按照配置的策略,淘汰掉相應的key-value,使得內存可以繼續留有足夠的空間保存新的數據。redis 確定驅逐某個鍵值對后,會刪除這個數據並,並將這個數據變更消息發布到本地(AOF 持久化)和從機(主從連接)。

(1)volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用的數據淘汰。

(2)volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數據淘汰

(3)volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據淘汰

(4)allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰

(5)allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰

(6)no-enviction:禁止淘汰數據

除此之外還有一個配置項,就是maxmemory-samples,默認值是3,因為上面的策略代碼實現的都是近似算法,所以不管是lru算法,還是ttl,都並不是在數據庫中所有的數據為基礎的算法,因為當數據庫的數據很多的時候,這樣效率太低,所以代碼中都是基於maxmemory-samples個數據的近似算法。詳情請讀下文。

置換策略是如何工作的:

1)客戶端執行一條新命令,導致數據庫需要增加數據(比如set key value)

2)Redis會檢查內存使用,如果內存使用超過maxmemory,就會按照置換策略刪除一些key

3)新的命令執行成功

注意:

如果我們持續的寫數據會導致內存達到或超出上限maxmemory,但是置換策略會將內存使用降低到上限以下。

如果一次需要使用很多的內存(比如一次寫入一個很大的set),那么,Redis的內存使用可能超出最大內存限制一段時間。

二、內存釋放機制原理:

(1)概述:

當mem_used內存已經超過maxmemory的設定,對於所有的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的內存。注意這個清理過程是阻塞的,直到清理出足夠的內存空間。所以如果在達到maxmemory並且調用方還在不斷寫入的情況下,可能會反復觸發主動清理策略,導致請求會有一定的延遲。

清理時會根據用戶配置的maxmemory-policy來做適當的清理(一般是LRU或TTL),這里的LRU或TTL策略並不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。

maxmemory-samples在redis-3.0.0中的默認配置為5,如果增加,會提高LRU或TTL的精准度,redis作者測試的結果是當這個配置為10時已經非常接近全量LRU的精准度了,並且增加maxmemory-samples會導致在主動清理時消耗更多的CPU時間,有如下建議:

1)盡量不要觸發maxmemory,最好在mem_used內存占用達到maxmemory的一定比例后,需要考慮調大hz以加快淘汰,或者進行集群擴容。

2)如果能夠控制住內存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為LRU cache服務(這種服務一般長時間處於maxmemory狀態,由Redis自動做LRU淘汰),可以適當調大maxmemory-samples。

(2)內存管理源碼解析:參考博文

Redis釋放內存是由函數freeMemoryIfNeeded完成的,redis用processCommand函數處理每條命令,函數中在真正處理命令之前都會調用freeMemoryIfNeeded函數,這個函數會判斷當前使用的內存是否超過了最大使用內存,如果超過,就會根據內存釋放策略釋放內存。

freeMemoryIfNeeded函數首先會計算出當前使用了多少內存,注意,這里並不會包括slaves 輸出緩存以及AOF緩存,源碼如下:

int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);

    /* Remove the size of slaves output buffers and AOF buffer from the
     * count of used memory. 
     */ 
     //計算占用內存大小時,並不計算slave output buffer和aof buffer,因此maxmemory應該比實際內存小,為這兩個buffer留足空間。
    mem_used = zmalloc_used_memory();
    if (slaves) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = listNodeValue(ln);
            unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
            if (obuf_bytes > mem_used)
                mem_used = 0;
            else
                mem_used -= obuf_bytes;
        }
    }
    if (server.appendonly) {
        mem_used -= sdslen(server.aofbuf);
        mem_used -= sdslen(server.bgrewritebuf);
    }
//判斷已經使用內存是否超過最大使用內存,如果沒有超過就返回REDIS_OK,
    /* Check if we are over the memory limit. */
    if (mem_used <= server.maxmemory) return REDIS_OK;
//當超過了最大使用內存時,就要判斷此時redis到底采用的是那種內存釋放策略,根據不同的策略,采取不同的手段。
//(1)首先判斷是否是為no-enviction策略,如果是,則返回REDIS_ERR,然后redis就不再接受任何寫命令了。
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; /* We need to free memory, but policy forbids. */

    /* Compute how much memory we need to free. */
    mem_tofree = mem_used - server.maxmemory;
    mem_freed = 0;
    //(2)接下來就判斷淘汰策略是基於所有的鍵還是只是基於設置了過期時間的鍵,如果是針對所有的鍵,就從server.db[j].dict中取數據,如果是針對設置了過期時間的鍵,就從server.db[j].expires中取數據。
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;

        for (j = 0; j < server.dbnum; j++) {
            long bestval = 0; /* just to prevent warning */
            sds bestkey = NULL;
            struct dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;
    //(3)然后判斷是不是random策略,包括volatile-random 和allkeys-random,這兩種策略是最簡單的,就是在上面的數據集中隨便去一個鍵,然后刪掉。
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                dict = server.db[j].dict;
            } else {
                dict = server.db[j].expires;
            }
            if (dictSize(dict) == 0) continue;
//接着又判斷allkeys-random還是volatile-ttl策略
            /* volatile-random and allkeys-random policy */
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);
                bestkey = dictGetEntryKey(de);
            }//如果是random delete,則從dict中隨機選一個key
//然后就是判斷是lru策略還是ttl策略,如果是lru策略就采用lru近似算法
            /* volatile-lru and allkeys-lru policy */
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;
                    robj *o;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetEntryKey(de);
                    /* When policy is volatile-lru we need an additonal lookup
                     * to locate the real key, as dict is set to db->expires. */
                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                        de = dictFind(db->dict, thiskey); //因為dict->expires維護的數據結構里並沒有記錄該key的最后訪問時間
                    o = dictGetEntryVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* Higher idle time is better candidate for deletion */
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }//為了減少運算量,redis的lru算法和expire淘汰算法一樣,都是非最優解,lru算法是在相應的dict中,選擇maxmemory_samples(默認設置是3)份key,挑選其中lru的,進行淘汰
            }
//如果是ttl策略。ttl策略很簡單,就是取maxmemory_samples個鍵,然后比較他們的過期時間,然后從這些鍵中找到最快過期的那個鍵,就是我們將要刪除的鍵。
            /* volatile-ttl */
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetEntryKey(de);
                    thisval = (long) dictGetEntryVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better
                     * candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }//注意ttl實現和上邊一樣,都是挑選出maxmemory_samples份進行挑選
            }
//根據不同的策略,我們找到了將要刪除的鍵,下面就是將他們刪除的時候了,刪除選定的鍵值對
            /* Finally remove the selected key. */
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                // 發布數據更新消息,主要是AOF 持久化和從機
                propagateExpire(db,keyobj); //將del命令擴散給slaves

    // 注意, propagateExpire() 可能會導致內存的分配,
    // propagateExpire() 提前執行就是因為redis 只計算
    // dbDelete() 釋放的內存大小。倘若同時計算dbDelete()
    // 釋放的內存和propagateExpire() 分配空間的大小,與此
    // 同時假設分配空間大於釋放空間,就有可能永遠退不出這個循環。
    // 下面的代碼會同時計算dbDelete() 釋放的內存和propagateExpire() 分配空間的大小
                /* We compute the amount of memory freed by dbDelete() alone.
                 * It is possible that actually the memory needed to propagate
                 * the DEL in AOF and replication link is greater than the one
                 * we are freeing removing the key, but we can't account for
                 * that otherwise we would never exit the loop.
                 *
                 * AOF and Output buffer memory will be freed eventually so
                 * we only care about memory used by the key space. */
              // 只計算dbDelete() 釋放內存的大小
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                server.stat_evictedkeys++;
                decrRefCount(keyobj);
                keys_freed++;

                /* When the memory to free starts to be big enough, we may
                 * start spending so much time here that is impossible to
                 * deliver data to the slaves fast enough, so we force the
                 * transmission here inside the loop. */
                 // 將從機回復空間中的數據及時發送給從機
                if (slaves) flushSlavesOutputBuffers();
            }
        }//在所有的db中遍歷一遍,然后判斷刪除的key釋放的空間是否足夠,未能釋放空間,且此時redis 使用的內存大小依舊超額,失敗返回
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}

此函數是在執行特定命令之前進行調用的,並且在當前占用內存低於限制后即返回OK。因此可能在后續執行命令后,redis占用的內存就超過了maxmemory的限制。因此,maxmemory是redis執行命令所需保證的最大內存占用,而非redis實際的最大內存占用。(在不考慮slave buffer和aof buffer的前提下)。

TTL 數據淘汰機制 :

redis 數據集數據結構中保存了鍵值對過期時間的表,即 redisDb.expires。

定義:

從過期時間的表中隨機挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣你會發現,
redis 並不是保證取得所有過期時間的表中最快過期的鍵值對,而只是隨機挑選的幾個鍵值對中的。

freeMemoryIfNeeded函數關於TTL的源碼:

//挑選將要過期的數據
else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
 // server.maxmemory_samples 為隨機挑選鍵值對次數
    // 隨機挑選server.maxmemory_samples 個鍵值對,驅逐最快要過期的數據
    for (k = 0; k < server.maxmemory_samples; k++) {
        sds thiskey;
        long thisval;

        de = dictGetRandomKey(dict);
        thiskey = dictGetKey(de);
        thisval = (long) dictGetVal(de);

        /* Expire sooner (minor expire unix timestamp) is better
         * candidate for deletion */
        if (bestkey == NULL || thisval < bestval) {
            bestkey = thiskey;
            bestval = thisval;
        }
    }
}

LRU 數據淘汰機制 :

在服務器配置中保存了 lru 計數器 server.lrulock,會定時(redis 定時程序 serverCorn())更新,server.lrulock 的值是根據 server.unixtime 計算出來的。另外,從 struct redisObject 中可以發現,每一個 redis 對象都會設置相應的 lru。可以想象的是,每一次訪問數據的時候,會更新 redisObject.lru。

LRU 數據淘汰機制定義: 

在數據集中隨機挑選幾個鍵值對,取出其中 lru 最小的鍵值對淘汰。所以,你會發現,redis 
並不是保證取得所有數據集中最近最少使用(LRU)的鍵值對,而只是隨機挑選的幾個鍵值對中的鍵值對。

freeMemoryIfNeeded函數關於LRU的源碼:

//不同的策略,操作的數據集不同
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
    server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{
    dict = server.db[j].dict;
} else {//操作的是設置了過期時間的key集
    dict = server.db[j].expires;
}
if (dictSize(dict) == 0) continue;

/* volatile-random and allkeys-random policy */
//隨機選擇進行淘汰
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
{
    de = dictGetRandomKey(dict);
    bestkey = dictGetKey(de);
}

/* volatile-lru and allkeys-lru policy */
//具體的LRU算法
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
    struct evictionPoolEntry *pool = db->eviction_pool;

    while(bestkey == NULL) {
        //選擇隨機樣式,並從樣本中作用LRU算法選擇需要淘汰的數據
        evictionPoolPopulate(dict, db->dict, db->eviction_pool);
        /* Go backward from best to worst element to evict. */
        for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
            if (pool[k].key == NULL) continue;
            de = dictFind(dict,pool[k].key);
            sdsfree(pool[k].key);
            //將pool+k+1之后的元素向前平移一個單位
            memmove(pool+k,pool+k+1,
                sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
            /* Clear the element on the right which is empty
             * since we shifted one position to the left.  */
            pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
            pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;
            //選擇了需要淘汰的數據
            if (de) {
                bestkey = dictGetKey(de);
                break;
            } else {
                /* Ghost... */
                continue;
            }
        }
    }
}

LRU算法實現在evictionPoolPopulate方法內.(有興趣的朋友再進行相關查閱了解吧)


三、項目中如何合理應用淘汰策略:

此部分部分轉載於此博主此博客

(1)合理設置maxmemory:

修改方式有兩種:

1. 通過CONFIG SET 設定:

127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> CONFIG SET maxmemory 80MB
OK
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "83886080"

2.修改配置文件redis.conf:配置文件講解

maxmemory 80mb

注意:在64bit系統下,maxmemory設置為0表示不限制Redis內存使用,在32bit系統下,maxmemory隱式不能超過3GB。

當Redis內存使用達到指定的限制時,就需要選擇一個置換的策略。

(2)置換策略的選擇:

當Redis內存使用達到maxmemory時,就會使用設置好的maxmemory-policy進行對老數據的置換。

設置maxmemory-policy的方法和設置maxmemory方法類似,通過redis.conf或是通過CONFIG SET動態修改。

如果沒有匹配到可以刪除的key,那么volatile-lru、volatile-random和volatile-ttl策略和noeviction替換策略一樣——不對任何key進行置換。

選擇合適的置換策略是很重要的,這主要取決於你的應用的訪問模式,當然你也可以動態的修改置換策略,並通過用Redis命令——INFO去輸出cache的命中率情況,進而可以對置換策略進行調優。

針對一些策略所使用的場景:

1)allkeys-lru:如果我們的應用對緩存的訪問符合冪律分布(也就是存在相對熱點數據),或者我們不太清楚我們應用的緩存訪問分布狀況,我們可以選擇allkeys-lru策略。

在所有的key都是最近最經常使用,那么就需要選擇allkeys-lru進行置換最近最不經常使用的key,如果你不確定使用哪種策略。

設置是失效時間expire會占用一些內存,而采用allkeys-lru就沒有必要設置失效時間,進而更有效的利用內存

2)allkeys-random:如果我們的應用對於緩存key的訪問概率相等,則可以使用這個策略。

如果所有的key的訪問概率都是差不多的,那么可以選用allkeys-random策略去置換數據。

3)volatile-ttl:這種策略使得我們可以向Redis提示哪些key更適合被eviction。

如果對數據有足夠的了解,能夠為key指定hint(通過expire/ttl指定),那么可以選擇volatile-ttl進行置換

4)volatile-lru策略和volatile-random策略適合我們將一個Redis實例既應用於緩存和又應用於持久化存儲的時候,然而我們也可以通過使用兩個Redis實例來達到相同的效果,值得一提的是將key設置過期時間實際上會消耗更多的內存,因此我們建議使用allkeys-lru策略從而更有效率的使用內存。


四、Redis內存優化注意點:(先針對單機版)

(1)Redis的編碼:此部分參考此博主此博客

概述:

很多數據類型都可以通過特殊編碼的方式來進行存儲空間的優化。其中,Hash、List和由Integer組成的Sets都可以通過該方式來優化存儲結構,以便占用更少的空間,在有些情況下,可以省去9/10的空間。

這些特殊編碼對於Redis的使用而言是完全透明的,事實上,它只是CPU和內存之間的一個交易而言。如果內存使用率方面高一些,那么在操作數據時消耗的CPU自然要多一些,反之亦然。在Redis中提供了一組配置參數用於設置與特殊編碼相關的各種閾值。

閾值詳細設置:

    #如果Hash中字段的數量小於該參數值,Redis將對該Key的Hash Value采用特殊編碼。
    hash-max-zipmap-entries 64
    #如果Hash中各個字段的最大長度不超過512字節,Redis也將對該Key的Hash Value采用特殊編碼方式。
    hash-max-zipmap-value 512
    #下面兩個參數的含義基本等同於上面兩個和Hash相關的參數,只是作用的對象類型為List。
    list-max-ziplist-entries 512
    list-max-ziplist-value 64
    #如果set中整型元素的數量不超過512時,Redis將會采用該特殊編碼。
    set-max-intset-entries 512
    #如果zset中的元素數量小於128或者各字段長度不超過64,redis會對zset采用特殊編碼 
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64

倘若某個已經被編碼的值再經過修改之后超過了配置信息中的最大限制,那么Redis會自動將其轉換為正常編碼格式,這一操作是非常快速的,但是如果反過來操作,將一個正常編碼的較大值轉換為特殊編碼,Redis的建議是,在正式做之前最好先簡單測試一下轉換效率,因為這樣的轉換往往是非常低效的。

(2)使用bit位級別操作和byte字節級別操作來減少不必要的內存使用:

bit位級別操作:GETRANGE, SETRANGE, GETBIT and SETBIT

byte字節級別操作:GETRANGE and SETRANGE

Redis提供了GETRANGE/SETRANGE/GETBIT/SETBIT四個用於字符串類型Key/Value的命令。通過這些命令,我們便可以像操作數組那樣來訪問String類型的值數據了。比如唯一標識用戶身份的ID,可能僅僅是String值的其中一段子字符串。這樣就可以通過GETRANGE/SETRANGE命令來方便的提取。再有就是可以使用BITMAP來表示用戶的性別信息,如1表示male,0表示female。用這種方式來表示100,000,000個用戶的性別信息時,也僅僅占用12MB的存儲空間,與此同時,在通過SETBIT/GETBIT命令進行數據遍歷也是非常高效的。

(3)盡可能地使用hashes哈希,因為小Hashes會被編碼成一個非常小的空間。

由於小的Hash類型數據占用的空間相對較少,因此我們在實際應用時應該盡可能的考慮使用Hash類型,比如用戶的注冊信息,這其中包括姓名、性別、email、年齡和口令等字段。我們當然可以將這些信息以Key的形式進行存儲,而用戶填寫的信息則以String Value的形式存儲。然而Redis則更為推薦以Hash的形式存儲,以上信息則以Field/Value的形式表示。

現在我們就通過學習Redis的存儲機制來進一步證明這一說法。在該篇博客的開始處已經提到了特殊編碼機制,其中有兩個和Hash類型相關的配置參數:hash-max-zipmap-entries和hash-max-zipmap-value。至於它們的作用范圍前面已經給出,這里就不再過多的贅述了。現在我們先假設存儲在Hash Value中的字段數量小於hash-max-zipmap-entries,而每個元素的長度又同時小於hash-max-zipmap-value。這樣每當有新的Hash類型的Key/Value存儲時,Redis都會為Hash Value創建定長的空間,最大可預分配的字節數為:

 total_bytes = hash-max-zipmap-entries * hash-max-zipmap-value 
  • 1
  • 2

這樣一來,Hash中所有字段的位置已經預留,並且可以像訪問數組那樣隨機的訪問Field/Value,他們之間的步長間隔為hash-max-zipmap-value。只有當Hash Value中的字段數量或某一新元素的長度分別超過以上兩個參數值時,Redis才會考慮將他們以Hash Table的方式進行重新存儲,否則將始終保持這種高效的存儲和訪問方式。不僅如此,由於每個Key都要存儲一些關聯的系統信息,如過期時間、LRU等,因此和String類型的Key/Value相比,Hash類型極大的減少了Key的數量(大部分的Key都以Hash字段的形式表示並存儲了),從而進一步優化了存儲空間的使用效率。

(4)過期策略的合理設計:

請參考此博客點此處

(5)合理的內存分配:

如果maxmemory沒有設置的Redis會繼續分配內存,因為它認為合適的,因此它可以(逐漸)吃了你的全部可用內存。因此,通常建議配置一些限制。您可能還需要設置maxmemory策略,默認的是:noeviction(這不是在一些舊版本的Redis的默認值)。

這使得Redis的返回內存不足的錯誤寫命令,如果當它到達了極限 - 這反過來可能會導致應用程序錯誤,但不會導致因為內存飢餓而整機死亡。

(6)在存到Redis之前先把你的數據壓縮下。

(7)一些真正的實際Redis內存設計方案會在本系列的后面寫出。比如:共享對象,【string】–>【hash】–>【segment-hash】的優化


好了,Redis系列(四)–內存淘汰機制(含單機版內存優化建議)講完了,這是項目過程中Redis內存優化的筆記,現在羅列給大家,這是積累的必經一步,我會繼續出這個系列文章,分享經驗給大家。歡迎在下面指出錯誤,共同學習!!你的點贊是對我最好的支持!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM