一、背景
在京東到家購(gòu)物車(chē)系統(tǒng)中,用戶(hù)基于門(mén)店能夠?qū)ι唐愤M(jìn)行加車(chē)操作。用戶(hù)與門(mén)店商品使用Redis的Hash類(lèi)型存儲(chǔ),如下代碼塊所示。不知細(xì)心的你有沒(méi)有發(fā)現(xiàn),如果單門(mén)店加車(chē)商品過(guò)多,或者門(mén)店過(guò)多時(shí),此Key就會(huì)越來(lái)越大,從而影響線上業(yè)務(wù)。
userPin:{
storeId:{門(mén)店下加車(chē)的所有商品基本信息},
storeId:{門(mén)店下加車(chē)的所有商品基本信息},
......
}
二、BigKey的界定和如何產(chǎn)生
2.1、BigKey的界定
BigKey稱(chēng)為大Key,通常以Key對(duì)應(yīng)Value的存儲(chǔ)大小,或者Key對(duì)應(yīng)Value的數(shù)量來(lái)進(jìn)行綜合判斷。對(duì)于大Key也沒(méi)有嚴(yán)格的定義區(qū)分,針對(duì)String與非String結(jié)構(gòu),給出如下定義:
- String:String類(lèi)型的 Key 對(duì)應(yīng)的 Value 超過(guò) 10KB
- 非String結(jié)構(gòu)(Hash,Set,ZSet,List):Value的數(shù)量達(dá)到10000個(gè),或者Vaule的總大小為100KB
- 集群中Key的總數(shù)超過(guò)1億
2.2、如何產(chǎn)生
1、數(shù)據(jù)結(jié)構(gòu)設(shè)置不合理,例如集合中元素唯一時(shí),應(yīng)該使用Set替換List;
2、針對(duì)業(yè)務(wù)缺少預(yù)估性,沒(méi)有預(yù)見(jiàn)Value動(dòng)態(tài)增長(zhǎng);
3、Key沒(méi)有設(shè)置過(guò)期時(shí)間,把緩存當(dāng)成垃圾桶,一直再往里面扔,但是從不處理。
三、BigKey的危害
3.1、數(shù)據(jù)傾斜
redis數(shù)據(jù)傾斜分為數(shù)據(jù)訪問(wèn)傾斜和**數(shù)據(jù)量?jī)A斜,**會(huì)導(dǎo)致該Key所在的數(shù)據(jù)分片節(jié)點(diǎn)CPU使用率、帶寬使用率升高,從而影響該分片上所有Key的處理。
**數(shù)據(jù)訪問(wèn)傾斜:**某節(jié)點(diǎn)中key的QPS高于其他節(jié)點(diǎn)中的Key
**數(shù)據(jù)量?jī)A斜:**某節(jié)點(diǎn)中key的大小高于其他節(jié)點(diǎn)中的Key,如下圖,實(shí)例1中的Key1存儲(chǔ)高于其他實(shí)例。
3.2、網(wǎng)絡(luò)阻塞
Redis服務(wù)器是一個(gè)事件驅(qū)動(dòng)程序,有文件事件和時(shí)間事件,文件事件和時(shí)間事件都是主線程完成。其中文件事件就是服務(wù)器對(duì)套接字操作的抽象,客戶(hù)端與服務(wù)端的通信會(huì)產(chǎn)生相應(yīng)的文件事件,服務(wù)器通過(guò)監(jiān)聽(tīng)并處理這些事件來(lái)完成一系列網(wǎng)絡(luò)通信操作。
Redis基于Reactor模式開(kāi)發(fā)了自己的網(wǎng)絡(luò)事件處理器,即文件事件處理器,該處理器內(nèi)部使用I/O多路復(fù)用程序,可同時(shí)監(jiān)聽(tīng)多個(gè)套接字,并根據(jù)套接字執(zhí)行的任務(wù)來(lái)關(guān)聯(lián)不同的事件處理器。文件事件處理器以單線程的方式運(yùn)行,但是通過(guò)I/O多路復(fù)用程序來(lái)監(jiān)聽(tīng)多個(gè)套接字,既實(shí)現(xiàn)了高性能網(wǎng)絡(luò)通信模型,又保持了內(nèi)部單線程設(shè)計(jì)的簡(jiǎn)單性。文件事件處理器構(gòu)成如下圖:
文件事件是對(duì)套接字操作的抽象,包括連接應(yīng)答,寫(xiě)入,讀取,關(guān)閉,因?yàn)橐粋€(gè)服務(wù)器會(huì)連接多個(gè)套接字,所以文件事件可能并發(fā)出現(xiàn),即使文件事件并發(fā)的出現(xiàn),但是I/O多路復(fù)用程序會(huì)將套接字放入一個(gè)隊(duì)列,通過(guò)隊(duì)列有序的,同步的每次一個(gè)套接字的方式向文件事件分派器傳送套接字,當(dāng)讓一個(gè)套接字產(chǎn)生的事件被處理完畢后,I/O多路復(fù)用程序才會(huì)繼續(xù)向文件事件分派器傳送下一個(gè)套接字,當(dāng)有大key時(shí),單次操作時(shí)間延長(zhǎng),導(dǎo)致網(wǎng)絡(luò)阻塞。
3.3、慢查詢(xún)
嚴(yán)重影響 QPS 、TP99 等指標(biāo),對(duì)大Key進(jìn)行的慢操作會(huì)導(dǎo)致后續(xù)的命令被阻塞,從而導(dǎo)致一系列慢查詢(xún)。
3.4、CPU壓力
當(dāng)單Key過(guò)大時(shí),每一次訪問(wèn)此Key都可能會(huì)造成Redis阻塞,其他請(qǐng)求只能等待了。如果應(yīng)用中設(shè)置了超時(shí)等,那么上層就會(huì)拋出異常信息。最后刪除的時(shí)候也會(huì)造成redis阻塞,到時(shí)候內(nèi)存中數(shù)據(jù)量過(guò)大,就會(huì)造成CPU負(fù)載過(guò)高。單個(gè)分片cpu占用率過(guò)高,其他分片無(wú)法擁有cpu資源,從而被影響。此外,大 key 對(duì)持久化也有些影響。fork 操作會(huì)拷貝父進(jìn)程的頁(yè)表項(xiàng),如果過(guò)大,會(huì)占用更多頁(yè)表,主線程阻塞拷貝需要一定的時(shí)間。
四、如何檢測(cè)BigKey
4.1、redis-cli --bigkeys
首先我們從運(yùn)行結(jié)果出發(fā)。首先通過(guò)腳本插入一些數(shù)據(jù)到redis中,然后執(zhí)行redis-cli的–bigkeys選項(xiàng)
$ redis-cli --bigkeys
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.01 to sleep 0.01 sec
# per SCAN command (not usually needed).
-------- 第一部分start -------
[00.00%] Biggest string found so far 'key-419' with 3 bytes
[05.14%] Biggest list found so far 'mylist' with 100004 items
[35.77%] Biggest string found so far 'counter:__rand_int__' with 6 bytes
[73.91%] Biggest hash found so far 'myobject' with 3 fields
-------- 第一部分end -------
-------- summary -------
-------- 第二部分start -------
Sampled 506 keys in the keyspace!
Total key length in bytes is 3452 (avg len 6.82)
Biggest string found 'counter:__rand_int__' has 6 bytes
Biggest list found 'mylist' has 100004 items
Biggest hash found 'myobject' has 3 fields
-------- 第二部分end -------
-------- 第三部分start -------
504 strings with 1403 bytes (99.60% of keys, avg size 2.78)
1 lists with 100004 items (00.20% of keys, avg size 100004.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
1 hashs with 3 fields (00.20% of keys, avg size 3.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
-------- 第三部分end -------
以下我們分三步對(duì)bigkeys選項(xiàng)源碼原理進(jìn)行解析,簡(jiǎn)要流程如下圖:
4.1.1、第一部分是如何進(jìn)行找key的呢?
Redis找bigkey的函數(shù)是static void findBigKeys(int memkeys, unsigned memkeys_samples),因?yàn)楱Cmemkeys選項(xiàng)和–bigkeys選項(xiàng)是公用同一個(gè)函數(shù),所以使用memkeys時(shí)會(huì)有額外兩個(gè)參數(shù)memkeys、memkeys_sample,但這和–bigkeys選項(xiàng)沒(méi)關(guān)系,所以不用理會(huì)。findBigKeys具體函數(shù)框架為:
1.申請(qǐng)6個(gè)變量用以統(tǒng)計(jì)6種數(shù)據(jù)類(lèi)型的信息(每個(gè)變量記錄該數(shù)據(jù)類(lèi)型的key的總數(shù)量、bigkey是哪個(gè)等信息)
typedef struct {
char *name;//數(shù)據(jù)類(lèi)型,如string
char *sizecmd;//查詢(xún)大小命令,如string會(huì)調(diào)用STRLEN
char *sizeunit;//單位,string類(lèi)型為bytes,而hash為field
unsigned long long biggest;//最大key信息域,此數(shù)據(jù)類(lèi)型最大key的大小,如string類(lèi)型是多少bytes,hash為多少field
unsigned long long count;//統(tǒng)計(jì)信息域,此數(shù)據(jù)類(lèi)型的key的總數(shù)
unsigned long long totalsize;//統(tǒng)計(jì)信息域,此數(shù)據(jù)類(lèi)型的key的總大小,如string類(lèi)型是全部string總共多少bytes,hash為全部hash總共多少field
sds biggest_key;//最大key信息域,此數(shù)據(jù)類(lèi)型最大key的鍵名,之所以在數(shù)據(jù)結(jié)構(gòu)末尾是考慮字節(jié)對(duì)齊
} typeinfo;
dict *types_dict = dictCreate(&typeinfoDictType);
typeinfo_add(types_dict, "string", &type_string);
typeinfo_add(types_dict, "list", &type_list);
typeinfo_add(types_dict, "set", &type_set);
typeinfo_add(types_dict, "hash", &type_hash);
typeinfo_add(types_dict, "zset", &type_zset);
typeinfo_add(types_dict, "stream", &type_stream);
2.調(diào)用scan命令迭代地獲取一批key(注意只是key的名稱(chēng),類(lèi)型和大小scan命令不返回)
/* scan循環(huán)掃描 */
do {
/* 計(jì)算完成的百分比情況 */
pct = 100 * (double)sampled/total_keys;//這里記錄下掃描的進(jìn)度
/* 獲取一些鍵并指向鍵數(shù)組 */
reply = sendScan(&it);//這里發(fā)送SCAN命令,結(jié)果保存在reply中
keys = reply->element[1];//keys來(lái)保存這次scan獲取的所有鍵名,注意只是鍵名,每個(gè)鍵的數(shù)據(jù)類(lèi)型是不知道的。
......
} while(it != 0);
3.對(duì)每個(gè)key獲取它的數(shù)據(jù)類(lèi)型(type)和key的大?。╯ize)
/* 檢索類(lèi)型,然后檢索大小*/
getKeyTypes(types_dict, keys, types);
getKeySizes(keys, types, sizes, memkeys, memkeys_samples);
4.如果key的大小大于已記錄的最大值的key,則更新最大key的信息
/* Now update our stats */
for(i=0;i<keys->elements;i++) {
......//前面已解析
//如果遍歷到比記錄值更大的key時(shí)
if(type->biggest<sizes[i]) {
/* Keep track of biggest key name for this type */
if (type->biggest_key)
sdsfree(type->biggest_key);
//更新最大key的鍵名
type->biggest_key = sdscatrepr(sdsempty(), keys->element[i]->str, keys->element[i]->len);
if(!type->biggest_key) {
fprintf(stderr, "Failed to allocate memory for key!\n");
exit(1);
}
//每當(dāng)找到一個(gè)更大的key時(shí)則輸出該key信息
printf(
"[%05.2f%%] Biggest %-6s found so far '%s' with %llu %s\n",
pct, type->name, type->biggest_key, sizes[i],
!memkeys? type->sizeunit: "bytes");
/* Keep track of the biggest size for this type */
//更新最大key的大小
type->biggest = sizes[i];
}
......//前面已解析
}
5.對(duì)每個(gè)key更新對(duì)應(yīng)數(shù)據(jù)類(lèi)型的統(tǒng)計(jì)信息
/* 現(xiàn)在更新統(tǒng)計(jì)數(shù)據(jù) */
for(i=0;i<keys->elements;i++) {
typeinfo *type = types[i];
/* 跳過(guò)在SCAN和TYPE之間消失的鍵 */
if(!type)
continue;
//對(duì)每個(gè)key更新每種數(shù)據(jù)類(lèi)型的統(tǒng)計(jì)信息
type->totalsize += sizes[i];//某數(shù)據(jù)類(lèi)型(如string)的總大小增加
type->count++;//某數(shù)據(jù)類(lèi)型的key數(shù)量增加
totlen += keys->element[i]->len;//totlen不針對(duì)某個(gè)具體數(shù)據(jù)類(lèi)型,將所有key的鍵名的長(zhǎng)度進(jìn)行統(tǒng)計(jì),注意只統(tǒng)計(jì)鍵名長(zhǎng)度。
sampled++;//已經(jīng)遍歷的key數(shù)量
......//后續(xù)解析
/* 更新整體進(jìn)度 */
if(sampled % 1000000 == 0) {
printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
}
}
4.1.2、第二部分是如何執(zhí)行的?
1.輸出統(tǒng)計(jì)信息、最大key信息
/* We're done */
printf("\n-------- summary -------\n\n");
if (force_cancel_loop) printf("[%05.2f%%] ", pct);
printf("Sampled %llu keys in the keyspace!\n", sampled);
printf("Total key length in bytes is %llu (avg len %.2f)\n\n",
totlen, totlen ? (double)totlen/sampled : 0);
2.首先輸出總共掃描了多少個(gè)key、所有key的總長(zhǎng)度是多少。
/* Output the biggest keys we found, for types we did find */
di = dictGetIterator(types_dict);
while ((de = dictNext(di))) {
typeinfo *type = dictGetVal(de);
if(type->biggest_key) {
printf("Biggest %6s found '%s' has %llu %s\n", type->name, type->biggest_key,
type->biggest, !memkeys? type->sizeunit: "bytes");
}
}
dictReleaseIterator(di);
4.1.3、第三部分是如何執(zhí)行的?
di為字典迭代器,用以遍歷types_dict里面的所有dictEntry。de = dictNext(di)則可以獲取下一個(gè)dictEntry,de是指向dictEntry的指針。又因?yàn)閠ypeinfo結(jié)構(gòu)體保存在dictEntry的v域中,所以用dictGetVal獲取。然后就是輸出typeinfo結(jié)構(gòu)體里面保存的最大key相關(guān)的數(shù)據(jù),包括最大key的鍵名和大小。
di = dictGetIterator(types_dict);
while ((de = dictNext(di))) {
typeinfo *type = dictGetVal(de);
printf("%llu %ss with %llu %s (%05.2f%% of keys, avg size %.2f)\n",
type->count, type->name, type->totalsize, !memkeys? type->sizeunit: "bytes",
sampled ? 100 * (double)type->count/sampled : 0,
type->count ? (double)type->totalsize/type->count : 0);
}
dictReleaseIterator(di);
4.2、使用開(kāi)源工具發(fā)現(xiàn)大Key
在不影響線上服務(wù)的同時(shí)得到精確的分析報(bào)告。使用redis-rdb-tools工具以定制化方式找出大Key,該工具能夠?qū)edis的RDB文件進(jìn)行定制化的分析,但由于分析RDB文件為離線工作,因此對(duì)線上服務(wù)不會(huì)有任何影響,這是它的最大優(yōu)點(diǎn)但同時(shí)也是它的最大缺點(diǎn):離線分析代表著分析結(jié)果的較差時(shí)效性。對(duì)于一個(gè)較大的RDB文件,它的分析可能會(huì)持續(xù)很久很久。
redis-rdb-tools的項(xiàng)目地址為:https://github.com/sripathikrishnan/redis-rdb-tools
五、如何解決Bigkey
5.1、提前預(yù)防
- 設(shè)置過(guò)期時(shí)間,盡量過(guò)期時(shí)間分散,防止同一時(shí)間過(guò)期;
- 存儲(chǔ)為String類(lèi)型的JSON,可以刪除不使用的Filed;
例如對(duì)象為{“userName”:“京東到家”,“ciyt”:“北京”},如果只需要用到userName屬性,那就定義新對(duì)象,只具有userName屬性,精簡(jiǎn)緩存中數(shù)據(jù)
- 存儲(chǔ)為String類(lèi)型的JSON,利用@JsonProperty注解讓FiledName字符集縮小,代碼例子如下。但是存在緩存數(shù)據(jù)識(shí)別性低的缺點(diǎn);
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import java.io.IOException;
public class JsonTest {
@JsonProperty("u")
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public static void main(String[] args) throws IOException {
JsonTest output = new JsonTest();
output.setUserName("京東到家");
System.out.println(new ObjectMapper().writeValueAsString(output));
String json = "{\"u\":\"京東到家\"}";
JsonTest r1 = new ObjectMapper().readValue(json, JsonTest.class);
System.out.println(r1.getUserName());
}
}
{"u":"京東到家"}
京東到家
- 采用壓縮算法,利用時(shí)間換空間,進(jìn)行序列化與反序列化。同時(shí)也存在緩存數(shù)據(jù)識(shí)別性低的缺點(diǎn);
- 在業(yè)務(wù)上進(jìn)行干預(yù),設(shè)置閾值。比如用戶(hù)購(gòu)物車(chē)的商品數(shù)量,或者領(lǐng)券的數(shù)量,不能無(wú)限的增大;
5.2、如何優(yōu)雅刪除BigKey
5.2.1、DEL
此命令在Redis不同版本中刪除的機(jī)制并不相同,以下分別進(jìn)行分析:
redis_version < 4.0 版本:在主線程中同步刪除,刪除大Key會(huì)阻塞主線程,見(jiàn)如下源碼基于redis 3.0版本。那針對(duì)非String結(jié)構(gòu)數(shù)據(jù),可以先通過(guò)SCAN命令讀取部分?jǐn)?shù)據(jù),然后逐步進(jìn)行刪除,避免一次性刪除大key導(dǎo)致Redis阻塞。
// 從數(shù)據(jù)庫(kù)中刪除給定的鍵,鍵的值,以及鍵的過(guò)期時(shí)間。
// 刪除成功返回 1,因?yàn)殒I不存在而導(dǎo)致刪除失敗時(shí),返回 0
int dbDelete(redisDb *db, robj *key) {
// 刪除鍵的過(guò)期時(shí)間
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
// 刪除鍵值對(duì)
if (dictDelete(db->dict,key->ptr) == DICT_OK) {
// 如果開(kāi)啟了集群模式,那么從槽中刪除給定的鍵
if (server.cluster_enabled) slotToKeyDel(key);
return 1;
} else {
// 鍵不存在
return 0;
}
}
4.0 版本 < redis_version < 6.0 版本:引入lazy-free**,**手動(dòng)開(kāi)啟lazy-free時(shí),有4個(gè)選項(xiàng)可以控制,分別對(duì)應(yīng)不同場(chǎng)景下,是否開(kāi)啟異步釋放內(nèi)存機(jī)制:
- lazyfree-lazy-expire:key在過(guò)期刪除時(shí)嘗試異步釋放內(nèi)存
- lazyfree-lazy-eviction:內(nèi)存達(dá)到maxmemory并設(shè)置了淘汰策略時(shí)嘗試異步釋放內(nèi)存
- lazyfree-lazy-server-del:執(zhí)行RENAME/MOVE等命令或需要覆蓋一個(gè)key時(shí),刪除舊key嘗試異步釋放內(nèi)存
- replica-lazy-flush:主從全量同步,從庫(kù)清空數(shù)據(jù)庫(kù)時(shí)異步釋放內(nèi)存
開(kāi)啟lazy-free后,Redis在釋放一個(gè)key的內(nèi)存時(shí),首先會(huì)評(píng)估代價(jià),如果釋放內(nèi)存的代價(jià)很小,那么就直接在主線程中操作了,沒(méi)必要放到異步線程中執(zhí)行
redis_version >= 6.0 版本:引入lazyfree-lazy-user-del,只要開(kāi)啟了,del直接可以異步刪除key,不會(huì)阻塞主線程。具體是為什么呢,現(xiàn)在先賣(mài)個(gè)關(guān)子,在下面進(jìn)行解析。
5.2.2、SCAN
SCAN命令可以幫助在不阻塞主線程的情況下逐步遍歷大量的鍵,以及避免對(duì)數(shù)據(jù)庫(kù)的阻塞。以下代碼是利用scan來(lái)掃描集群中的Key。
public void scanRedis(String cursor,String endCursor) {
ReloadableJimClientFactory factory = new ReloadableJimClientFactory();
String jimUrl = "jim://xxx/546";
factory.setJimUrl(jimUrl);
Cluster client = factory.getClient();
ScanOptions.ScanOptionsBuilder scanOptions = ScanOptions.scanOptions();
scanOptions.count(100);
Boolean end = false;
int k = 0;
while (!end) {
KeyScanResult< String > result = client.scan(cursor, scanOptions.build());
for (String key :result.getResult()){
if (client.ttl(key) == -1){
logger.info("永久key為:{}" , key);
}
}
k++;
cursor = result.getCursor();
if (endCursor.equals(cursor)){
break;
}
}
}
5.2.3、UNLINK
Redis 4.0 提供了 lazy delete (unlink命令) ,下面基于源碼(redis_version:7.2版本)分析下實(shí)現(xiàn)原理
- del與unlink命令底層都調(diào)用了delGenericCommand()方法;
void delCommand(client *c) {
delGenericCommand(c,server.lazyfree_lazy_user_del);
}
void unlinkCommand(client *c) {
delGenericCommand(c,1);
}
- lazyfree-lazy-user-del支持yes或者no。默認(rèn)是no;
- 如果設(shè)置為yes,那么del命令就等價(jià)于unlink,也是異步刪除,這也同時(shí)解釋了之前咱們的問(wèn)題,為什么設(shè)置了lazyfree-lazy-user-del后,del命令就為異步刪除。
void delGenericCommand(client *c, int lazy) {
int numdel = 0, j;
// 遍歷所有輸入鍵
for (j = 1; j < c->argc; j++) {
// 先刪除過(guò)期的鍵
expireIfNeeded(c->db,c->argv[j],0);
int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
dbSyncDelete(c->db,c->argv[j]);
// 嘗試刪除鍵
if (deleted) {
// 刪除鍵成功,發(fā)送通知
signalModifiedKey(c,c->db,c->argv[j]);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[j],c->db->id);
server.dirty++;
// 成功刪除才增加 deleted 計(jì)數(shù)器的值
numdel++;
}
}
// 返回被刪除鍵的數(shù)量
addReplyLongLong(c,numdel);
}
下面分析異步刪除dbAsyncDelete()與同步刪除dbSyncDelete(),底層同時(shí)也是調(diào)用dbGenericDelete()方法
int dbSyncDelete(redisDb *db, robj *key) {
return dbGenericDelete(db, key, 0, DB_FLAG_KEY_DELETED);
}
int dbAsyncDelete(redisDb *db, robj *key) {
return dbGenericDelete(db, key, 1, DB_FLAG_KEY_DELETED);
}
int dbGenericDelete(redisDb *db, robj *key, int async, int flags) {
dictEntry **plink;
int table;
dictEntry *de = dictTwoPhaseUnlinkFind(db->dict,key->ptr,&plink,&table);
if (de) {
robj *val = dictGetVal(de);
/* RM_StringDMA may call dbUnshareStringValue which may free val, so we need to incr to retain val */
incrRefCount(val);
/* Tells the module that the key has been unlinked from the database. */
moduleNotifyKeyUnlink(key,val,db->id,flags);
/* We want to try to unblock any module clients or clients using a blocking XREADGROUP */
signalDeletedKeyAsReady(db,key,val->type);
// 在調(diào)用用freeObjAsync之前,我們應(yīng)該先調(diào)用decrRefCount。否則,引用計(jì)數(shù)可能大于1,導(dǎo)致freeObjAsync無(wú)法正常工作。
decrRefCount(val);
// 如果是異步刪除,則會(huì)調(diào)用 freeObjAsync 異步釋放 value 占用的內(nèi)存。同時(shí),將 key 對(duì)應(yīng)的 value 設(shè)置為 NULL。
if (async) {
/* Because of dbUnshareStringValue, the val in de may change. */
freeObjAsync(key, dictGetVal(de), db->id);
dictSetVal(db->dict, de, NULL);
}
// 如果是集群模式,還會(huì)更新對(duì)應(yīng) slot 的相關(guān)信息
if (server.cluster_enabled) slotToKeyDelEntry(de, db);
/* Deleting an entry from the expires dict will not free the sds of the key, because it is shared with the main dictionary. */
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
// 釋放內(nèi)存
dictTwoPhaseUnlinkFree(db->dict,de,plink,table);
return 1;
} else {
return 0;
}
}
如果為異步刪除,調(diào)用freeObjAsync()方法,根據(jù)以下代碼分析:
#define LAZYFREE_THRESHOLD 64
/* Free an object, if the object is huge enough, free it in async way. */
void freeObjAsync(robj *key, robj *obj, int dbid) {
size_t free_effort = lazyfreeGetFreeEffort(key,obj,dbid);
if (free_effort > LAZYFREE_THRESHOLD && obj->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateLazyFreeJob(lazyfreeFreeObject,1,obj);
} else {
decrRefCount(obj);
}
}
size_t lazyfreeGetFreeEffort(robj *key, robj *obj, int dbid) {
if (obj->type == OBJ_LIST && obj->encoding == OBJ_ENCODING_QUICKLIST) {
quicklist *ql = obj->ptr;
return ql->len;
} else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht);
} else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST){
zset *zs = obj->ptr;
return zs->zsl->length;
} else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) {
dict *ht = obj->ptr;
return dictSize(ht);
} else if (obj->type == OBJ_STREAM) {
...
return effort;
} else if (obj->type == OBJ_MODULE) {
size_t effort = moduleGetFreeEffort(key, obj, dbid);
/* If the module's free_effort returns 0, we will use asynchronous free
* memory by default. */
return effort == 0 ? ULONG_MAX : effort;
} else {
return 1; /* Everything else is a single allocation. */
}
}
分析后咱們可以得出如下結(jié)論:
- 當(dāng)Hash/Set底層采用哈希表存儲(chǔ)(非ziplist/int編碼存儲(chǔ))時(shí),并且元素?cái)?shù)量超過(guò)64個(gè)
- 當(dāng)ZSet底層采用跳表存儲(chǔ)(非ziplist編碼存儲(chǔ))時(shí),并且元素?cái)?shù)量超過(guò)64個(gè)
- 當(dāng)List鏈表節(jié)點(diǎn)數(shù)量超過(guò)64個(gè)(注意,不是元素?cái)?shù)量,而是鏈表節(jié)點(diǎn)的數(shù)量,List的實(shí)現(xiàn)是在每個(gè)節(jié)點(diǎn)包含了若干個(gè)元素的數(shù)據(jù),這些元素采用ziplist存儲(chǔ))
- refcount == 1 就是在沒(méi)有引用這個(gè)Key時(shí)
只有以上這些情況,在刪除key釋放內(nèi)存時(shí),才會(huì)真正放到異步線程中執(zhí)行,其他情況一律還是在主線程操作。也就是說(shuō)String(不管內(nèi)存占用多大)、List(少量元素)、Set(int編碼存儲(chǔ))、Hash/ZSet(ziplist編碼存儲(chǔ))這些情況下的key在釋放內(nèi)存時(shí),依舊在主線程中操作。
5.3、分而治之
采用經(jīng)典算法“分治法”,將大而化小。針對(duì)String和集合類(lèi)型的Key,可以采用如下方式:
- String類(lèi)型的大Key:可以嘗試將對(duì)象分拆成幾個(gè)Key-Value, 使用MGET或者多個(gè)GET組成的pipeline獲取值,分拆單次操作的壓力,對(duì)于集群來(lái)說(shuō)可以將操作壓力平攤到多個(gè)分片上,降低對(duì)單個(gè)分片的影響。
- 集合類(lèi)型的大Key,并且需要整存整取要在設(shè)計(jì)上嚴(yán)格禁止這種場(chǎng)景的出現(xiàn),如無(wú)法拆分,有效的方法是將該大Key從JIMDB去除,單獨(dú)放到其他存儲(chǔ)介質(zhì)上。
- 集合類(lèi)型的大Key,每次只需操作部分元素:將集合類(lèi)型中的元素分拆。以Hash類(lèi)型為例,可以在客戶(hù)端定義一個(gè)分拆Key的數(shù)量N,每次對(duì)HGET和HSET操作的field計(jì)算哈希值并取模N,確定該field落在哪個(gè)Key上。
如果線上服務(wù)強(qiáng)依賴(lài)Redis,需要考慮到如何做到“無(wú)感”,并保證數(shù)據(jù)一致性。咱們基本上可以采用三步走策略,如下圖所示。分別是進(jìn)行雙寫(xiě),雙讀校驗(yàn),最后讀新Key。在此基礎(chǔ)上可以設(shè)置開(kāi)關(guān),做到上線后的平穩(wěn)遷移。
六、總結(jié)
綜上所述,針對(duì)文章開(kāi)頭咱們購(gòu)物車(chē)大Key問(wèn)題,相信你已經(jīng)有了答案。咱們可以限制門(mén)店數(shù),限制門(mén)店中的商品數(shù)。如果不作限制,咱們也能進(jìn)行拆分,將大Key分散存儲(chǔ)。例如。將Redis中Key類(lèi)型改為L(zhǎng)ist,key為用戶(hù)與門(mén)店唯一鍵,Value為用戶(hù)在此門(mén)店下的商品。
存儲(chǔ)結(jié)構(gòu)拆分成兩種:
第一種:
userPin:storeId的集合
第二種:
userPin_storeId1:{門(mén)店下加車(chē)的所有商品基本信息};
userPin_storeId2:{門(mén)店下加車(chē)的所有商品基本信息}
以上介紹了大key的產(chǎn)生、識(shí)別、處理,以及如何使用合理策略和技術(shù)來(lái)應(yīng)對(duì)。在使用Redis過(guò)程中,防范大于治理,在治理過(guò)程中也要做到業(yè)務(wù)無(wú)感。
七、參考
https://github.com/redis/redis.git
http://redisbook.com/
https://github.com/huangz1990/redis-3.0-annotated.git
https://blog.csdn.net/ldw201510803006/article/details/124790121
https://blog.csdn.net/kuangd_1992/article/details/130451679
http://sd.jd.com/article/4930?shareId=119428&isHideShareButton=1
https://www.liujiajia.me/2023/3/28/redis-bigkeys
https://www.51cto.com/article/701990.html
https://help.aliyun.com/document_detail/353223.html
https://juejin.cn/post/7167015025154981895
https://www.jianshu.com/p/9e150d72ffc9
https://zhuanlan.zhihu.com/p/449648332
作者:京東零售?高凱文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-737860.html
來(lái)源:京東云開(kāi)發(fā)者社區(qū) 轉(zhuǎn)載請(qǐng)注明來(lái)源文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-737860.html
到了這里,關(guān)于淺析Redis大Key | 京東云技術(shù)團(tuán)隊(duì)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!