Eine kurze Analyse des technischen Teams von Redis Big Key | JD Cloud

1. Hintergrund

Im Warenkorbsystem von JD Daojia können Benutzer Produkte basierend auf dem Geschäft zum Warenkorb hinzufügen. Benutzer und Shop-Produkte werden mithilfe des Hash-Typs von Redis gespeichert, wie im folgenden Codeblock gezeigt. Ist Ihnen aufgefallen, dass der Schlüssel immer größer wird, wenn ein einzelnes Geschäft zu viele Produkte hinzufügt oder es zu viele Geschäfte gibt, was sich negativ auf das Online-Geschäft auswirkt?

userPin:{
      storeId:{门店下加车的所有商品基本信息},
      storeId:{门店下加车的所有商品基本信息},
      ......
}

2. Definition von BigKey und wie es erstellt wurde

2.1. Definition von BigKey

BigKey wird als großer Schlüssel bezeichnet und wird normalerweise umfassend anhand der Speichergröße des dem Wert entsprechenden Schlüssels oder der Nummer des dem Wert entsprechenden Schlüssels beurteilt. Es gibt keine strikte Definitionsunterscheidung für große Schlüssel. Für String- und Nicht-String-Strukturen gelten die folgenden Definitionen:

  • String: Der dem String-Typschlüssel entsprechende Wert überschreitet 10 KB
  • Nicht-String-Struktur (Hash , Set , ZSet , Liste): Die Anzahl der Werte erreicht 10000 oder die Gesamtgröße von Vaule beträgt 100 KB
  • Die Gesamtzahl der Schlüssel im Cluster übersteigt 100 Millionen

2.2. So generieren Sie

1. Die Datenstruktureinstellung ist unangemessen. Wenn beispielsweise das Element in der Sammlung eindeutig ist, sollte Set anstelle von List verwendet werden.

2. Mangelnde Vorhersehbarkeit für das Geschäft und Unfähigkeit, das dynamische Wachstum von Value vorherzusehen;

3. Der Schlüssel legt keine Ablaufzeit fest. Er behandelt den Cache wie einen Mülleimer und wirft ihn immer wieder hinein, verarbeitet ihn jedoch nie.

3. Gefahren von BigKey

3.1. Datenverzerrung

Der Redis-Datenversatz wird in Datenzugriffsversatz und Datenvolumenversatz unterteilt, was dazu führt, dass die CPU-Auslastung und Bandbreitennutzung des Daten-Shard-Knotens, auf dem sich der Schlüssel befindet, zunimmt, was sich auf die Verarbeitung aller Schlüssel auf dem Shard auswirkt.

Datenzugriffsverzerrung: Die QPS eines Schlüssels in einem Knoten ist höher als die des Schlüssels in anderen Knoten

Skew des Datenvolumens: Die Größe des Schlüssels in einem bestimmten Knoten ist größer als die des Schlüssels in anderen Knoten. Wie in der folgenden Abbildung dargestellt, ist der Schlüssel1-Speicher in Instanz 1 höher als in anderen Instanzen.

3.2. Netzwerkblockierung

Der Redis-Server ist ein ereignisgesteuertes Programm mit Dateiereignissen und Zeitereignissen. Dateiereignisse und Zeitereignisse werden vom Hauptthread vervollständigt. Das Dateiereignis ist die Abstraktion der Socket-Operation durch den Server. Die Kommunikation zwischen dem Client und dem Server generiert entsprechende Dateiereignisse. Der Server führt eine Reihe von Netzwerkkommunikationsvorgängen durch, indem er diese Ereignisse überwacht und verarbeitet.

Redis hat einen eigenen Netzwerkereignisprozessor entwickelt, der auf dem Reactor-Modus basiert, nämlich dem Dateiereignisprozessor. Dieser Prozessor verwendet intern einen E/A-Multiplexer, um mehrere Sockets gleichzeitig zu überwachen und Aufgaben basierend auf den Sockets auszuführen. um verschiedene zuzuordnen Ereignishandler. Der Dateiereignishandler läuft im Single-Thread-Modus, lauscht aber über einen I/O-Multiplexer auf mehrere Sockets, was nicht nur ein leistungsstarkes Netzwerkkommunikationsmodell implementiert, sondern auch die Einfachheit des internen Single-Thread-Designs beibehält. Der Datei-Event-Handler setzt sich wie folgt zusammen:

Dateiereignisse sind eine Abstraktion von Socket-Vorgängen, einschließlich Verbindungsantwort, Schreiben, Lesen und Schließen. Da ein Server eine Verbindung zu mehreren Sockets herstellt, können Dateiereignisse gleichzeitig auftreten, auch wenn Dateiereignisse gleichzeitig auftreten, aber ich Der /O-Multiplexer wird gesetzt Stellen Sie den Socket in eine Warteschlange und senden Sie den Socket einen Socket nach dem anderen auf geordnete und synchrone Weise sendet der E/A-Multiplexer weiterhin den nächsten Socket an Der Dateiereignis-Dispatcher. Wenn ein großer Schlüssel vorhanden ist, verlängert sich die Zeit für einen einzelnen Vorgang, was zu einer Überlastung des Netzwerks führt.

3.3. Langsame Abfrage

Es beeinträchtigt Indikatoren wie QPS und TP99 erheblich. Langsame Operationen auf großen Tasten führen dazu, dass nachfolgende Befehle blockiert werden, was zu einer Reihe langsamer Abfragen führt.

3.4. CPU-Auslastung

Wenn ein einzelner Schlüssel zu groß ist, wird Redis möglicherweise bei jedem Zugriff blockiert und andere Anforderungen können nur warten. Wenn in der Anwendung ein Timeout festgelegt ist, löst die obere Schicht eine Ausnahmemeldung aus. Das endgültige Löschen führt auch zu einer Blockierung von Redis. Wenn die Datenmenge im Speicher zu groß ist, ist die CPU-Auslastung zu hoch. Wenn die CPU-Auslastung eines einzelnen Shards zu hoch ist, können andere Shards nicht über CPU-Ressourcen verfügen und sind somit betroffen. Darüber hinaus haben große Schlüssel auch einen gewissen Einfluss auf die Persistenz. Der Fork-Vorgang kopiert den Seitentabelleneintrag des übergeordneten Prozesses. Wenn er zu groß ist, belegt er mehr Seitentabellen und es dauert eine gewisse Zeit, bis der Hauptthread die Kopie blockiert.

4. So erkennen Sie BigKey

4.1、redis-cli --bigkeys

Zunächst beginnen wir mit den Laufergebnissen. Fügen Sie zunächst einige Daten über ein Skript in Redis ein und führen Sie dann die Option --bigkeys von Redis-Cli aus

$ 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 -------

Im Folgenden analysieren wir das Prinzip des Quellcodes der Bigkeys-Option in drei Schritten. Der kurze Prozess ist wie folgt:

4.1.1. Wie finde ich den Schlüssel im ersten Teil?

Die Funktion von Redis zum Finden von Bigkey ist static void findBigKeys(int memkeys, unsigned memkeys_samples). Da die Optionen --memkeys und --bigkeys dieselbe Funktion haben, gibt es bei Verwendung von memkeys zwei zusätzliche Parameter memkeys und memkeys_sample, die jedoch unterschiedlich sind from Die Option --bigkeys spielt keine Rolle, also ignorieren Sie sie. Das spezifische Funktionsgerüst von findBigKeys ist:

1. Beantragen Sie 6 Variablen, um Informationen von 6 Datentypen zu zählen (jede Variable zeichnet die Gesamtzahl der Schlüssel des Datentyps auf, welcher große Schlüssel usw. ist).

typedef struct {
    char *name;//数据类型,如string
    char *sizecmd;//查询大小命令,如string会调用STRLEN
    char *sizeunit;//单位,string类型为bytes,而hash为field
    unsigned long long biggest;//最大key信息域,此数据类型最大key的大小,如string类型是多少bytes,hash为多少field
    unsigned long long count;//统计信息域,此数据类型的key的总数
    unsigned long long totalsize;//统计信息域,此数据类型的key的总大小,如string类型是全部string总共多少bytes,hash为全部hash总共多少field
    sds biggest_key;//最大key信息域,此数据类型最大key的键名,之所以在数据结构末尾是考虑字节对齐
} 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. Rufen Sie den Scan-Befehl auf, um iterativ einen Schlüsselstapel abzurufen (beachten Sie, dass der Scan-Befehl nicht nur den Namen, den Typ und die Größe des Schlüssels zurückgibt).

/* scan循环扫描 */
do {
    /* 计算完成的百分比情况 */
    pct = 100 * (double)sampled/total_keys;//这里记录下扫描的进度

    /* 获取一些键并指向键数组 */
    reply = sendScan(&it);//这里发送SCAN命令,结果保存在reply中
    keys  = reply->element[1];//keys来保存这次scan获取的所有键名,注意只是键名,每个键的数据类型是不知道的。
    ......

} while(it != 0); 

3. Ermitteln Sie den Datentyp (Typ) und die Schlüsselgröße (Größe) für jeden Schlüssel

/* 检索类型,然后检索大小*/
getKeyTypes(types_dict, keys, types);
getKeySizes(keys, types, sizes, memkeys, memkeys_samples);

4. Wenn die Größe des Schlüssels größer als der aufgezeichnete maximale Schlüssel ist, aktualisieren Sie die Informationen des maximalen Schlüssels

/* Now update our stats */
for(i=0;i<keys->elements;i++) {
    ......//前面已解析

    //如果遍历到比记录值更大的key时
    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);
        }

        //每当找到一个更大的key时则输出该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. Aktualisieren Sie die Statistiken des entsprechenden Datentyps für jeden Schlüssel

/* 现在更新统计数据 */
for(i=0;i<keys->elements;i++) {
    typeinfo *type = types[i];
    /* 跳过在SCAN和TYPE之间消失的键 */
    if(!type)
        continue;

    //对每个key更新每种数据类型的统计信息
    type->totalsize += sizes[i];//某数据类型(如string)的总大小增加
    type->count++;//某数据类型的key数量增加
    totlen += keys->element[i]->len;//totlen不针对某个具体数据类型,将所有key的键名的长度进行统计,注意只统计键名长度。
    sampled++;//已经遍历的key数量

    ......//后续解析

    /* 更新整体进度 */
    if(sampled % 1000000 == 0) {
        printf("[%05.2f%%] Sampled %llu keys so far\n", pct, sampled);
    }
}

4.1.2. Wie wird der zweite Teil durchgeführt?

1. Geben Sie statistische Informationen und maximale Schlüsselinformationen aus

   /* 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. Geben Sie zunächst die Gesamtzahl der gescannten Schlüssel und die Gesamtlänge aller Schlüssel aus.

/* 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. Wie wird der dritte Teil ausgeführt?

di ist ein Wörterbuch-Iterator, der zum Durchlaufen aller dictEntry intypes_dict verwendet wird. de = dictNext(di) kann den nächsten dictEntry abrufen, de ist der Zeiger auf dictEntry. Und da die Typeinfo-Struktur im v-Feld von dictEntry gespeichert ist, wird sie mit dictGetVal abgerufen. Anschließend werden die Daten ausgegeben, die sich auf den größten in der Typeinfo-Struktur gespeicherten Schlüssel beziehen, einschließlich des Schlüsselnamens und der Größe des größten Schlüssels.

  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. Verwenden Sie Open-Source-Tools, um wichtige Schlüssel zu entdecken

Erhalten Sie genaue Analyseberichte, ohne die Online-Dienste zu beeinträchtigen. Verwenden Sie das Tool redis-rdb-tools, um den großen Schlüssel auf benutzerdefinierte Weise zu finden. Dieses Tool kann eine benutzerdefinierte Analyse von Redis-RDB-Dateien durchführen. Da die Analyse von RDB-Dateien jedoch offline funktioniert, hat sie keine Auswirkungen auf Online-Dienste. Dies ist ihr größter Vorteil, aber auch ihr größter Nachteil: Offline-Analysen bedeuten eine schlechte Aktualität der Analyseergebnisse. Bei einer größeren RDB-Datei kann die Analyse sehr lange dauern.

Die Projektadresse von redis-rdb-tools lautet: https://github.com/sripathikrishnan/redis-rdb-tools

5. So lösen Sie Bigkey

5.1. Frühzeitige Prävention

  • Legen Sie die Ablaufzeit fest und versuchen Sie, die Ablaufzeit so weit wie möglich zu verteilen, um zu verhindern, dass sie gleichzeitig abläuft.
  • Als JSON vom Typ String gespeichert, können nicht verwendete Felder gelöscht werden;

Das Objekt lautet beispielsweise {"userName": "Jingdong Daojia", "ciyt": "Beijing"} . Wenn Sie nur das Attribut "userName" verwenden müssen, definieren Sie ein neues Objekt nur mit dem Attribut "userName", um die Daten zu optimieren der Cache.

  • Als JSON vom Typ String gespeichert, verwenden Sie die Annotation @JsonProperty, um den FiledName-Zeichensatz zu reduzieren. Das Codebeispiel lautet wie folgt. Allerdings besteht der Nachteil einer geringen Erkennbarkeit der zwischengespeicherten Daten;
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":"京东到家"}
京东到家

  • Mithilfe eines Komprimierungsalgorithmus wird Zeit genutzt, um Speicherplatz für Serialisierung und Deserialisierung auszutauschen. Gleichzeitig besteht aber auch der Nachteil einer geringen Erkennbarkeit der zwischengespeicherten Daten;
  • In das Geschäft eingreifen und Schwellenwerte festlegen. Beispielsweise kann die Anzahl der Produkte im Warenkorb des Nutzers oder die Anzahl der Coupons nicht unbegrenzt erhöht werden;

5.2. So löschen Sie BigKey ordnungsgemäß

5.2.1、DEL

Der Löschmechanismus dieses Befehls unterscheidet sich in den verschiedenen Versionen von Redis. Sie werden im Folgenden separat analysiert:

redis_version < 4.0 Version : Synchronisiertes Löschen im Hauptthread. Durch das Löschen eines großen Schlüssels wird der Hauptthread blockiert. Siehe den folgenden Quellcode basierend auf Redis Version 3.0. Bei Nicht-String-Strukturdaten können Sie zunächst einen Teil der Daten über den SCAN-Befehl lesen und ihn dann Schritt für Schritt löschen, um eine Redis-Blockierung aufgrund des einmaligen Löschens großer Schlüssel zu vermeiden.

// 从数据库中删除给定的键,键的值,以及键的过期时间。
// 删除成功返回 1,因为键不存在而导致删除失败时,返回 0 
int dbDelete(redisDb *db, robj *key) {
    // 删除键的过期时间
    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    // 删除键值对
    if (dictDelete(db->dict,key->ptr) == DICT_OK) {
        // 如果开启了集群模式,那么从槽中删除给定的键
        if (server.cluster_enabled) slotToKeyDel(key);
        return 1;
    } else {
        // 键不存在
        return 0;
    }
}

Version 4.0 < redis_version < Version 6.0 : Lazy-Free wird eingeführt . Wenn Lazy-Free manuell aktiviert wird, gibt es 4 zu steuernde Optionen, je nachdem, ob der asynchrone Speicherfreigabemechanismus in verschiedenen Szenarien aktiviert werden soll:

  • lazyfree-lazy-expire: Der Schlüssel versucht, Speicher asynchron freizugeben, wenn er abläuft und gelöscht wird
  • lazyfree-lazy-eviction: Versuchen Sie, Speicher asynchron freizugeben, wenn der Speicher maxmemory erreicht und die Eliminierungsstrategie festgelegt ist.
  • lazyfree-lazy-server-del: Wenn Sie Befehle wie RENAME/MOVE ausführen oder einen Schlüssel überschreiben müssen, löschen Sie den alten Schlüssel und versuchen Sie, Speicher asynchron freizugeben
  • Replica-Lazy-Flush: Master-Slave ist vollständig synchronisiert und der Speicher wird asynchron freigegeben, wenn der Slave die Datenbank löscht.

Nachdem Lazy-Free aktiviert wurde, bewertet Redis zunächst die Kosten für die Freigabe des Speichers eines Schlüssels. Wenn die Kosten für die Freigabe des Speichers sehr gering sind, kann er direkt im Hauptthread ausgeführt werden. Es ist keine Ausführung erforderlich es in einem asynchronen Thread.

redis_version >= 6.0 Version : lazyfree-lazy-user-del wird eingeführt . Solange es aktiviert ist, kann del Schlüssel direkt asynchron löschen, ohne den Hauptthread zu blockieren. Warum das so ist, wollen wir zunächst einmal aufschlüsseln und im Folgenden analysieren.

5.2.2、SCANNEN

Der SCAN-Befehl kann dabei helfen, eine große Anzahl von Schlüsseln zu durchlaufen, ohne den Hauptthread zu blockieren, und eine Blockierung der Datenbank zu vermeiden. Der folgende Code verwendet scan, um die Schlüssel im Cluster zu scannen.

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、VERBINDUNG ENTFERNEN

Redis 4.0 bietet verzögertes Löschen (Befehl zum Aufheben der Verknüpfung). Das Folgende ist eine Analyse des Implementierungsprinzips basierend auf dem Quellcode (redis_version: Version 7.2).

  • Die Methode delGenericCommand() wird am Ende der Befehle del und unlink aufgerufen.
void delCommand(client *c) {
    delGenericCommand(c,server.lazyfree_lazy_user_del);
}
void unlinkCommand(client *c) {
    delGenericCommand(c,1);
}

  • lazyfree-lazy-user-del unterstützt Ja oder Nein. Der Standardwert ist „Nein“;
  • Wenn auf „Ja“ gesetzt, entspricht der Befehl „del“ dem Aufheben der Verknüpfung, was ebenfalls ein asynchrones Löschen darstellt. Dies erklärt auch unser vorheriges Problem, warum der Befehl „del“ nach dem Festlegen von lazyfree-lazy-user-del ein asynchrones Löschen ist.
void delGenericCommand(client *c, int lazy) {
    int numdel = 0, j;
    // 遍历所有输入键
    for (j = 1; j < c->argc; j++) {
        // 先删除过期的键
        expireIfNeeded(c->db,c->argv[j],0);
        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
                              dbSyncDelete(c->db,c->argv[j]);
        // 尝试删除键
        if (deleted) {
            // 删除键成功,发送通知
            signalModifiedKey(c,c->db,c->argv[j]);
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[j],c->db->id);
            server.dirty++;
            // 成功删除才增加 deleted 计数器的值
            numdel++;
        }
    }
    // 返回被删除键的数量
    addReplyLongLong(c,numdel);
}

Im Folgenden wird das asynchrone Löschen von dbAsyncDelete() und das synchrone Löschen von dbSyncDelete() analysiert. Die unterste Ebene ruft auch die Methode dbGenericDelete() auf.

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);
        // 在调用用freeObjAsync之前,我们应该先调用decrRefCount。否则,引用计数可能大于1,导致freeObjAsync无法正常工作。
        decrRefCount(val);
        // 如果是异步删除,则会调用 freeObjAsync 异步释放 value 占用的内存。同时,将 key 对应的 value 设置为 NULL。
        if (async) {
            /* Because of dbUnshareStringValue, the val in de may change. */
            freeObjAsync(key, dictGetVal(de), db->id);
            dictSetVal(db->dict, de, NULL);
        }
        // 如果是集群模式,还会更新对应 slot 的相关信息
        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);
        // 释放内存
        dictTwoPhaseUnlinkFree(db->dict,de,plink,table);
        return 1;
    } else {
        return 0;
    }
}

Wenn es sich um eine asynchrone Löschung handelt, rufen Sie die Methode freeObjAsync() auf und analysieren Sie sie gemäß dem folgenden Code:

#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. */
    }
}

Nach der Analyse können wir folgende Schlussfolgerungen ziehen:

  • Wenn die unterste Ebene von Hash/Set einen Hash-Tabellenspeicher verwendet (keinen Ziplist/Int-Codierungsspeicher) und die Anzahl der Elemente 64 überschreitet
  • Wenn die unterste Ebene von ZSet Sprunglistenspeicher (Nicht-Ziplist-Kodierungsspeicher) verwendet und die Anzahl der Elemente 64 überschreitet
  • Wenn die Anzahl der Listenknoten 64 überschreitet (beachten Sie, dass es sich nicht um die Anzahl der Elemente, sondern um die Anzahl der verknüpften Listenknoten handelt. Die Implementierung von List besteht darin, dass jeder Knoten Daten mehrerer Elemente enthält und diese Elemente in der Ziplist gespeichert werden.)
  • refcount == 1 ist, wenn auf diesen Schlüssel nicht verwiesen wird

Nur in den oben genannten Fällen wird der Schlüssel tatsächlich im asynchronen Thread ausgeführt, wenn er gelöscht wird, um den Speicher freizugeben. In anderen Fällen wird er weiterhin im Hauptthread ausgeführt. Das heißt, die Schlüssel in String (unabhängig davon, wie viel Speicher belegt ist), List (eine kleine Anzahl von Elementen), Set (Int-Codierungsspeicher) und Hash/ZSet (Ziplist-Codierungsspeicher) werden weiterhin im Hauptthread betrieben wenn der Speicher freigegeben wird.

5.3. Teile und herrsche

Nutzen Sie den klassischen Algorithmus „Teile und herrsche“, um das Große auf das Kleine zu reduzieren. Für Schlüssel vom Typ String und Sammlungstypen können die folgenden Methoden verwendet werden:

  • Großer Schlüssel vom Typ String: Sie können versuchen, das Objekt in mehrere Schlüsselwerte aufzuteilen, MGET oder eine Pipeline aus mehreren GETs verwenden, um den Wert zu erhalten, und den Druck einer einzelnen Operation aufteilen. Für den Cluster kann der Operationsdruck gleichmäßig verteilt werden. Reduzieren Sie auf mehreren Shards die Auswirkungen auf einen einzelnen Shard.
  • Bei großen Schlüsseln eines Sammlungstyps, die als Ganzes gespeichert und abgerufen werden müssen, muss dieses Szenario im Entwurf strikt verboten werden. Wenn er nicht aufgeteilt werden kann, besteht eine wirksame Methode darin, den großen Schlüssel aus JIMDB zu entfernen und ihn separat auf anderen zu platzieren Speichermedium.
  • Der große Schlüssel des Sammlungstyps muss jedes Mal nur einen Teil der Elemente bearbeiten: Teilen Sie die Elemente im Sammlungstyp auf. Am Beispiel des Hash-Typs können Sie eine Anzahl N geteilter Schlüssel auf dem Client definieren, den Hash-Wert für jedes Feld in den HGET- und HSET-Operationen berechnen und Modulo N verwenden, um zu bestimmen, auf welchen Schlüssel das Feld fällt.

Wenn Online-Dienste stark auf Redis angewiesen sind, müssen Sie überlegen, wie Sie „unempfindlich“ sein und die Datenkonsistenz sicherstellen können. Grundsätzlich können wir eine dreistufige Strategie anwenden, wie in der folgenden Abbildung dargestellt. Sie müssen doppeltes Schreiben, doppeltes Lesen und Überprüfen durchführen und schließlich den neuen Schlüssel lesen. Auf dieser Basis können Schalter eingestellt werden, um eine reibungslose Migration nach dem Online-Gehen zu erreichen.

6. Zusammenfassung

Zusammenfassend glaube ich, dass Sie die Antwort auf unsere Warenkorb-Schlüsselfrage am Anfang des Artikels bereits haben. Wir können die Anzahl der Geschäfte und die Anzahl der Produkte im Geschäft begrenzen. Wenn es keine Einschränkungen gibt, können wir auch große Schlüssel aufteilen und verteilt speichern. Zum Beispiel. Ändern Sie den Schlüsseltyp in Redis in „Liste“. Der Schlüssel ist der eindeutige Schlüssel zwischen dem Benutzer und dem Geschäft und der Wert ist das Produkt, das der Benutzer in diesem Geschäft hat.

存储结构拆分成两种:
第一种:
    userPin:storeId的集合
第二种:
    userPin_storeId1:{门店下加车的所有商品基本信息};
    userPin_storeId2:{门店下加车的所有商品基本信息}     

Das Obige führt in die Generierung, Identifizierung und Verarbeitung großer Schlüssel sowie in die Verwendung angemessener Strategien und Techniken für den Umgang mit ihnen ein. Bei der Verwendung von Redis ist Prävention wichtiger als Governance, und während des Governance-Prozesses muss auch geschäftliche Unempfindlichkeit erreicht werden.

7. Referenz

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

Autor: JD Retail Gao Kai

Quelle: JD Cloud Developer Community Bitte geben Sie beim Nachdruck die Quelle an

 

Alibaba Cloud erlitt einen schwerwiegenden Ausfall und alle Produkte waren betroffen (wiederhergestellt). Tumblr hat das russische Betriebssystem Aurora OS 5.0 abgekühlt . Neue Benutzeroberfläche vorgestellt : Delphi 12 & C++ Builder 12, RAD Studio 12. Viele Internetunternehmen stellen dringend Hongmeng-Programmierer ein. UNIX-Zeit steht kurz vor dem Eintritt in die 1,7-Milliarden-Ära (bereits eingetreten). Meituan rekrutiert Truppen und plant die Entwicklung der Hongmeng-System-App. Amazon entwickelt ein Linux-basiertes Betriebssystem, um die Abhängigkeit von Android von .NET 8 unter Linux zu beseitigen. Die unabhängige Größe ist um 50 % reduziert. FFmpeg 6.1 „Heaviside“ ist erschienen
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10139889