<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          《Redis開(kāi)發(fā)與運(yùn)維》讀書(shū)筆記

          共 23786字,需瀏覽 48分鐘

           ·

          2021-05-07 00:32

          最近在看《Redis開(kāi)發(fā)與運(yùn)維》,個(gè)人認(rèn)為這是一本很好的 Redis 實(shí)戰(zhàn)書(shū)籍,書(shū)中有多處講解了提升 Redis 性能的小技巧,以及使用 Redis 過(guò)程中踩過(guò)的一些“坑”,這周打算把這部分提煉總結(jié)成一篇文章,在開(kāi)發(fā)過(guò)程中能夠快速查閱。

          1、使用批量操作(mset、mget)代替多個(gè)單條操作(多個(gè)set、多個(gè)get),可以顯著提高 Redis 性能。
          127.0.0.1:6379>?set?a?1
          OK
          127.0.0.1:6379>?set?b?2
          OK
          127.0.0.1:6379>?set?c?3
          OK
          127.0.0.1:6379>?set?d?4
          OK

          (1)、n 次 get 命令,如下圖所示,耗時(shí)如下:n 次 get 時(shí)間 = n 次網(wǎng)絡(luò)時(shí)間 + n 次命令時(shí)間。

          127.0.0.1:6379>?get?a
          "1"
          127.0.0.1:6379>?get?b
          "2"
          127.0.0.1:6379>?get?c
          "3"
          127.0.0.1:6379>?get?d
          "4"
          0ab385f7f8915c8a2b334952e5b4ed94.webpn 次 get 命令

          (2)、1 次 mget 命令,如下圖所示,耗時(shí)如下:1 次 mget 時(shí)間 = 1次網(wǎng)絡(luò)時(shí)間 + n次命令時(shí)間。

          127.0.0.1:6379>?mget?a?b?c?d
          1)?"1"
          2)?"2"
          3)?"3"
          4)?"4"

          9dec95257437dba9f038bf57cfd9bd16.webp(3)、總結(jié)
          對(duì)于客戶(hù)端來(lái)說(shuō),一次命令除了命令時(shí)間還有網(wǎng)絡(luò)時(shí)間,假設(shè)網(wǎng)絡(luò)時(shí)間為1毫秒,命令時(shí)間為0.1毫秒(按照每秒處理1萬(wàn)條命令算),那么執(zhí)行 1000 次 get 命令和 1 次 mget 命令的區(qū)別如下表所示,因?yàn)镽edis的處理能力已經(jīng) 足夠高,對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),網(wǎng)絡(luò)可能會(huì)成為性能的瓶頸,使用mget操作可以減少網(wǎng)絡(luò)的影響。

          操作時(shí)間
          1000次get10001+10000.1=1100毫秒=1.1秒
          1次mget11+10000.1=101毫秒=0.101秒

          需要注意的是,雖然使用mget命令可以減少網(wǎng)絡(luò)對(duì)性能的影響,但是每次批 量操作所發(fā)送的命令數(shù)不是無(wú)節(jié)制的,如果數(shù)量過(guò)多可能造成Redis阻塞或 者網(wǎng)絡(luò)擁塞。

          2、Redis設(shè)計(jì)合理的鍵名,有利于防止鍵沖突和項(xiàng)目的可維護(hù)性。

          比較推薦使用“業(yè)務(wù)名:對(duì)象名:id:[屬性]”作為鍵名,比如 MySQL 數(shù)據(jù)庫(kù)名為 vs,用戶(hù)表名為 user,那么對(duì)應(yīng)的鍵可以用"vs:user:1","vs:user:1:name"來(lái)表示

          如果鍵名比較長(zhǎng),可以在能描述鍵含義的前提下適當(dāng)減少鍵的長(zhǎng)度,從而減少由于鍵過(guò)長(zhǎng)的內(nèi)存浪費(fèi)
          例如“user:{uid}:friends:messages:{mid}”,可以變?yōu)樽優(yōu)椤皍:{uid}:fr:m:{mid}”。

          3、smembers(返回set中所有元素)、lrange(返回list所有元素)、hgetall(返回hash中所有元素),它們都是一次性返回所有元素,時(shí)間復(fù)雜度O(n),如果元素過(guò)多存在阻塞 Redis 的可能性,這時(shí)可以使用?scan 命令漸進(jìn)式分段獲取元素,避免 Redis 阻塞。
          4、對(duì)于字符串類(lèi)型鍵,執(zhí)行set命令會(huì)去掉過(guò)期時(shí)間,這個(gè)問(wèn)題很容易在開(kāi)發(fā)過(guò)程中被忽視。
          127.0.0.1:6379>?set?hello?world
          (integer)?1
          127.0.0.1:6379>?expire?hello?50
          (integer)?1
          127.0.0.1:6379>?ttl?hello?
          (integer)?48?
          127.0.0.1:6379>?set?hello?happy?//?此時(shí)?hello?這個(gè)key會(huì)失效
          OK?
          127.0.0.1:6379>?ttl?hello?
          (integer)?-1
          5、建議生產(chǎn)環(huán)境不要使用全局遍歷命令? keys ,因?yàn)槿绻?Redis 包含了大量的鍵,執(zhí)行 keys 命令很可能會(huì)造成 Redis 阻塞。
          //?遍歷所有符合給定模式?pattern?的鍵,parttern取值如下所示
          keys?pattern

          *:代表匹配任意字符。
          []:代表匹配部分字符,例如[1,3]代表匹配數(shù)字1和3,[1-10]代表匹配1到10 的任意數(shù)字。

          keys命令使用舉例:

          //?匹配以j,r開(kāi)頭,緊跟edis字符串的所有鍵
          127.0.0.1:6379>?keys?[j,r]edis
          1)?"jedis"?
          2)?"redis"

          //?匹配包含h、l、l的鍵
          127.0.0.1:6379>?keys?hll*?
          1)?"hill"?
          2)?"hello"

          我們已經(jīng)知道,在生產(chǎn)環(huán)境最好不使用keys命令,但有時(shí)候確實(shí)有遍歷鍵的需求該怎么辦,可以在以下三種情況使用:
          (1)、在一個(gè)不對(duì)外提供服務(wù)的Redis從節(jié)點(diǎn)上執(zhí)行,這樣不會(huì)阻塞到客戶(hù)端的請(qǐng)求,但是會(huì)影響到主從復(fù)制。
          (2)、如果確認(rèn)鍵值總數(shù)確實(shí)比較少,可以執(zhí)行該命令。
          (3)、使用 scan 命令漸進(jìn)式的遍歷所有鍵,可以有效防止阻塞。

          6、Redis 從2.8版本后,提供了 scan 命令,采用漸進(jìn)式遍歷的方式,解決 hgetall、keys、smembers、lrange 這些命令(時(shí)間復(fù)雜度為 O(n) )阻塞 Redis 的問(wèn)題。scan 命令的時(shí)間復(fù)雜度也是O(n),但是它是通過(guò)游標(biāo)分步進(jìn)行,不會(huì)一下子阻塞住 Redis。

          scan命令是針對(duì)list做遍歷,Redis還提供了其他數(shù)據(jù)結(jié)構(gòu)的漸進(jìn)式遍歷命令,比如hscan命令是針對(duì)hash做遍歷,sscan命令是針對(duì)set做遍歷,zscan命令是針對(duì)zset做遍歷。

          在 scan 的過(guò)程中如果有鍵發(fā)生了變化(增加、刪除、修改),那么遍歷效果可能會(huì)碰到如下問(wèn)題:新增的鍵可能沒(méi)有遍歷到,遍歷出了重復(fù)的鍵等情況(對(duì)于重復(fù)問(wèn)題,需要客戶(hù)端做去重處理),也就是說(shuō)scan并不能保證完整的遍歷出來(lái)所有的鍵,這些是我們?cè)陂_(kāi)發(fā)時(shí)需要考慮的。

          7、慎用flushdb/flushall命令。
          flushdb和flushall的區(qū)別:
          flushdb:Redis內(nèi)部默認(rèn)有16個(gè)數(shù)據(jù)庫(kù),flushdb用于清除當(dāng)前數(shù)據(jù)庫(kù)中的所有key,但是不執(zhí)行持久化操作。
          flushall:flushall用于清除Redis中所有的key,執(zhí)行持久化操作。

          這兩條命令是原子性的,不會(huì)終止執(zhí)行,一旦開(kāi)始執(zhí)行,將不會(huì)執(zhí)行失敗。如果當(dāng)前數(shù)據(jù)庫(kù)鍵值數(shù)量比較多,flushdb/flushall存在阻塞 Redis 的可能。

          對(duì)于keys、flushall、flashdb這些危險(xiǎn)命令,為了避免我們不小心用了,我們可以在配置文件中把這些命令設(shè)置為禁用,redis.conf配置文件中的rename-command參數(shù)。

          rename-command?KEYS?????""
          rename-command?FLUSHALL?""
          rename-command?FLUSHDB??""
          8、對(duì)bigkey執(zhí)行del命令,可能會(huì)導(dǎo)致Redis阻塞。
          說(shuō)明:
          Redis是單線程處理客戶(hù)端發(fā)送過(guò)來(lái)的命令,相當(dāng)于命令是串行執(zhí)行的,某條命令耗時(shí)過(guò)長(zhǎng)(比如del bigkey),會(huì)阻塞接下來(lái)的命令,這就是Redis阻塞。
          所以避免在生產(chǎn)環(huán)境中使用耗時(shí)過(guò)長(zhǎng)的命令。

          bigkey是指key對(duì)應(yīng)的value所占的內(nèi)存空間比較大。按照數(shù)據(jù)結(jié)構(gòu)來(lái)細(xì)分的話,bigkey分為字符串類(lèi)型bigkey非字符串類(lèi)型bigkey

          字符串類(lèi)型:體現(xiàn)在單個(gè)value值很大,一般認(rèn)為超過(guò)10KB就是bigkey。
          非字符串類(lèi)型:哈希、列表、集合、有序集合,體現(xiàn)在元素個(gè)數(shù)過(guò)多。

          那么對(duì)于bigkey,如果不用del刪除,我們應(yīng)該怎么刪除呢?

          一、分批刪除

          (1)、Delete Large List Key(刪除大的list鍵)
          方法1:使用ltrim key start end命令,分多次刪除,每次刪除少量元素。

          //?只保留[start, end]之間的元素,不在這個(gè)區(qū)間的元素都將被刪除。
          //?下標(biāo)?0?表示列表的第一個(gè)元素,下標(biāo) 1 表示列表的第二個(gè)元素,依次類(lèi)推。
          //?也可以使用負(fù)數(shù)下標(biāo),?-1 表示列表的最后一個(gè)元素,?-2 表示列表的倒數(shù)第二個(gè)元素,依次類(lèi)推。
          ltrim?key?start?end;
          //?插入元素
          127.0.0.1:6379>?lpush?listkey?a?b?c?d?e?f?
          (integer)?6
          //?分多次刪除元素
          //?第一次刪除,刪除"a""b"
          127.0.0.1:6379>?ltrim?listkey?0?1
          (integer)?2
          //?第一次刪除之后,遍歷list
          127.0.0.1:6379>?lrange?0?-1
          "c"
          "d"
          "e"
          "f"
          //?第二次刪除,刪除"c""d"
          127.0.0.1:6379>?ltrim?listkey?0?1
          (integer)?2
          //?第二次刪除之后,遍歷list
          127.0.0.1:6379>?lrange?0?-1
          "e"
          "f"
          //?第三次刪除,刪除"e""f"
          127.0.0.1:6379>?ltrim?listkey?0?1
          (integer)?2
          //?第三次刪除之后,遍歷list
          127.0.0.1:6379>?lrange?0?-1
          (empty?list?or?set)

          方法2:使用lpop或者rpop命令,依次將list隊(duì)列中所有的元素出隊(duì),直至清空隊(duì)列,達(dá)到刪除的目的。

          //?插入元素
          127.0.0.1:6379>?lpush?listkey?a?b?c?d?e?
          (integer)?5
          //?刪除元素
          127.0.0.1:6379>?lpop?listkey
          "e"
          127.0.0.1:6379>?lpop?listkey
          "d"
          127.0.0.1:6379>?lpop?listkey
          "c"
          127.0.0.1:6379>?lpop?listkey
          "b"
          127.0.0.1:6379>?lpop?listkey
          "a"
          //?獲取list列表長(zhǎng)度,列表長(zhǎng)度為0,表示列表已經(jīng)被清空。
          127.0.0.1:6379>?llen?listkey
          (integer)?0

          (2)、Delete Large Hash Key(刪除大的hash鍵)
          首先通過(guò)hscan命令遍歷出指定數(shù)量的元素,然后使用hdel命令依次刪除遍歷出來(lái)的數(shù)據(jù)集中的每個(gè)元素。

          見(jiàn)下面的例子,通過(guò)hscan命令,每次獲取500個(gè)元素,再用hdel命令對(duì)這500個(gè)元素一條一條進(jìn)行刪除。

          Jedis?jedis?=?new?Jedis("0.0.0.0",6379);

          public?void?deleteLargeHashKey(){
          ????//?分批刪除
          ????try?{
          ????????ScanParams?scanParams?=?new?ScanParams();
          ????????scanParams.count(500);//?每次刪除?500?條
          ????????String?cursor?=?"";//?分批刪除的游標(biāo),游標(biāo)為0,說(shuō)明全部數(shù)據(jù)都已經(jīng)被遍歷

          ????????while?(!cursor.equals("0")){
          ??????????ScanResult<String>?scanResult=jedis.sscan(hashkey,?cursor,?scanParams);
          ??????????cursor?=?scanResult.getStringCursor();
          ??????????List<String>?result?=?scanResult.getResult();
          ??????????long?t1?=?System.currentTimeMillis();
          ??????????//?遍歷出來(lái)的數(shù)據(jù)集,依次刪除每一條數(shù)據(jù)
          ??????????for(int?i?=?0;i?<?result.size();i++){
          ?????????????String?hashvalue?=?result.get(i);
          ?????????????jedis.srem(hashkey,?hashvalue);//?刪除一條數(shù)據(jù)
          ??????????}
          ??????????long?t2?=?System.currentTimeMillis();
          ??????????System.out.println("刪除"+result.size()+"條數(shù)據(jù),耗時(shí):?"+(t2-t1)+"毫秒,cursor:"+cursor);
          ????????}
          ????}catch?(JedisException?e){
          ????????e.printStackTrace();
          ????}finally?{
          ????????if(jedis?!=?null){
          ????????????jedis.close();
          ????????}
          ????}
          }

          (3)、Delete Large Set Key(刪除大的set鍵)
          和hash類(lèi)似,使用sscan命令,每次獲取500個(gè)元素,再用srem命令命令對(duì)這500個(gè)元素一條一條進(jìn)行刪除。

          (4)、Delete Large Sorted Set Key(刪除大的zset鍵)
          方法1:和List類(lèi)似,使用sortedset自帶的zremrangebyrank zsetkey 0 99命令,每次刪除下標(biāo)在[0, 99]之間的元素,即刪除top前100個(gè)元素。

          //?刪除指定排名內(nèi)的升序元素
          zremrangebyrank?key?start?end
          //?插入元素
          127.0.0.1:6379>?zadd?zsetkey?2000?jack?5000?tom?3500?peter?4000?lily?3000?alice
          //?第一次刪除,刪除下標(biāo)在[0,?1]之間的元素
          127.0.0.1:6379>?zremrangebyrank?zsetkey?0?1
          // jack 2000和alice 3000被刪除,此時(shí)遍歷一下集合。
          127.0.0.1:6379>?zrange?zsetkey?0?-1?withscores
          1)?"peter"
          2)?"3500"
          3)?"lily"
          4)?"4000"
          5)?"tom"
          6)?"5000"
          //?第二次刪除,刪除下標(biāo)在[0,?1]之間的元素
          127.0.0.1:6379>?zremrangebyrank?zsetkey?0?1
          // peter 3500和lily 4000被刪除,此時(shí)遍歷一下集合。
          127.0.0.1:6379>?zrange?zsetkey?0?-1?withscores
          1)?"tom"
          2)?"5000"
          //?第三次刪除,刪除下標(biāo)在[0,?1]之間的元素
          127.0.0.1:6379>?zremrangebyrank?zsetkey?0?1
          // tom 5000被刪除,此時(shí)遍歷一下集合,集合為空。
          127.0.0.1:6379>?zrange?zsetkey?0?-1?withscores
          (empty?list?or?set)

          方法2:和hash類(lèi)似,使用zscan命令,每次獲取500個(gè)元素,再用zrem命令命令對(duì)這500個(gè)元素一條一條進(jìn)行刪除。

          二、后臺(tái)刪除之Lazy Free方式

          為了解決redis使用del命令刪除大體積的key,或者使用flushdb、flushall刪除數(shù)據(jù)庫(kù)時(shí),造成redis阻塞的情況,在redis 4.0版本中引入了lazy free機(jī)制,可將刪除操作放在后臺(tái),讓后臺(tái)子線程(bio)執(zhí)行,避免主線程阻塞。使用lazy delete free的方式,刪除大鍵的過(guò)程不會(huì)阻塞正常請(qǐng)求。

          Lazy Free的使用分為兩類(lèi):第一類(lèi)是與DEL命令對(duì)應(yīng)的主動(dòng)刪除,第二類(lèi)是過(guò)期key刪除、maxmemory key驅(qū)逐淘汰刪除。

          • 主動(dòng)刪除

          UNLINK命令是與DEL一樣刪除key功能的lazy free實(shí)現(xiàn)。唯一不同時(shí),UNLINK在刪除集合類(lèi)鍵時(shí),如果集合鍵的元素個(gè)數(shù)大于64個(gè),會(huì)把真正的內(nèi)存釋放工作,交給單獨(dú)的bio來(lái)操作,這樣就不會(huì)阻塞 Redis 主線程了。

          示例如下:使用UNLINK命令刪除一個(gè)大鍵mylist, 它包含200萬(wàn)個(gè)元素,但用時(shí)只有0.03毫秒。

          //?查詢(xún)mylist中元素個(gè)數(shù)
          127.0.0.1:7000>?LLEN?mylist
          (integer)?2000000
          //?使用UNLINK命令刪除mylist
          127.0.0.1:7000>?UNLINK?mylist
          (integer)?1
          //?獲取慢查詢(xún)?nèi)罩?br />127.0.0.1:7000>?SLOWLOG?get
          1)?1)?(integer)?1
          ???2)?(integer)?1505465188
          ???3)?(integer)?30?//?命令耗時(shí)30微妙,0.03毫秒
          ???4)?1)?"UNLINK"
          ??????2)?"mylist"
          • 被動(dòng)刪除

          lazy free應(yīng)用于被動(dòng)刪除中,目前有4種場(chǎng)景,每種場(chǎng)景對(duì)應(yīng)一個(gè)配置參數(shù),默認(rèn)都是關(guān)閉的(no)。

          lazyfree-lazy-eviction?no
          lazyfree-lazy-expire?no
          lazyfree-lazy-server-del?no
          slave-lazy-flush?no

          lazyfree-lazy-eviction:針對(duì)redis中內(nèi)存使用達(dá)到maxmeory,并設(shè)置有淘汰策略的鍵,在被動(dòng)淘汰時(shí),是否采用lazy free機(jī)制。此場(chǎng)景開(kāi)啟lazy free,可能出現(xiàn)如下問(wèn)題:因?yàn)閮?nèi)存釋放不及時(shí),導(dǎo)致redis內(nèi)存超用,超過(guò)maxmemory的限制。此場(chǎng)景使用時(shí),請(qǐng)結(jié)合業(yè)務(wù)測(cè)試。

          lazyfree-lazy-expire:針對(duì)設(shè)置有TTL的鍵,到達(dá)過(guò)期時(shí)間,被redis刪除時(shí)是否采用lazy free機(jī)制。此場(chǎng)景建議開(kāi)啟,因TTL本身是自適應(yīng)調(diào)整的速度。

          lazyfree-lazy-server-del:針對(duì)有些指令在處理已存在的鍵時(shí),會(huì)帶有一個(gè)隱式的DEL鍵的操作。如rename命令,當(dāng)目標(biāo)鍵已存在,redis會(huì)先刪除目標(biāo)鍵,如果這些目標(biāo)鍵是一個(gè)bigkey,那就會(huì)引入阻塞刪除的性能問(wèn)題。此參數(shù)設(shè)置就是解決這類(lèi)問(wèn)題,建議開(kāi)啟。

          slave-lazy-flush:主從之間進(jìn)行全量數(shù)據(jù)同步時(shí),slave(從)在加載master(主)的RDB文件之前,會(huì)運(yùn)行flushall來(lái)清理自己的數(shù)據(jù)。這個(gè)參數(shù)設(shè)置決定同步過(guò)程中是否采用異常flush機(jī)制,如果內(nèi)存變動(dòng)不大,建議開(kāi)啟,可減少全量同步耗時(shí)。

          9、了解每個(gè)命令的時(shí)間復(fù)雜度在開(kāi)發(fā)中至關(guān)重要,例如在使用keys、 hgetall、smembers、zrange等時(shí)間復(fù)雜度(O(n))較高的命令時(shí),需要考慮數(shù)據(jù)規(guī)模(即 n)對(duì)Redis的影響。
          10、對(duì)于一些不支持批量操作的命令,盡量使用Pipeline支持批量執(zhí)行,提高程序效率。
          (1)、為什么使用 Pipeline?

          Redis客戶(hù)端執(zhí)行一次命令分為如下4個(gè)過(guò)程:1.發(fā)送命令-> 2.命令排隊(duì)-> 3.命令執(zhí)行-> 4.返回結(jié)果,其中 1 + 4 稱(chēng)為 Round Trip Time(RTT,命令執(zhí)行往返時(shí)間)。

          Redis提供了批量操作命令(例如mget、mset等),相對(duì)于單條操作命令(例如get、set等),可以有效地節(jié)約RTT。但是Redis還有大部分命令是不支持批量操作的,比如要執(zhí)行n次hgetall命令,并沒(méi)有 mhgetall 命令存在,所以需要引入pipeline來(lái)解決這個(gè)問(wèn)題。

          見(jiàn)下圖,逐條執(zhí)行n條命令,整個(gè)過(guò)程需要n次RRT,而使用Pipeline執(zhí)行n條命令,整個(gè)過(guò)程只需要1次RTT,可以看出來(lái)使用Pipeline可以減少網(wǎng)絡(luò)開(kāi)銷(xiāo),提高Redis請(qǐng)求的速度,減少Redis請(qǐng)求所需時(shí)間,同時(shí)提高Redis的吞吐量和并發(fā)量4de6f18450652f5c321ef3f9320f86ac.webp

          2b58b970175b9ef5d9c7b76fa3d094e1.webp使用Pipeline執(zhí)行n條命令模型
          (2)、非Pipeline和Pipeline性能對(duì)比?

          兩者性能對(duì)比見(jiàn)下表,執(zhí)行10000條set命令,使用非Pipeline和Pipeline的執(zhí)行時(shí)間對(duì)比。

          網(wǎng)絡(luò)類(lèi)型客戶(hù)端和Redis實(shí)例之間網(wǎng)絡(luò)延遲非PipelinePipeline
          客戶(hù)端和Redis實(shí)例部署在同一臺(tái)機(jī)器上0.17ms573ms134ms
          客戶(hù)端和Redis實(shí)例部署在內(nèi)網(wǎng)的兩臺(tái)機(jī)器上0.41ms1610ms240ms
          客戶(hù)端和Redis實(shí)例部署在異地機(jī)房7ms78499ms1104ms

          這是一組統(tǒng)計(jì)數(shù)據(jù)出來(lái)的數(shù)據(jù),使用Pipeline執(zhí)行速度比逐條執(zhí)行要快,特別是客戶(hù)端與服務(wù)端的網(wǎng)絡(luò)延遲越大,性能體現(xiàn)地越明顯。

          (3)、Redis原生批量命令(mset、mget)與Pipeline對(duì)比

          a)原生批量命令是原子的,Pipeline是非原子的,如果想要保證Pipeline里面所有的操作要么全部執(zhí)行,要么都不執(zhí)行,就要把Pipeline放到Redis事務(wù)中執(zhí)行。
          b)原生批量命令是一個(gè)命令對(duì)應(yīng)多個(gè)key,Pipeline是對(duì)應(yīng)多個(gè)命令。
          c)原生批量命令是Redis內(nèi)部自己實(shí)現(xiàn)的,而Pipeline需要Redis和客戶(hù)端共同實(shí)現(xiàn)。

          (4)、開(kāi)發(fā)中需要注意的地方

          a)使用 Pipeline 發(fā)送命令時(shí),每次 Pipeline 組裝的命令個(gè)數(shù)不能沒(méi)有節(jié)制,否則一次組裝的命令數(shù)據(jù)量過(guò)大,一方面會(huì)增加客戶(hù)端的等待時(shí)間,另一方面會(huì)造成一定的網(wǎng)絡(luò)阻塞,可以將一次包含大量命令的 Pipeline 拆分成多個(gè)較小的 Pipeline 來(lái)完成。

          b)雖然Pipeline只能操作一個(gè)Redis實(shí)例,但是在分布式 Redis 場(chǎng)景中,Pipeline也可以作為批量操作的重要優(yōu)化手段。

          c)使用Pipeline的前提條件時(shí),多個(gè)指令之間沒(méi)有依賴(lài)關(guān)系,先寫(xiě)后讀這種場(chǎng)景就不能使用Pipeline因?yàn)樽x之前必須寫(xiě),二者之間有依賴(lài)關(guān)系。

          //?此時(shí)不能使用Pipeline
          set?name?Tom
          get?name
          (5)、Pipeline實(shí)戰(zhàn)
          //?使用pipeline提交所有命令并返回執(zhí)行結(jié)果
          public?void?testPipeline(){
          ????//?建立Redis連接
          ????Jedis?jedis?=?new?Jedis("192.168.1.111",?6379);
          ????//?創(chuàng)建pipeline對(duì)象
          ????Pipeline?pipeline?=?jedis.pipelined();
          ????//?pipeline執(zhí)行命令,注意此時(shí)命令并未真正執(zhí)行?
          ????pipeline.set("name",?"james");?//?set命令
          ????pipeline.incr("age");//?incr命令
          ????pipeline.get("name");//?get命令
          ????//?將所有命令一起提交,此時(shí)命令才真正被執(zhí)行,并將執(zhí)行結(jié)果以List形式返回。
          ????List<Object>?list?=?pipeline.syncAndReturnAll();
          ????for?(Object?obj?:?list)?{
          ????????//?將執(zhí)行結(jié)果打印出來(lái)
          ????????System.out.println(obj);
          ????}
          ????//?斷開(kāi)連接,釋放資源
          ????jedis.disconnect();
          }

          1.pipelined.sync():一次性將所有命令異步發(fā)送到redis,不關(guān)注執(zhí)行結(jié)果。
          2.pipelined.syncAndReturnAll():使用這個(gè)方法,程序會(huì)阻塞,等到所有命令執(zhí)行完之后返回一個(gè)List集合。

          11、Redis每條原生命令都是原子性的,但是多條原生命令放在一起就無(wú)法保證是原子性的了,此時(shí)我們可以將這多條命令放在 Redis 事務(wù)中,以此來(lái)保證原子操作。

          (1)、multi命令和exec命令

          Redis提供了簡(jiǎn)單的事務(wù)功能,將一組需要一起執(zhí)行的命令放到 multi 和 exec兩個(gè)命令之間。multi命令表示事務(wù)的開(kāi)始,exec 命令表示事務(wù)的執(zhí)行,它們之間的命令是原子順序執(zhí)行的,即這組動(dòng)作,要么全部執(zhí)行,要么全部不執(zhí)行。

          例如在社交網(wǎng)站上用戶(hù)A關(guān)注了用戶(hù)B,那么需要在用戶(hù)A的關(guān)注表中加入用戶(hù)B,并且在用戶(hù)B的粉絲表中添加用戶(hù)A,這兩個(gè)行為要么全部執(zhí)行,要么全部不執(zhí)行,否則會(huì)出現(xiàn)數(shù)據(jù)不一致的情況。

          127.0.0.1:6379>?multi?
          OK?
          127.0.0.1:6379>?sadd?user:a:follow?user:b?
          QUEUED?
          127.0.0.1:6379>?sadd?user:b:fans?user:a?
          QUEUED
          //?此時(shí)上述命令還沒(méi)有真正執(zhí)行,所以用戶(hù)B還未加入到用戶(hù)A的關(guān)注表中,用戶(hù)A還未加入到用戶(hù)B的粉絲表中。
          127.0.0.1:6379>?sismember?user:a:follow?user:b?
          (integer)?0
          127.0.0.1:6379>?sismember?user:b:fans?user:a
          (integer)?0
          127.0.0.1:6379>?exec?
          1)?(integer)?1
          2)?(integer)?1?
          //?此時(shí)命令執(zhí)行完了,用戶(hù)B加入到了用戶(hù)A的關(guān)注表中,用戶(hù)A加入到了用戶(hù)B的粉絲表中。
          127.0.0.1:6379>?sismember?user:a:follow?user:b?
          (integer)?1

          上面的指令演示了如何使用事務(wù)保證上述業(yè)務(wù)的正確性。在Redis事務(wù)中,所有的指令在 exec 之前不執(zhí)行,而是緩存在服務(wù)器的一個(gè)事務(wù)隊(duì)列中,服務(wù)器一旦收到 exec 指令,才開(kāi)始執(zhí)行整個(gè)事務(wù)隊(duì)列,執(zhí)行完畢后一次性返回所有指令的運(yùn)行結(jié)果。因?yàn)?Redis 的單線程特性,它不用擔(dān)心自己在執(zhí)行隊(duì)列的時(shí)候被其它指令打攪,所以可以保證隊(duì)列中的指令能夠得到的「原子性」執(zhí)行。

          (2)、discard命令
          如果要停止事務(wù)的執(zhí)行,使用discard命令代替exec命令即可。discard 表示事務(wù)的丟棄,用于丟棄事務(wù)緩存隊(duì)列中的所有指令,在 exec 執(zhí)行之前。

          127.0.0.1:6379>?set?books?10
          127.0.0.1:6379>?get?books?
          "10"
          127.0.0.1:6379>?multi?
          OK?
          127.0.0.1:6379>?incr?books?
          QUEUED?
          127.0.0.1:6379>?incr?books?
          QUEUED?
          127.0.0.1:6379>?discard?
          OK?
          127.0.0.1:6379>?get?books?
          "10"

          我們可以看到 discard 之后,隊(duì)列中的所有指令都沒(méi)執(zhí)行,就好像 multi 和 discard 中間的所有指令從未發(fā)生過(guò)一樣。

          (3)、在分布式環(huán)境,當(dāng)多個(gè)客戶(hù)端并發(fā)訪問(wèn)存儲(chǔ)在 Redis 中的共享數(shù)據(jù)時(shí),為了保證數(shù)據(jù)一致性,除了可以使用分布式鎖(悲觀鎖),也可以使用 Redis 事務(wù)機(jī)制以及 watch 指令實(shí)現(xiàn)的樂(lè)觀鎖,樂(lè)觀鎖的效率相對(duì)悲觀鎖要高得多。

          watch 會(huì)一直監(jiān)控這個(gè)共享變量直到exec命令結(jié)束,當(dāng)事務(wù)執(zhí)行時(shí),也就是Redis接收到 exec 指令要順序執(zhí)行事務(wù)隊(duì)列中的命令時(shí),Redis 會(huì)檢查共享變量自 watch 之后,是否被修改了,如果共享變量被人動(dòng)過(guò)了,exec 指令就會(huì)返回 null 告知客戶(hù)端事務(wù)執(zhí)行失敗,這個(gè)時(shí)候客戶(hù)端一般會(huì)選擇重試。

          見(jiàn)下面這個(gè)業(yè)務(wù)場(chǎng)景,Redis 中存儲(chǔ)了我們的賬戶(hù)余額數(shù)據(jù),現(xiàn)在有兩個(gè)并發(fā)的客戶(hù)端要對(duì)賬戶(hù)余額進(jìn)行修改操作,我們需要先取出余額,然后做計(jì)算,最后將結(jié)果寫(xiě)回 Redis,Java代碼如下所示。

          public?int?doubleAccount(String?userId){
          ??Jedis?jedis?=?new?Jedis("192.168.1.111",?6379);
          ??String?key?=?userId;// key:用戶(hù)ID,value:賬戶(hù)余額
          ??while?(true)?{?
          ?????jedis.watch(key);//?watch?????
          ?????int?value?=?Integer.parseInt(jedis.get(key));//?賬戶(hù)余額
          ?????value?*=?2;?//?修改余額
          ?????Transaction?transaction?=?jedis.multi();//?multi??????
          ?????transaction.set(key,?String.valueOf(value));//?set命令
          ?????List<Object>?res?=?transaction.exec();?//?exec,并返回exec執(zhí)行結(jié)果
          ????if?(res?!=?null)?{?????????
          ??????break;?//?如果exec返回null,說(shuō)明共享變量已經(jīng)被其他客戶(hù)端修改了,此時(shí)需要重試,重新進(jìn)入循環(huán)。??
          ????}???
          ??}
          }

          注意事項(xiàng) :Redis 禁止在 multi 和 exec 之間執(zhí)行 watch 指令,watch 指令必須在 multi 之前,因?yàn)橹挥袕囊婚_(kāi)始就盯住這個(gè)有可能修改的變量,才能絕對(duì)保證不出錯(cuò)。

          (4)、使用Redis事務(wù)時(shí),如何優(yōu)化性能?
          上面的 Redis 事務(wù)在發(fā)送每個(gè)指令到事務(wù)緩存隊(duì)列時(shí)都要經(jīng)過(guò)一次網(wǎng)絡(luò)讀寫(xiě),當(dāng)一個(gè)事務(wù)內(nèi)部的指令較多時(shí),需要的網(wǎng)絡(luò) IO 時(shí)間也會(huì)線性增長(zhǎng)。所以通常 Redis 的客戶(hù)端在執(zhí)行事務(wù)時(shí)都會(huì)結(jié)合 pipeline 一起使用,這樣可以將多次 IO 操作壓縮為單次 IO 操作。

          (5)、在事務(wù)這塊,Redis和MySQL不一樣,當(dāng)Redis事務(wù)中某條命令執(zhí)行失敗,不會(huì)進(jìn)行回滾。

          當(dāng)Redis事務(wù)中某條命令執(zhí)行失敗的話,Redis會(huì)如何處理呢?分兩種情況,如下所示:
          a)命令語(yǔ)法錯(cuò)誤。
          如果事務(wù)中有一條命令語(yǔ)法不正確,進(jìn)入事務(wù)隊(duì)列的時(shí)候就會(huì)報(bào)錯(cuò)。

          這種情況下,事務(wù)中所有的命令就都不會(huì)執(zhí)行。

          //?下面操作錯(cuò)將set寫(xiě)成了sett,屬于語(yǔ)法錯(cuò)誤,會(huì)造成整個(gè)事務(wù)無(wú)法執(zhí)行,key和counter的值未發(fā)生變化。
          127.0.0.1:6388>?mget?key?counter?
          1)?"hello"?
          2)?"100"?
          127.0.0.1:6388>?multi?
          OK?
          127.0.0.1:6388>?sett?key?world?
          (error)?ERR?unknown?command?'sett'?
          127.0.0.1:6388>?incr?counter?
          QUEUED?
          127.0.0.1:6388>?exec?
          (error)?EXECABORT?Transaction?discarded?because?of?previous?errors.?
          127.0.0.1:6388>?mget?key?counter?
          1)?"hello"?
          2)?"100"

          b)命令格式正確,而操作的數(shù)據(jù)的類(lèi)型不符合命令要求。
          當(dāng)事務(wù)中存在某條命令,其語(yǔ)法正確,但是這條命令操作的數(shù)據(jù)的類(lèi)型不符合該條命令的要求,那么這條命令會(huì)進(jìn)入事務(wù)隊(duì)列,但是在執(zhí)行時(shí)會(huì)出現(xiàn)錯(cuò)誤。

          這種情況下,該條命令會(huì)執(zhí)行失敗,而其之前和之后的命令都會(huì)被正常執(zhí)行。

          127.0.0.1:6379>?multi?
          OK?
          127.0.0.1:6379>?set?name?Tom?
          QUEUED
          // name是字符串格式,不能進(jìn)行計(jì)算。
          127.0.0.1:6379>?incr?name?
          QUEUED?
          127.0.0.1:6379>?set?age?25?
          QUEUED
          127.0.0.1:6379>?exec?
          1)?(integer)?1?//?set?name?Tom?執(zhí)行成功
          2)?(error)?WRONGTYPE?Operation?against?a?key?holding?the?wrong?kind?of?value?//?zadd?這條命令執(zhí)行報(bào)錯(cuò)
          3)?(integer)?1?//?set?age?25?執(zhí)行成功
          127.0.0.1:6379>?get?name
          "Tom"
          127.0.0.1:6379>?get?age
          25

          第一種情況,所有命令要么全部執(zhí)行成功,要么全部執(zhí)行失敗,這不會(huì)產(chǎn)生什么問(wèn)題,大不了檢查一下,重新執(zhí)行。但是第二種情況,有可能一部分命令執(zhí)行成功,一部分命令執(zhí)行失敗,這就可能產(chǎn)生數(shù)據(jù)不一致的問(wèn)題。

          所以對(duì)于一些重要的操作,我們必須通過(guò)程序去檢測(cè)事務(wù)中命令操作的數(shù)據(jù)是否符合命令的要求,以保證 Redis 事務(wù)的正確執(zhí)行,避免出現(xiàn)數(shù)據(jù)不一致的情況。

          Redis 之所以保持這樣簡(jiǎn)易的事務(wù),完全是為了保證移動(dòng)互聯(lián)網(wǎng)的核心問(wèn)題——性能。

          12、Lua腳本同樣可以實(shí)現(xiàn) Pipeline 和 Redis 事務(wù)的功能,Redis可以使用Lua腳本創(chuàng)造出原子、高效、自定義命令組合。

          Lua腳本的好處如下所示:

          • Lua腳本在Redis中是原子執(zhí)行的,執(zhí)行過(guò)程中間不會(huì)插入其他命令(和 Redis 的事務(wù)很相似)。
          • Lua腳本可以幫助開(kāi)發(fā)和運(yùn)維人員創(chuàng)造出自己定制的命令,并可以將這 些命令常駐在Redis內(nèi)存中,實(shí)現(xiàn)復(fù)用的效果。
          • Lua腳本可以將多條命令一次性打包,有效地減少網(wǎng)絡(luò)開(kāi)銷(xiāo)(和 Redis 的 Pipeline 很相似)。
          13、運(yùn)維提示:在 RDB 持久化的過(guò)程中,如果遇到壞盤(pán)或磁盤(pán)寫(xiě)滿(mǎn)的情況,可以通過(guò) config set dir{newDir} 命令,在線修改文件路徑到可用的磁盤(pán)路徑,之后執(zhí)行 bgsave 進(jìn)行磁盤(pán)切換,同樣適用于 AOF 持久化文件。
          //?在持久化運(yùn)行期間動(dòng)態(tài)修改持久化文件存儲(chǔ)路徑。
          config?set?dir{newDir}
          14、Redis 默認(rèn)采用 LZF 算法對(duì)生成的 RDB 文件做壓縮處理,壓縮后的文件內(nèi)存變小很多很多,默認(rèn)開(kāi)啟,可以通過(guò) config set rdbcompressionyesno命令動(dòng)態(tài)修改。運(yùn)維提示:雖然壓縮 RDB 會(huì)消耗 CPU,但可大幅降低文件的體積,方便保存到硬盤(pán)或通過(guò)網(wǎng)絡(luò)發(fā)送給從節(jié)點(diǎn),因此線上建議開(kāi)啟。
          15、Redis 持久化功能一直是影響 Redis 性能的高發(fā)地,在本篇文章中,首先將列舉 Redis 持久化過(guò)程中影響 Redis 性能的幾個(gè)要素,以及如何避免它們拖慢 Redis 性能?
          (1)、fork 操作對(duì) Redis 性能的影響

          a)為什么 fork 操作會(huì)影響 Redis 性能?

          當(dāng) Redis 做 RDB 或 AOF 重寫(xiě)時(shí),一個(gè)必不可少的操作就是執(zhí)行 fork 操作創(chuàng)建子進(jìn)程(RDB 持久化和 AOF 持久化中的 AOF 重寫(xiě),都是由父進(jìn)程 fork 出來(lái)的子進(jìn)程執(zhí)行的),fork操作是一個(gè)重量級(jí)操作,耗時(shí)過(guò)長(zhǎng),會(huì)阻塞 Redis 主進(jìn)程

          因?yàn)?Redis 是單線程模式接收并執(zhí)行客戶(hù)端的命令,如果 Redis 發(fā)生阻塞,客戶(hù)端發(fā)出的命令無(wú)法被執(zhí)行,造成客戶(hù)端命令堆積。

          雖然 fork 創(chuàng)建的子進(jìn)程不需要拷貝父進(jìn)程的全部物理內(nèi)存空間,但是會(huì)復(fù)制父進(jìn)程的空間內(nèi)存頁(yè)表。例如對(duì)于 10GB 的 Redis 進(jìn)程(Redis 中存儲(chǔ)數(shù)據(jù)量占用內(nèi)存大小為 10 GB),需要復(fù)制大約 20MB 的內(nèi)存頁(yè)表。所以說(shuō) fork 操作耗時(shí)時(shí)長(zhǎng)和 Redis 內(nèi)存中存儲(chǔ)數(shù)據(jù)量成正相關(guān),即 Redis 內(nèi)存中存儲(chǔ)的數(shù)據(jù)集越大,fork 操作耗時(shí)時(shí)間越長(zhǎng)。

          b)fork 操作耗時(shí)問(wèn)題定位?

          如果 fork 操作耗時(shí)在秒級(jí)別就將拖慢 Redis 幾萬(wàn)條命令執(zhí)行,對(duì)線上應(yīng)用延遲影響非常明顯,正常情況下 fork 耗時(shí)應(yīng)該是每 GB 消耗 20 毫秒左右,即 fork 正常操作耗時(shí) = (Redis數(shù)據(jù)集 /1 GB) * 20 毫秒。

          我們可以執(zhí)行 info stats 命令,獲得 latest_fork_usec 參數(shù),這個(gè)參數(shù)表示最近一個(gè) fork 操作的耗時(shí),單位為微秒,根據(jù) Redis 當(dāng)前數(shù)據(jù)量大小計(jì)算正常情況下的 fork 耗時(shí),二者進(jìn)行對(duì)比,判斷當(dāng)前是否發(fā)生 fork 超時(shí)了。

          c)既然 fork 操作在所難免,那么如何盡量降低其對(duì) Redis 性能的影響呢?

          c.1)在部署 Redis 時(shí),優(yōu)先使用物理機(jī)或者高效支持fork操作的虛擬化技術(shù),避免使用 Xen 虛擬機(jī)。
          c.2)控制 Redis 實(shí)例存儲(chǔ)的數(shù)據(jù)量,fork 耗時(shí)跟 Redis 存儲(chǔ)數(shù)據(jù)量成正比,線上建議每個(gè) Redis 實(shí)例存儲(chǔ)數(shù)據(jù)量占用內(nèi)存大小控制在10GB以?xún)?nèi)。
          c.3)合理配置部署 Redis 的 Linux 操作系統(tǒng)的內(nèi)存分配策略,避免物理內(nèi)存不足導(dǎo)致 fork 失敗,具體看后述《使用?Redis時(shí),Linux 操作系統(tǒng)的配置》。
          c.4)降低fork操作的頻率,比如當(dāng)使用 AOF 持久化時(shí),可以調(diào)大 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 這兩個(gè)參數(shù),降低 AOF 和 AOF 重寫(xiě)的頻率。

          (2)、子進(jìn)程對(duì) CPU、內(nèi)存、硬盤(pán)三部分的開(kāi)銷(xiāo),以及如何優(yōu)化盡量降低開(kāi)銷(xiāo)。

          子進(jìn)程負(fù)責(zé) RDB 和 AOF重寫(xiě),它的運(yùn)行過(guò)程主要涉及CPU、內(nèi)存、硬盤(pán)三部分的消耗。

          a)CPU開(kāi)銷(xiāo)
          a.1)開(kāi)銷(xiāo)
          子進(jìn)程負(fù)責(zé)把 Redis 主進(jìn)程內(nèi)的數(shù)據(jù)分批寫(xiě)入 RDB 或者 AOF 文件中,這個(gè)過(guò)程屬于CPU密集操作,通常子進(jìn)程對(duì)單核 CPU 的利用率接近90%。

          a.2)優(yōu)化
          1、最好不要把 Redis 實(shí)例綁在一個(gè) CPU 核上,因?yàn)?fork 子進(jìn)程非常消耗CPU,會(huì)和 Redis 主進(jìn)程搶占 cpu 資源,導(dǎo)致 Redis 主進(jìn)程變慢,影響性能。

          我們可以把 Redis 實(shí)例綁定到一個(gè)物理核上,因?yàn)槲覀兊?CPU 一個(gè)物理核上有兩個(gè)邏輯核,這樣可以把我們的兩個(gè)邏輯核都給用上,可以在一定程度上緩解 CPU 的資源競(jìng)爭(zhēng)。還有一種辦法,就是修改 Redis 源代碼,把子進(jìn)程和主進(jìn)程綁到不同的CPU核上。

          2、不要將 Redis 和其他 CPU 密集型服務(wù)部署在一起,造成CPU過(guò)度競(jìng)爭(zhēng)。

          3、如果部署多個(gè)Redis實(shí)例,盡量保證同一時(shí)刻只有一個(gè)子進(jìn)程執(zhí)行重寫(xiě)工作。

          b)內(nèi)存消耗
          b.1)開(kāi)銷(xiāo)
          子進(jìn)程是由主進(jìn)程 fork 出來(lái)的,所以子進(jìn)程占用內(nèi)存和主進(jìn)程內(nèi)存成正相關(guān)

          理論上,主進(jìn)程在 fork 子進(jìn)程的時(shí)候,會(huì)將自己的內(nèi)存復(fù)制完整的一份給子進(jìn)程。由于這個(gè)全量復(fù)制的過(guò)程非常耗時(shí),所以 Linux 操作系統(tǒng)引入了寫(xiě)時(shí)復(fù)制(copy-on-write)機(jī)制,父子進(jìn)程一開(kāi)始就共享相同的物理內(nèi)存頁(yè),只有當(dāng)父進(jìn)程有寫(xiě)入的時(shí)候,父進(jìn)程才會(huì)把要修改的物理內(nèi)存頁(yè)創(chuàng)建副本發(fā)給子進(jìn)程

          b.2)如何查看 fork 內(nèi)存消耗是多少?
          查看使用 RDB 持久化方式, fork 操作消耗的內(nèi)存大小,Redis 進(jìn)行 RDB 時(shí),Redis日志輸出內(nèi)容如下所示:

          *?Background?saving?started?by?pid?7692?
          *?DB?saved?on?disk?
          *?RDB:?5?MB?of?memory?used?by?copy-on-write?
          *?Background?saving?terminated?with?success

          從日志第三行,可以看到 RDB 持久化 fork 子進(jìn)程的時(shí)候,消耗了 5MB 的內(nèi)存。

          查看使用 AOF 持久化方式, fork 操作消耗的內(nèi)存大小,Redis 進(jìn)行 AOF 時(shí),Redis日志輸出內(nèi)容如下所示:

          *?Background?append?only?file?rewriting?started?by?pid?8937?
          *?AOF?rewrite?child?asks?to?stop?sending?diffs.?
          *?Parent?agreed?to?stop?sending?diffs.?Finalizing?AOF...?
          *?Concatenating?0.00?MB?of?AOF?diff?received?from?parent.?
          *?SYNC?append?only?file?rewrite?performed?
          *?AOF?rewrite:?53?MB?of?memory?used?by?copy-on-write?
          *?Background?AOF?rewrite?terminated?with?success?
          *?Residual?parent?diff?successfully?flushed?to?the?rewritten?AOF?(1.49?MB)?
          *?Background?AOF?rewrite?finished?successfully

          從日志第六行,可以看到 AOF 重寫(xiě) fork 子進(jìn)程的時(shí)候消耗了 53MB 的內(nèi)存。

          b.3)優(yōu)化
          1、如果部署多個(gè) Redis 實(shí)例,盡量保證同一時(shí)刻只有一個(gè)子進(jìn)程在工作。

          2、避免在大量寫(xiě)入時(shí)做持久化操作,這樣將導(dǎo)致父進(jìn)程維護(hù)大量?jī)?nèi)存頁(yè)副本,造成內(nèi)存消耗。

          3、在使用 Linux 2.6.38及以后的版本,建議關(guān)閉 Linux 的 THP 功能,可以降低 fork 操作的內(nèi)存消耗。

          Linux kernel 在 2.6.38 及以后的版本中,增加了 Transparent Huge Pages(內(nèi)存大頁(yè)機(jī)制,THP)功能,該功能支持 2MB 的大頁(yè)內(nèi)存分配,默認(rèn)開(kāi)啟。

          THP 能減少內(nèi)存分配的次數(shù),同時(shí)可以加快子進(jìn)程的 fork 速度。但是如果開(kāi)啟 THP,復(fù)制內(nèi)存頁(yè)單位將從原來(lái)的 4KB 變?yōu)?2MB,如果主進(jìn)程只寫(xiě)入或者修改了 2KB 的數(shù)據(jù),Redis 也需要從主進(jìn)程復(fù)制 2MB 的內(nèi)存大頁(yè)給子進(jìn)程,而在常規(guī)情況下,只需要復(fù)制 4KB 的內(nèi)存頁(yè),這使得每次寫(xiě)命令引起的復(fù)制內(nèi)存頁(yè)的單位放大了 512 倍,這會(huì)拖慢寫(xiě)操作,影響 Redis 的性能

          c)硬盤(pán)消耗

          c.1)開(kāi)銷(xiāo)
          子進(jìn)程主要職責(zé)是把 AOF 或者 RDB 文件寫(xiě)入硬盤(pán)持久化,勢(shì)必會(huì)造成硬盤(pán)寫(xiě)入的壓力。

          c.2)優(yōu)化
          1、不要和其他高硬盤(pán)負(fù)載的服務(wù)部署在一起。如:存儲(chǔ)服務(wù)、消息隊(duì)列服務(wù)等。

          2、當(dāng)開(kāi)啟 AOF 功能的 Redis 用于高流量寫(xiě)入場(chǎng)景時(shí),這時(shí) Redis 實(shí)例的瓶頸主要在 AOF 文件同步到硬盤(pán)這個(gè)操作上。

          3、對(duì)于單機(jī)配置多個(gè)Redis實(shí)例的情況,可以配置不同實(shí)例分盤(pán)存儲(chǔ) AOF文件,分?jǐn)傆脖P(pán)寫(xiě)入壓力。

          16、巧用慢查詢(xún),可以幫助我們找出一些慢redis操作,這些慢操作可能阻塞Redis,及時(shí)發(fā)現(xiàn)及時(shí)解決,此外對(duì)慢操作進(jìn)行優(yōu)化,可以提高程序性能。

          (1)、什么是慢查詢(xún)?
          所謂慢查詢(xún)?nèi)罩揪褪窍到y(tǒng)在命令執(zhí)行前后計(jì)算每條命令的執(zhí)行時(shí)間,當(dāng)超過(guò)預(yù)設(shè)閥值,就將這條命令的相關(guān)信息(例如:發(fā)生時(shí)間,耗時(shí),命令的詳細(xì)信息)記錄下來(lái)。
          (2)、慢查詢(xún)的兩個(gè)參數(shù),以及如何修改參數(shù)設(shè)置。
          slowlog-log-slower-than:慢查詢(xún)閾值,單位是微秒,默認(rèn)值是10000,假如執(zhí)行了一條“很慢”的命令(例如keys *),如果它的執(zhí)行時(shí)間超過(guò)了10000微秒,那么它將被記錄在慢查詢(xún)?nèi)罩局小?br />slowlog-max-len:Redis使用一個(gè)列表(列表在內(nèi)存中)來(lái)存儲(chǔ)慢查詢(xún)?nèi)罩荆瑂lowlog-max-len是這個(gè)列表的容量,表示最多存儲(chǔ)多少條慢查詢(xún)?nèi)罩尽.?dāng)慢查詢(xún)?nèi)罩玖斜硪烟幱谄渥畲箝L(zhǎng)度時(shí),最早插入的一個(gè)命令將從列表中移出。
          (3)、修改慢查詢(xún)的參數(shù)配置。
          一種是修改配置文件Redis.conf,另一種是使用config set命令動(dòng)態(tài)修改。

          slowlog-log-slower-than?10000
          slowlog-max-len?128?
          config?set?slowlog-log-slower-than?20000?
          config?set?slowlog-max-len?1000?
          config rewrite //?如果要Redis將配置持久化到本地配置文件,需要執(zhí)行config rewrite命令。

          (4)、慢查詢(xún)?nèi)罩镜慕M成。
          每個(gè)慢查詢(xún)?nèi)罩居?個(gè)屬性組成,分別是慢查詢(xún)?nèi)罩镜臉?biāo)識(shí) id、發(fā)生時(shí)間戳、命令耗時(shí)、執(zhí)行命令和參數(shù)。

          127.0.0.1:6379>?slowlog?get?//?獲取慢查詢(xún)?nèi)罩?br />1)?1)?(integer)?666??//?慢查詢(xún)?nèi)罩镜臉?biāo)識(shí)?id
          ???2)?(integer)?1456786500?//?慢查詢(xún)發(fā)生時(shí)間戳
          ???3)?(integer)?11615?//?命令耗時(shí),單位微妙
          ???4)?1)?"BGREWRITEAOF"?//?執(zhí)行命令和參數(shù)
          2)?1)?(integer)?665?
          ???2)?(integer)?1456718400?
          ???3)?(integer)?12006?
          ???4)?1)?"SETEX"?
          ??????2)?"video_info_200"?
          ??????3)?"300"?
          ??????4)?"2"
          ......

          (5)、慢查詢(xún)命令。
          a)slowlog get [n]:slowlog get [n],參數(shù)n指定查詢(xún)條數(shù)。
          b)slowlog len:獲取慢查詢(xún)?nèi)罩玖斜懋?dāng)前的長(zhǎng)度,即當(dāng)前Redis中有多少條慢查詢(xún)。

          127.0.0.1:6379>?slowlog?len
          (integer)?45

          c)slowlog reset:清理慢查詢(xún)?nèi)罩玖斜怼?br />

          127.0.0.1:6379>?slowlog?len
          (integer)?45
          127.0.0.1:6379>?slowlog?reset
          OK
          127.0.0.1:6379>?slowlog?len
          (integer)?0
          17、Redis提供了三種數(shù)據(jù)遷移的方式,建議使用 migrate 命令進(jìn)行鍵值遷移。

          (1)、move key db:在Redis內(nèi)部多個(gè)數(shù)據(jù)庫(kù)之間進(jìn)行數(shù)據(jù)遷移,遷移過(guò)程是原子性的,只能遷移一個(gè)鍵。
          Redis內(nèi)部可以有多個(gè)數(shù)據(jù)庫(kù),彼此在數(shù)據(jù)上是相互隔離的,move key db就 是把指定的鍵從源數(shù)據(jù)庫(kù)移動(dòng)到目標(biāo)數(shù)據(jù)庫(kù)中。由于Redis的多數(shù)據(jù)庫(kù)功能不 建議在生產(chǎn)環(huán)境使用,所該條命令基本用不到。a75d5386ea0ec73fcb0d9faf0bacfd01.webp

          (2)、dump key + restore key ttl value:在不同的Redis實(shí)例間進(jìn)行數(shù)據(jù)遷移,遷移過(guò)程是非原子性的,只能遷移一個(gè)鍵。
          整個(gè)遷移過(guò)程分為兩步:
          1)在源Redis上,dump命令會(huì)將需要遷移的鍵值進(jìn)行序列化,格式采用的是RDB格式。
          2)在目標(biāo)Redis上,restore命令將上面序列化的鍵值進(jìn)行復(fù)原,其中ttl參 數(shù)代表過(guò)期時(shí)間,如果ttl=0代表沒(méi)有過(guò)期時(shí)間。
          f4be91abb23190d44c9544f65d8cb59a.webp

          使用dump+restore,有一點(diǎn)需要注意,整個(gè)遷移過(guò)程并非原子性的,而是通過(guò)客戶(hù)端分步完成的(source redis的客戶(hù)端:將源Redis需要遷移的鍵對(duì)應(yīng)的值執(zhí)行序列化,target redis的客戶(hù)端:執(zhí)行反序列化,并將反序列化的結(jié)果寫(xiě)入目標(biāo)Redis)。

          下面用示例演示 dump+restore 命令,包括命令模式和Java模式,如下所示。

          命令模式:

          //?在源Redis上執(zhí)行dump
          redis-source>?set?hello?world
          OK
          redis-source>?dump?hello
          "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
          //?在目標(biāo)Redis上執(zhí)行restore
          redis-target>?get?hello
          (nil)
          redis-target>?restore?hello?0?"\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
          OK
          redis-target>?get?hello
          "world"

          Java模式:

          Redis?sourceRedis?=?new?Redis("sourceMachine",?6379);
          Redis?targetRedis?=?new?Redis("targetMachine",?6379);
          targetRedis.restore("hello",?0,?sourceRedis.dump(key));

          (3)、migrate host port key|"" destination-db timeout [copy] [replace] [keys key [key ..,,遷移過(guò)程是原子性的,可以遷移多個(gè)鍵。

          ?host:目標(biāo)Redis的IP地址。
          ?port:目標(biāo)Redis的端口。
          ?key|"":在Redis3.0.6版本之前,migrate只支持遷移一個(gè)鍵,所以此處是 要遷移的鍵,但Redis3.0.6版本之后支持遷移多個(gè)鍵,如果當(dāng)前需要遷移多 個(gè)鍵,此處為空字符串""。
          ?destination-db:目標(biāo)Redis的數(shù)據(jù)庫(kù)索引,例如要遷移到0號(hào)數(shù)據(jù)庫(kù),這 里就寫(xiě)0。
          ?timeout:遷移的超時(shí)時(shí)間(單位為毫秒)。
          ?[copy]:如果添加此選項(xiàng),遷移后并不刪除源鍵。
          ?[replace]:針對(duì)這種場(chǎng)景,源Redis要遷移的鍵在目標(biāo)Redis已經(jīng)存在,如果添加replace選項(xiàng),則正常遷移并進(jìn)行數(shù)據(jù)覆蓋,如果不添加replace選項(xiàng),則報(bào)錯(cuò)遷移失敗。
          ?[keys key[key...]]:遷移多個(gè)鍵,例如要遷移key1、key2、key3,此處填 寫(xiě)“keys key1 key2 key3”。

          migrate命令也是用于在Redis實(shí)例間進(jìn)行數(shù)據(jù)遷移,migrate命令是dump、restore、del三個(gè)命令的組合。

          與dump+restore不同的是,migrate具有如下特點(diǎn):
          一是:使用migrate進(jìn)行鍵遷移,整個(gè)過(guò)程是原子執(zhí)行的,不需要在多個(gè)Redis實(shí)例上開(kāi)啟客戶(hù)端,只需要在源Redis上執(zhí)行migrate命令即可。
          二是:從Redis3.0.6版本以后,migrate支持遷移多個(gè)鍵的功能,有效地提高了遷移效率。

          基于以上兩點(diǎn),原子性和批量遷移,migrate成為Redis Cluster實(shí)現(xiàn)水平擴(kuò)容的重要工具。

          migrate內(nèi)部操作過(guò)程,如下圖所示:
          第一步:在源Redis上執(zhí)行migrate命令。
          第二步:要遷移的數(shù)據(jù)從源Redis直接傳輸?shù)侥繕?biāo)Redis,不需要序列化、反序列化操作。
          第三步:目標(biāo)Redis完成restore操作后會(huì)發(fā)送OK給源Redis,源Redis接收后會(huì)根據(jù)migrate對(duì)應(yīng)的選項(xiàng)來(lái)決定是否在源Redis上刪除對(duì)應(yīng)的鍵。

          下面用示例演示 migrate 命令,如下所示,現(xiàn)要將源Redis的hello鍵遷移到目標(biāo)Redis中,有以下三種情況。

          情況1:源Redis有鍵hello,目標(biāo)Redis沒(méi)有:

          127.0.0.1:6379>?migrate?127.0.0.1?6380?hello?0?1000?OK

          情況2:源Redis和目標(biāo)Redis都有鍵hello:

          127.0.0.1:6379>?get?hello?
          "world"?
          127.0.0.1:6380>?get?hello?
          "redis"

          如果migrate命令沒(méi)有加replace選項(xiàng)會(huì)收到錯(cuò)誤提示,如果加了replace會(huì)返回OK表明遷移成功:

          127.0.0.1:6379>?migrate?127.0.0.1?6379?hello?0?1000?
          (error)?ERR?Target?instance?replied?with?error:?BUSYKEY?Target?key?name?already?
          127.0.0.1:6379>?migrate?127.0.0.1?6379?hello?0?1000?replace?
          OK

          情況3:源Redis沒(méi)有鍵hello。如下所示,此時(shí)會(huì)收到nokey的提示:

          127.0.0.1:6379>?migrate?127.0.0.1?6380?hello?0?1000?
          NOKEY

          Redis3.0.6版本以后遷移多個(gè)鍵的功能:

          //?源Redis批量添加多個(gè)鍵
          127.0.0.1:6379>?mset?key1?value1?key2?value2?key3?value3?
          OK
          //?源Redis執(zhí)行如下命令完成多個(gè)鍵的遷移
          127.0.0.1:6379>?migrate?127.0.0.1?6380?""?0?5000?keys?key1?key2?key3?
          OK
          18、使用redis-cli --bigkeys命令,可以找到 Redis 中內(nèi)存占用比較大的的鍵值,這些大鍵可能會(huì)阻塞 Redis。

          redis-cli --bigkeys 內(nèi)部原理使用 scan 命令進(jìn)行分段采樣,把歷史掃描過(guò)的最大對(duì)象統(tǒng)計(jì)出來(lái)。

          #?redis-cli?--bigkeys?
          #?Scanning?the?entire?keyspace?to?find?biggest?keys?as?well?as?
          #?average?sizes?per?key?type.?You?can?use?-i?0.1?to?sleep?0.1?sec?
          #?per?100?SCAN?commands?(not?usually?needed).?
          [00.00%]?Biggest?string?found?so?far?'ptc:-571805194744395733'?with?17?bytes?
          [00.00%]?Biggest?string?found?so?far?'RVF#2570599,1'?with?3881?bytes?
          [00.01%]?Biggest?hash?found?so?far?'pcl:8752795333786343845'?with?208?fields?
          [00.37%]?Biggest?string?found?so?far?'RVF#1224557,1'?with?3882?bytes?
          [00.75%]?Biggest?string?found?so?far?'ptc:2404721392920303995'?with?4791?bytes?[04.64%]?Biggest?string?found?so?far?'pcltm:614'?with?5176729?bytes?
          [08.08%]?Biggest?string?found?so?far?'pcltm:8561'?with?11669889?bytes?
          [21.08%]?Biggest?string?found?so?far?'pcltm:8598'?with?12300864?bytes?
          ..忽略更多輸出...?
          --------?summary?------
          Sampled?3192437?keys?in?the?keyspace!?
          Total?key?length?in?bytes?is?78299956?(avg?len?24.53)?
          Biggest?string?found?'pcltm:121'?has?17735928?bytes?
          Biggest?hash?found?'pcl:3650040409957394505'?has?209?fields?
          2526878?strings?with?954999242?bytes?(79.15%?of?keys,?avg?size?377.94)?
          0?lists?with?0?items?(00.00%?of?keys,?avg?size?0.00)?
          0?sets?with?0?members?(00.00%?of?keys,?avg?size?0.00)?
          665559?hashs?with?19013973?fields?(20.85%?of?keys,?avg?size?28.57)?
          0?zsets?with?0?members?(00.00%?of?keys,?avg?size?0.00)
          19、使用redis-cli --latency命令可以獲取客戶(hù)端到 Redis 實(shí)例之間的網(wǎng)絡(luò)延遲。

          redis-cli --latency -h [host] -p [port],查看客戶(hù)端到目標(biāo)Redis之間的網(wǎng)絡(luò)延遲,它通過(guò)測(cè)量Redis服務(wù)器以毫秒為單位響應(yīng)Redis PING命令的時(shí)間來(lái)實(shí)現(xiàn)的,這個(gè)命令對(duì)于Redis的開(kāi)發(fā)和運(yùn)維非常有幫助。

          host和port分別是Redis實(shí)例的IP和端口,網(wǎng)絡(luò)延遲時(shí)間的單位是毫秒。

          min:0,max:15,avg:0.12(2839 samples),這是命令返回的數(shù)據(jù)。

          samples:采樣數(shù),表示redis-cli一共發(fā)出的PING命令和接收回復(fù)的次數(shù),稱(chēng)為一次網(wǎng)絡(luò)連接。
          min:最小值,它表示所有網(wǎng)絡(luò)連接中的最小延遲,這是我們采樣數(shù)據(jù)的最佳響應(yīng)時(shí)間。
          max:最大值,它表示所有網(wǎng)絡(luò)連接中的最大延遲,這是我們采樣數(shù)據(jù)的最長(zhǎng)響應(yīng)時(shí)間。
          avg:平均值,它表示所有網(wǎng)絡(luò)連接的平均響應(yīng)時(shí)間。

          見(jiàn)下圖,客戶(hù)端B和Redis實(shí)例都在機(jī)房B,客戶(hù)端A在機(jī)房A,機(jī)房A和機(jī)房B是跨地區(qū)的。1f164dac18c478be4f1b6177d55a4d52.webp

          //?獲取客戶(hù)端B到Redis之間的網(wǎng)絡(luò)延遲,只需在客戶(hù)端B執(zhí)行如下命令。
          redis-cli?-h?192.168.10.11?-p?6379?--latency?
          min:?0,?max:?1,?avg:?0.07?(4211?samples)
          //?獲取客戶(hù)端A到Redis之間的網(wǎng)絡(luò)延遲,只需在客戶(hù)端A執(zhí)行如下命令。
          redis-cli?-h?192.168.10.11?-p?6379?--latency??
          min:?0,?max:?2,?avg:?1.04?(2096?samples)

          我們看到客戶(hù)端A到Redis實(shí)例之間的網(wǎng)絡(luò)延遲時(shí)間為1.04毫秒,客戶(hù)端B到Redis實(shí)例之間的網(wǎng)絡(luò)延遲時(shí)間為0.07毫秒,這是因?yàn)榭蛻?hù)端A距離Redis比較遠(yuǎn),所以平均網(wǎng)絡(luò)延遲會(huì)稍微高一些。


          瀏覽 60
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产黄色小说 | 一区二区三区四区精品视频 | 成人毛片18女人毛片软件下载 | 麻豆视屏 | 5566中文字幕 |