Maison  >  Article  >  base de données  >  Comparaison des implémentations de Memcached et Redis

Comparaison des implémentations de Memcached et Redis

巴扎黑
巴扎黑original
2017-03-19 16:52:31909parcourir

Memcached et Redis, en tant que serveurs de cache les plus couramment utilisés ces dernières années, je pense que tout le monde les connaît. Quand j'étais encore à l'école il y a deux ans, j'ai lu leurs principaux codes sources. J'écris maintenant une note pour comparer brièvement leurs méthodes de mise en œuvre d'un point de vue personnel, je peux simplement l'utiliser comme critique. compréhension, j’apprécie les corrections.

La plupart des images d'architecture utilisées dans cet article proviennent d'Internet. Certaines images sont différentes de la dernière implémentation, soulignée dans l'article.

1. Aperçu

Pour lire le code source d’un logiciel, vous devez d’abord comprendre à quoi sert le logiciel. A quoi servent Memcached et Redis ? Comme nous le savons tous, les données sont généralement placées dans une base de données, mais l'interrogation des données est relativement lente. Surtout lorsqu'il y a de nombreux utilisateurs, les requêtes fréquentes prennent beaucoup de temps. Ce qu'il faut faire? Où sont placées les données pour pouvoir être interrogées rapidement ? Cela doit être en mémoire. Memcached et Redis stockent les données en mémoire et les interrogent de manière clé-valeur, ce qui peut grandement améliorer l'efficacité. Par conséquent, ils sont généralement utilisés comme serveurs de cache pour mettre en cache les données couramment utilisées, lorsque des requêtes sont nécessaires, elles sont obtenues directement à partir d'eux, ce qui réduit le nombre de requêtes de base de données et améliore l'efficacité des requêtes.

2. Méthode de service

Comment Memcached et Redis fournissent-ils des services ? Ce sont des processus indépendants. Si nécessaire, ils peuvent être transformés en processus démons. Par conséquent, si notre processus utilisateur souhaite utiliser les services memcached et redis, une communication inter-processus est requise. Étant donné que le processus utilisateur, Memcached et Redis ne se trouvent pas nécessairement sur la même machine, la communication inter-réseau doit être prise en charge. Par conséquent, Memcached et Redis eux-mêmes sont des serveurs réseau, et les processus utilisateur transmettent des données via le réseau avec eux. Évidemment, le plus simple et le plus couramment utilisé consiste à utiliser une connexion TCP. De plus, Memcached et Redis prennent en charge le protocole UDP. Et lorsque le processus utilisateur se trouve sur la même machine que Memcached et Redis, la communication par socket de domaine Unix peut également être utilisée.

3. Modèle d'événement

Commençons par comment ils le mettent en œuvre. Tout d’abord, jetons un coup d’œil à leurs modèles d’événements.

Depuis la sortie d'epoll, presque tous les serveurs réseau ont abandonné select et poll et les ont remplacés par epoll. La même chose est vraie pour Redis, sauf qu'il prend également en charge la sélection et l'interrogation. Vous pouvez configurer lequel utiliser, mais epoll est généralement utilisé. De plus, pour BSD, l'utilisation de kqueue est également prise en charge. Memcached est basé sur libevent, mais la couche inférieure de libevent utilise également epoll, on peut donc considérer qu'ils utilisent tous epoll. Les caractéristiques d'epoll ne seront pas présentées ici. Il existe de nombreux articles d'introduction en ligne.

Ils utilisent tous epoll pour la boucle d'événements, mais redis est un serveur monothread (redis est également multi-thread, mais à l'exception du thread principal, les autres threads n'ont pas de boucles d'événements, mais effectuent uniquement un travail de stockage en arrière-plan), tandis que memcached est multithread de. Le modèle d'événement de Redis est très simple, avec une seule boucle d'événement, qui est une simple implémentation de réacteur. Cependant, il y a un point positif dans le modèle d'événement redis. Nous savons qu'epoll est pour fd, et l'événement ready qu'il renvoie est uniquement fd. Le fd dans redis est le fd du socket connectant le serveur et le client, mais quand. le traitement, il doit être basé sur ce fd Comment trouver des informations spécifiques sur un client ? La méthode de traitement habituelle consiste à utiliser un arbre rouge-noir pour enregistrer les informations fd et client, et effectuer une recherche dans fd, et l'efficacité est lgn. Cependant, redis est spécial.La limite supérieure du nombre de clients redis peut être définie, c'est-à-dire que la limite supérieure du fd ouvert par redis peut être connue en même temps.Et nous savons que le fd du processus ne le sera pas. être répété en même temps (le fd ne peut être restauré qu'après sa fermeture). (), donc redis utilise un tableau et utilise fd comme indice du tableau. Les éléments du tableau sont ainsi les informations du client. , les informations client peuvent être localisées directement via fd. L'efficacité de la recherche est O(1), ce qui élimine également la complexité de l'implémentation de l'arborescence rouge-noir (j'ai déjà utilisé C pour écrire un serveur réseau, car je voulais maintenir le serveur réseau). relation correspondante entre fd et connect, et je ne voulais pas écrire l'arbre rouge-noir moi-même, puis j'ai utilisé l'ensemble en STL, ce qui a fait que le projet est devenu c. Au final, le projet est compilé en utilisant g, qui sait si je le fais. je ne te le dis pas ?) Évidemment, cette méthode ne peut être utilisée que pour les serveurs réseau où la limite supérieure du nombre de connexions a été déterminée et n'est pas trop grande. Les serveurs HTTP comme nginx ne conviennent pas. nginx écrit simplement son propre arbre rouge-noir.

Memcached est multithread, utilisant la méthode master-worker. Le thread principal écoute le port, établit une connexion, puis l'attribue séquentiellement à chaque thread de travail. Chaque thread esclave possède une boucle d'événements qui dessert différents clients. La communication par pipeline est utilisée entre le thread maître et le thread de travail. Chaque thread de travail créera un canal, puis enregistrera la fin d'écriture et la fin de lecture, et ajoutera la fin de lecture à la boucle d'événements pour écouter les événements lisibles. En même temps, chaque thread esclave dispose d'une file d'attente de connexion prête.Une fois que le thread principal s'est connecté, il place l'élément connecté dans cette file d'attente, puis écrit une commande de connexion à l'extrémité d'écriture du tube du thread, afin que le tube ajouté soit ajouté. la boucle d'événements La fin de lecture sera prête, lira la commande à partir du thread, analysera la commande et constatera qu'il existe une connexion, puis elle obtiendra la connexion à partir de sa propre file d'attente prête et la traitera. L'avantage du multi-threading est qu'il peut exploiter pleinement les avantages du multi-core, mais l'écriture d'un programme est un peu plus compliquée. Memcached comporte divers verrous et variables de condition pour la synchronisation des threads.

4. Allocation de mémoire

Les tâches principales de Memcached et Redis sont d'exploiter les données en mémoire, et la gestion de la mémoire est naturellement le contenu principal.

Regardez d’abord comment ils allouent la mémoire. Memcached possède son propre pool de mémoire, ce qui signifie qu'il alloue un gros bloc de mémoire à l'avance, puis alloue la mémoire du pool de mémoire. Cela peut réduire le nombre d'allocations de mémoire et améliorer l'efficacité. C'est également ainsi que fonctionnent la plupart des serveurs réseau. mis en œuvre Cependant, les méthodes de gestion de chaque pool de mémoire varient en fonction des circonstances spécifiques. Redis ne dispose pas de son propre pool de mémoire, mais l'alloue directement lorsqu'il est utilisé, c'est-à-dire l'alloue en cas de besoin. La gestion de la mémoire est laissée au noyau, et il est uniquement responsable de la récupération et de la libération (redis est monothread et le fait. n'a pas son propre pool de mémoire. L'implémentation vous semble-t-elle trop simple ? C'est parce qu'elle se concentre sur le module de base de données). Cependant, Redis prend en charge l'utilisation de tcmalloc pour remplacer le malloc de la glibc. Le premier est un produit Google et est plus rapide que le malloc de la glibc.

Étant donné que Redis n'a pas son propre pool de mémoire, la gestion de l'application et de la libération de la mémoire est beaucoup plus simple. Il suffit de malloc et de free pour être effectué directement, ce qui est très pratique. Memcached prend en charge le pool de mémoire, donc l'application mémoire est obtenue à partir du pool de mémoire, et free est également renvoyée dans le pool de mémoire, cela nécessite donc de nombreuses opérations de gestion supplémentaires et est très difficile à mettre en œuvre. Les détails seront expliqués plus loin. le mécanisme de dalle de memcached analyse.

5. Implémentation de la base de données

Examinons ensuite leur contenu principal, la mise en œuvre de leurs bases de données respectives.

1. Implémentation de la base de données Memcached

Memcached ne prend en charge que la clé-valeur, c'est-à-dire qu'une seule clé peut correspondre à une valeur. Ses données sont également stockées dans des paires clé-valeur en mémoire et utilisent le mécanisme de dalle.

Voyons d’abord comment memcached stocke les données, c’est-à-dire qu’il stocke les paires clé-valeur. Comme le montre la figure ci-dessous, chaque paire clé-valeur est stockée dans une structure d'éléments, comprenant les attributs associés et les valeurs clé et valeur.

Les éléments stockent des paires clé-valeur Lorsqu’il existe de nombreux éléments, la recherche d’éléments spécifiques pose problème. Memcached gère donc une table de hachage, utilisée pour rechercher rapidement des éléments. La table de hachage applique la méthode de chaîne ouverte (la même que Redis) pour résoudre les conflits de clés. Chaque compartiment de table de hachage stocke une liste chaînée est le pointeur de l'élément. Comme le montre la figure ci-dessus, h_next fait référence à l'élément. nœud suivant de la liste chaînée dans le bucket. La table de hachage prend en charge l'expansion (expansion lorsque le nombre d'éléments est supérieur à 1,5 du nombre de compartiments). Il existe une table_hashtable primaire et une table_hashtable primaire. Primary_hashtable est défini sur la table de hachage nouvellement appliquée (multipliez le nombre de compartiments par 2), puis déplacez les données de l'ancienne_hashtable vers la nouvelle table de hachage dans l'ordre, et utilisez une variable expand_bucket pour enregistrer le nombre de compartiments. déplacé. Une fois le déplacement terminé, libérez l'old_hashtable d'origine (Redis a également deux tables de hachage, qui sont également déplacées, mais cela n'est pas fait par le thread d'arrière-plan, mais déplace un compartiment à la fois). L'opération d'expansion est complétée par un thread d'expansion en arrière-plan. Lorsqu'une expansion est nécessaire, des variables de condition sont utilisées pour la notifier. Une fois l'expansion terminée, elle bloque la variable de condition en attente d'expansion. De cette façon, lors du développement, la recherche d'un élément peut se faire soit dans Primary_hashtable, soit dans old_hashtable. Vous devez comparer la position de son bucket et la taille de expand_bucket pour déterminer dans quelle table il se trouve.

D'où l'élément est-il attribué ? de la dalle. Comme le montre la figure ci-dessous, memcached possède de nombreuses classes de dalles, qui gèrent les dalles. Chaque dalle est en fait une collection de troncs. Les éléments réels sont alloués dans le tronc, et un seul élément est attribué à un tronc. La taille du coffre dans une dalle est la même. Pour différentes dalles, la taille du coffre augmente proportionnellement. Lorsque vous devez demander un nouvel article, sélectionnez le coffre en fonction de sa taille. plus grand que lui. De cette manière, les éléments de différentes tailles sont répartis dans différentes dalles et gérés par différentes classes de dalles. L'inconvénient est qu'une partie de la mémoire sera gaspillée, car un tronc peut être plus grand que l'élément, comme le montre la figure 2. Lors de l'allocation d'un élément de 100 B, sélectionnez le tronc 112, mais il y aura une perte de 12B, et cela une partie de la ressource mémoire n'est pas utilisée.



Comme le montre l'image ci-dessus, la structure entière est comme ceci. slabclass gère les dalles. Une slabclass a une slab_list, qui peut gérer plusieurs dalles. Les tailles de tronc des dalles dans la même slabclass sont toutes les mêmes. slabclass a un emplacement de pointeur, qui enregistre les éléments non alloués qui ont été libérés (pas vraiment de mémoire libre, mais qui n'est plus utilisée). Lorsqu'il y a des éléments qui ne sont pas utilisés, ils sont placés dans la tête de l'emplacement, de sorte que chaque élément ne soit pas utilisé. moment où ils doivent l'être. Lors de l'allocation d'un élément dans la dalle actuelle, vous pouvez accéder directement à l'emplacement, que l'élément ait été non alloué ou libéré.

Ensuite, chaque slabclass correspond à une liste chaînée, avec un tableau de tête et un tableau de queue, qui stockent respectivement le nœud de tête et le nœud de queue de la liste chaînée. Les nœuds de la liste chaînée sont les éléments alloués par la classe de dalle modifiée. Les éléments nouvellement alloués sont placés en tête. Les éléments plus en arrière dans la liste chaînée signifient qu'ils n'ont pas été utilisés depuis longtemps. Lorsque la slabclass n'a pas suffisamment de mémoire et doit supprimer certains éléments expirés, elle peut être supprimée de la fin de la liste chaînée. Oui, cette liste chaînée est conçue pour implémenter LRU. Il ne suffit pas de s'appuyer uniquement sur lui, car la requête de la liste chaînée est O(n), donc lors de la localisation de l'élément, utilisez la table de hachage. Elle est déjà disponible. Tous les éléments alloués sont déjà dans la table de hachage, donc le. hash est utilisé pour trouver l'élément, puis la liste chaînée peut stocker l'ordre des éléments le plus récemment utilisé, ce qui est également la méthode d'implémentation standard de lru.

Chaque fois que vous devez attribuer un nouvel élément, recherchez la liste chaînée correspondant à la classe de dalle et recherchez de la fin vers l'avant pour voir si l'élément a expiré. S'il a expiré, utilisez l'élément expiré directement comme nouvel élément. S'il n'est pas expiré, vous devez allouer le tronc de la dalle. Si la dalle est épuisée, vous devez ajouter une dalle à la classe de dalle.

Memcached prend en charge la définition du délai d'expiration, c'est-à-dire le délai d'expiration, mais il ne vérifie pas régulièrement si les données ont expiré en interne. Au lieu de cela, lorsque le processus client utilise les données, memcached vérifiera le délai d'expiration et, s'il expire, il le fera. renvoie directement une erreur. L'avantage est qu'aucun processeur supplémentaire n'est nécessaire pour vérifier le délai d'expiration. L'inconvénient est que les données expirées risquent de ne pas être utilisées pendant une longue période et ne seront pas libérées et n'occuperont pas de mémoire.

Memcached est multithread et ne gère qu'une seule base de données. Plusieurs processus clients peuvent donc fonctionner sur les mêmes données, ce qui peut entraîner des problèmes. Par exemple, A a modifié les données, puis B a également modifié les données, puis l'opération de A sera écrasée et A peut ne pas savoir que l'état actuel des données de la tâche de A est la valeur après qu'il l'a modifiée, cela peut donc créer des problèmes. . Afin de résoudre ce problème, memcached utilise le protocole CAS. En termes simples, l'élément enregistre une valeur int non signée de 64 bits pour marquer la version des données, chaque fois qu'elle est mise à jour (la valeur des données est modifiée), le numéro de version. augmente, puis les données sont modifiées à chaque fois. Pour fonctionner, vous devez comparer si le numéro de version envoyé par le processus client est cohérent avec le numéro de version de l'élément côté serveur. S'ils sont cohérents, vous pouvez effectuer une. changez l'opération, sinon cela entraînera des données sales.

Ce qui précède est une introduction à la façon dont memcached implémente une base de données clé-valeur.

2. Implémentation de la base de données Redis

Tout d'abord, la base de données Redis est plus puissante, car contrairement à Memcached, qui ne prend en charge que l'enregistrement des chaînes, Redis prend en charge cinq structures de données : chaîne, liste, ensemble, ensemble trié et table de hachage. Par exemple, pour stocker les informations d'une personne, vous pouvez utiliser une table de hachage, utiliser le nom de la personne comme clé, puis nommer super, 24 ans. Grâce à la clé et au nom, vous pouvez obtenir le nom super, ou via la clé et l'âge, vous pouvez obtenir l'âge de 24 ans. De cette façon, lorsque vous avez seulement besoin d'obtenir l'âge, vous n'avez pas besoin de récupérer toutes les informations de la personne, puis de rechercher l'âge de l'intérieur, et d'obtenir simplement l'âge directement, ce qui est efficace et pratique.

Afin d'implémenter ces structures de données, redis définit l'objet abstrait redis, comme indiqué ci-dessous. Chaque objet a un type, un total de 5 types : chaîne, liste chaînée, ensemble, ensemble ordonné, table de hachage. Dans le même temps, afin d'améliorer l'efficacité, redis prépare plusieurs méthodes d'implémentation pour chaque type et choisit la méthode d'implémentation appropriée en fonction de scénarios spécifiques. L'encodage représente la méthode d'implémentation de l'objet. Ensuite, le LRU de l'objet est enregistré, c'est-à-dire la dernière fois qu'il a été consulté. En même temps, une heure actuelle est enregistrée sur le serveur Redis (valeur approximative, car cette heure n'est mise à jour qu'à certains intervalles lorsque le serveur exécute. maintenance automatique). La différence entre les deux peut être utilisée pour calculer la durée pendant laquelle l'objet n'a pas été accédé. Ensuite, il y a également un comptage de références dans l'objet redis, qui est utilisé pour partager l'objet et déterminer l'heure de suppression de l'objet. Enfin, un pointeur void* est utilisé pour pointer vers le contenu réel de l'objet. Officiellement, grâce à l'utilisation d'objets redis abstraits, il est beaucoup plus pratique d'exploiter les données dans la base de données. Tous les objets redis peuvent être utilisés de manière uniforme. Lorsqu'il est nécessaire de distinguer les types d'objets, jugez en fonction du type. Et officiellement en raison de l'adoption de cette approche orientée objet, le code redis ressemble beaucoup au code C, mais en fait tout est écrit en C.

//#define REDIS_STRING 0    // 字符串类型
//#define REDIS_LIST 1        // 链表类型
//#define REDIS_SET 2        // 集合类型(无序的),可以求差集,并集等
//#define REDIS_ZSET 3        // 有序的集合类型
//#define REDIS_HASH 4        // 哈希类型

//#define REDIS_ENCODING_RAW 0     /* Raw representation */ //raw  未加工
//#define REDIS_ENCODING_INT 1     /* Encoded as integer */
//#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
//#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
//#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
//#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
//#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
//#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
//#define REDIS_ENCODING_EMBSTR 8  /* Embedded sds 
                                                                     string encoding */

typedef struct redisObject {
    unsigned type:4;            // 对象的类型,包括 /* Object types */
    unsigned encoding:4;        // 底部为了节省空间,一种type的数据,
                                                // 可   以采用不同的存储方式
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;         // 引用计数
    void *ptr;
} robj;

Après tout, redis est toujours une base de données clé-valeur, quel que soit le nombre de structures de données qu'il prend en charge, le stockage final est toujours en mode clé-valeur, mais la valeur peut être une liste chaînée, un ensemble, un ensemble trié, une table de hachage, etc. . Comme Memcached, toutes les clés sont des chaînes et les chaînes sont également utilisées pour un stockage spécifique tel qu'un ensemble, un ensemble trié, une table de hachage, etc. Il n'y a pas de chaîne prête à l'emploi en C, donc la première tâche de redis est d'implémenter une chaîne, nommée sds (simple chaîne dynamique). Le code suivant est une structure très simple len stocke la longueur totale de la mémoire en mémoire. free signifie qu'il est toujours disponible. Combien d'octets sont inutilisés et buf stocke des données spécifiques. Évidemment, len-free est la longueur actuelle de la chaîne.

struct sdshdr {
    int len;
    int free;
    char buf[];
};

字符串解决了,所有的key都存成sds就行了,那么key和value怎么关联呢?key-value的格式在脚本语言中很好处理,直接使用字典即可,C没有字典,怎么办呢?自己写一个呗(redis十分热衷于造轮子)。看下面的代码,privdata存额外信息,用的很少,至少我们发现。 dictht是具体的哈希表,一个dict对应两张哈希表,这是为了扩容(包括rehashidx也是为了扩容)。dictType存储了哈希表的属性。redis还为dict实现了迭代器(所以说看起来像c++代码)。

哈希表的具体实现是和mc类似的做法,也是使用开链法来解决冲突,不过里面用到了一些小技巧。比如使用dictType存储函数指针,可以动态配置桶里面元素的操作方法。又比如dictht中保存的sizemask取size(桶的数量)-1,用它与key做&操作来代替取余运算,加快速度等等。总的来看,dict里面有两个哈希表,每个哈希表的桶里面存储dictEntry链表,dictEntry存储具体的key和value。

前面说过,一个dict对于两个dictht,是为了扩容(其实还有缩容)。正常的时候,dict只使用dictht[0],当dict[0]中已有entry的数量与桶的数量达到一定的比例后,就会触发扩容和缩容操作,我们统称为rehash,这时,为dictht[1]申请rehash后的大小的内存,然后把dictht[0]里的数据往dictht[1]里面移动,并用rehashidx记录当前已经移动万的桶的数量,当所有桶都移完后,rehash完成,这时将dictht[1]变成dictht[0], 将原来的dictht[0]变成dictht[1],并变为null即可。不同于memcached,这里不用开一个后台线程来做,而是就在event loop中完成,并且rehash不是一次性完成,而是分成多次,每次用户操作dict之前,redis移动一个桶的数据,直到rehash完成。这样就把移动分成多个小移动完成,把rehash的时间开销均分到用户每个操作上,这样避免了用户一个请求导致rehash的时候,需要等待很长时间,直到rehash完成才有返回的情况。不过在rehash期间,每个操作都变慢了点,而且用户还不知道redis在他的请求中间添加了移动数据的操作,感觉redis太贱了 :-D

typedef struct dict {
    dictType *type;    // 哈希表的相关属性
    void *privdata;    // 额外信息
    dictht ht[2];    // 两张哈希表,分主和副,用于扩容
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 记录当前数据迁移的位置,在扩容的时候用的
    int iterators; /* number of iterators currently running */    // 目前存在的迭代器的数量
} dict;

typedef struct dictht {
    dictEntry **table;  // dictEntry是item,多个item组成hash桶里面的链表,table则是多个链表头指针组成的数组的指针
    unsigned long size;    // 这个就是桶的数量
    // sizemask取size - 1, 然后一个数据来的时候,通过计算出的hashkey, 让hashkey & sizemask来确定它要放的桶的位置
    // 当size取2^n的时候,sizemask就是1...111,这样就和hashkey % size有一样的效果,但是使用&会快很多。这就是原因
    unsigned long sizemask;  
    unsigned long used;        // 已经数值的dictEntry数量
} dictht;

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);     // hash的方法
    void *(*keyDup)(void *privdata, const void *key);    // key的复制方法
    void *(*valDup)(void *privdata, const void *obj);    // value的复制方法
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);    // key之间的比较
    void (*keyDestructor)(void *privdata, void *key);    // key的析构
    void (*valDestructor)(void *privdata, void *obj);    // value的析构
} dictType;

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next;
} dictEntry;

有了dict,数据库就好实现了。所有数据读存储在dict中,key存储成dictEntry中的key(string),用void* 指向一个redis object,它可以是5种类型中的任何一种。如下图,结构构造是这样,不过这个图已经过时了,有一些与redis3.0不符合的地方。

Chacun des objets de type 5 a au moins deux implémentations sous-jacentes. Il existe trois types de chaînes : REDIS_ENCODING_RAW, REDIS_ENCIDING_INT, REDIS_ENCODING_EMBSTR. Les listes incluent : une liste chaînée bidirectionnelle ordinaire et une liste chaînée compressée signifie simplement transformer le tableau en une liste chaînée, un espace continu, puis simuler la liste chaînée. en stockant les informations de taille de la chaîne. Par rapport aux listes chaînées ordinaires, cela peut économiser de l'espace, mais cela a des effets secondaires. Parce qu'il s'agit d'un espace continu, lors de la modification de la taille de la mémoire, il doit être réaffecté et parce que la taille en octets. de la chaîne est enregistrée, cela peut provoquer des mises à jour continues (implémentation spécifique Veuillez regarder le code en détail). Set a dict et intset (utilisez-le pour stocker tous les entiers), l'ensemble trié a : skiplist et ziplist, et l'implémentation de la table de hachage a une liste compressée, dict et ziplist. Skiplist est une liste à sauter. Elle a une efficacité proche de celle d'un arbre rouge-noir, mais est beaucoup plus simple à mettre en œuvre qu'un arbre rouge-noir, elle est donc adoptée (étrange, il n'y a pas de roue réinventée ici. Est-ce parce que cette roue est un peu difficile ?). La table de hachage peut être implémentée à l'aide de dict. Dans le dict, la clé dans chaque entrée de dictée stocke la clé (qui est la clé de la paire clé-valeur dans la table de hachage) et la valeur stocke la valeur. Quant au dict de l'ensemble, la clé de chaque entrée de dictée stocke la valeur d'un élément spécifique de l'ensemble et la valeur est nulle. Le zset (ensemble ordonné) dans l'image est faux. zset est implémenté à l'aide de skiplist et ziplist. Tout d'abord, skiplist est facile à comprendre, alors considérez-le simplement comme un substitut aux arbres rouge-noir. il peut également être trié. Comment utiliser ziplist pour stocker zset ? Premièrement, dans zset, chaque élément de l'ensemble a un score, qui est utilisé pour trier. Ainsi, dans la ziplist, en fonction du score, l'élément est stocké en premier, puis son score, puis l'élément suivant, et enfin le score. Il s'agit d'un stockage continu, donc lors de l'insertion ou de la suppression, la mémoire doit être réaffectée. Ainsi, lorsque le nombre d'éléments dépasse un certain nombre, ou que le nombre de caractères d'un élément dépasse un certain nombre, redis choisira d'utiliser skiplist pour implémenter zset (si ziplist est actuellement utilisé, les données de la ziplist seront supprimées et stocké dans une nouvelle liste de sauts, puis supprimez et modifiez la liste zip. Il s'agit de la conversion sous-jacente, et d'autres types d'objets Redis peuvent également être convertis). De plus, comment ziplist implémente-t-il hashtable ? En fait, c'est très simple, il suffit de stocker une clé, de stocker une valeur, puis de stocker une clé, puis de stocker une valeur. Il est toujours stocké séquentiellement, similaire à l'implémentation de zset, donc lorsque les éléments dépassent un certain nombre, ou que le nombre de caractères d'un élément dépasse un certain nombre, il sera converti en table de hachage pour l'implémentation. Diverses méthodes d'implémentation sous-jacentes peuvent être converties et Redis peut choisir la méthode d'implémentation la plus appropriée en fonction de la situation. C'est également l'avantage d'utiliser une méthode d'implémentation orientée objet similaire.

Il convient de souligner que lors de l'utilisation de skiplist pour implémenter zset, un dict est en fait utilisé, qui stocke les mêmes paires clé-valeur. Pourquoi? Étant donné que la recherche de skiplist est uniquement lgn (peut devenir n) et que dict peut être O(1), un dict est donc utilisé pour accélérer la recherche. Puisque skiplist et dict peuvent pointer vers le même objet redis, trop de mémoire sera utilisée. ne soit pas gaspillé. De plus, lorsque vous utilisez ziplist pour implémenter zset, pourquoi ne pas utiliser dict pour accélérer la recherche ? Parce que ziplist prend en charge un petit nombre d'éléments (lorsque le nombre est grand, il est converti en liste de sauts) et que le parcours séquentiel est également rapide, donc dict n'est pas nécessaire.

De ce point de vue, les objets dict, dictType, dictHt, dictEntry et redis ci-dessus sont tous très réfléchis. Ensemble, ils implémentent une base de données flexible et efficace avec des couleurs orientées objet. Je dois dire que la conception de la base de données Redis est toujours très puissante.

Contrairement à Memcached, Redis possède plusieurs bases de données, et il y en a 16 par défaut, numérotées de 0 à 15. Les clients peuvent choisir la base de données à utiliser et la base de données n° 0 est utilisée par défaut. Les différentes données de bases de données ne sont pas partagées, c'est-à-dire que la même clé peut exister dans différentes bases de données, mais dans la même base de données, la clé doit être unique.

Redis prend également en charge le réglage du délai d'expiration. Regardons l'objet redis ci-dessus. Il n'y a pas de champ pour enregistrer l'expiration. Alors, comment redis enregistre-t-il le délai d'expiration des données ? Redis ajoute un autre dict à chaque base de données. Ce dict est appelé expire dict. La clé dans l'entrée dict est une paire de clés et la valeur est un objet redis dont les données sont un entier de 64 bits. . De cette façon, lorsque vous déterminez si une clé a expiré, vous pouvez la trouver dans le dict d'expiration, supprimer l'heure d'expiration et la comparer avec l'heure actuelle. Pourquoi faire ça ? Étant donné que toutes les clés n'auront pas de délai d'expiration défini, pour les clés qui ne définissent pas de délai d'expiration, l'enregistrement d'un délai d'expiration gaspillera de l'espace. Au lieu de cela, utilisez un dict d'expiration pour le sauvegarder séparément et vous pourrez utiliser la mémoire de manière flexible selon vos besoins (. détecté Lorsque la clé expire, elle sera supprimée du dict d'expiration).

redis的expire 机制是怎样的呢? 与memcahed类似,redis也是惰性删除,即要用到数据时,先检查key是否过期,过期则删除,然后返回错误。单纯的靠惰性删除,上面说过可能会导致内存浪费,所以redis也有补充方案,redis里面有个定时执行的函数,叫servercron,它是维护服务器的函数,在它里面,会对过期数据进行删除,注意不是全删,而是在一定的时间内,对每个数据库的expire dict里面的数据随机选取出来,如果过期,则删除,否则再选,直到规定的时间到。即随机选取过期的数据删除,这个操作的时间分两种,一种较长,一种较短,一般执行短时间的删除,每隔一定的时间,执行一次长时间的删除。这样可以有效的缓解光采用惰性删除而导致的内存浪费问题。

以上就是redis的数据的实现,与memcached不同,redis还支持数据持久化,这个下面介绍。

4.redis数据库持久化

redis和memcached的最大不同,就是redis支持数据持久化,这也是很多人选择使用redis而不是memcached的最大原因。 redis的持久化,分为两种策略,用户可以配置使用不同的策略。

4.1 RDB持久化

用户执行save或者bgsave的时候,就会触发RDB持久化操作。RDB持久化操作的核心思想就是把数据库原封不动的保存在文件里。

那如何存储呢?如下图, 首先存储一个REDIS字符串,起到验证的作用,表示是RDB文件,然后保存redis的版本信息,然后是具体的数据库,然后存储结束符EOF,最后用检验和。关键就是databases,看它的名字也知道,它存储了多个数据库,数据库按照编号顺序存储,0号数据库存储完了,才轮到1,然后是2, 一直到最后一个数据库。

每一个数据库存储方式如下,首先一个1字节的常量SELECTDB,表示切换db了,然后下一个接上数据库的编号,它的长度是可变的,然后接下来就是具体的key-value对的数据了。

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
    /* Save the expire time */
    if (expiretime != -1) {
        /* If this key is already expired skip it */
        if (expiretime < now) return 0;
        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }

    /* Save type, key, value */
    if (rdbSaveObjectType(rdb,val) == -1) return -1;
    if (rdbSaveStringObject(rdb,key) == -1) return -1;
    if (rdbSaveObject(rdb,val) == -1) return -1;
    return 1;
}

由上面的代码也可以看出,存储的时候,先检查expire time,如果已经过期,不存就行了,否则,则将expire time存下来,注意,及时是存储expire time,也是先存储它的类型为REDIS_RDB_OPCODE_EXPIRETIME_MS,然后再存储具体过期时间。接下来存储真正的key-value对,首先存储value的类型,然后存储key(它按照字符串存储),然后存储value,如下图。

在rdbsaveobject中,会根据val的不同类型,按照不同的方式存储,不过从根本上来看,最终都是转换成字符串存储,比如val是一个linklist,那么先存储整个list的字节数,然后遍历这个list,把数据取出来,依次按照string写入文件。对于hash table,也是先计算字节数,然后依次取出hash table中的dictEntry,按照string的方式存储它的key和value,然后存储下一个dictEntry。 总之,RDB的存储方式,对一个key-value对,会先存储expire time(如果有的话),然后是value的类型,然后存储key(字符串方式),然后根据value的类型和底层实现方式,将value转换成字符串存储。这里面为了实现数据压缩,以及能够根据文件恢复数据,redis使用了很多编码的技巧,有些我也没太看懂,不过关键还是要理解思想,不要在意这些细节。

保存了RDB文件,当redis再启动的时候,就根据RDB文件来恢复数据库。由于以及在RDB文件中保存了数据库的号码,以及它包含的key-value对,以及每个key-value对中value的具体类型,实现方式,和数据,redis只要顺序读取文件,然后恢复object即可。由于保存了expire time,发现当前的时间已经比expire time大了,即数据已经超时了,则不恢复这个key-value对即可。

La sauvegarde des fichiers RDB est un projet énorme, donc Redis fournit également un mécanisme de sauvegarde en arrière-plan. Autrement dit, lorsque bgsave est exécuté, Redis crée un processus enfant pour permettre au processus enfant d'effectuer le travail enregistré, tandis que le processus parent continue de fournir les services de base de données normaux de Redis. Étant donné que le processus enfant copie l'espace d'adressage du processus parent, c'est-à-dire que le processus enfant possède la base de données lorsque le processus parent se divise, le processus enfant effectue une opération de sauvegarde et écrit la base de données dont il a hérité du processus parent dans un fichier temporaire. Lors de la copie du processus enfant, redis enregistrera le nombre de modifications (sales) de la base de données. Lorsque le processus enfant se termine, il envoie le signal SIGUSR1 au processus parent. Lorsque le processus parent capte ce signal, il sait que le processus enfant a terminé la copie. Le processus parent renomme ensuite le fichier temporaire enregistré par le processus enfant. vrai fichier rdb (c'est-à-dire que la véritable sauvegarde est réussie). Ce n'est qu'après avoir été modifié dans le fichier cible, c'est le moyen le plus sûr). Enregistrez ensuite l'heure de fin de cette sauvegarde.

Il y a ici un problème. Pendant la période de sauvegarde du processus enfant, la base de données du processus parent a été modifiée, et le processus parent n'enregistre que le nombre de modifications (sales) sans aucune opération de correction. Il semble que ce que RDB enregistre ne soit pas une base de données en temps réel, ce qui est un peu sans prétention. Cependant, la persistance AOF, qui sera introduite plus tard, résout ce problème.

Outre l'exécution par le client de la commande sava ou bgsave, les conditions de sauvegarde du RDB peuvent également être configurées. Autrement dit, il est configuré dans le fichier de configuration. Si la base de données est modifiée à des moments sales dans un délai t, elle sera enregistrée en arrière-plan. Lorsque Redis sert cron, il jugera s'il remplit les conditions en fonction du nombre d'objets sales et de l'heure de la dernière sauvegarde. Si les conditions sont remplies, la sauvegarde bg sera effectuée. Notez qu'il ne peut y avoir qu'un seul processus enfant. pour la sauvegarde en arrière-plan à tout moment, car la sauvegarde est une opération d'E/S très coûteuse. Un grand nombre d'opérations d'E/S dans plusieurs processus sont inefficaces et difficiles à gérer.

4.2 Persistance AOF

Tout d’abord, réfléchissez à une question : la sauvegarde d’une base de données nécessite-t-elle la sauvegarde de toutes les données de la base de données comme RDB ? Y a-t-il un autre moyen ?

RDB enregistre uniquement la base de données finale, qui est un résultat. Comment est né le résultat ? Il est établi via diverses commandes de l'utilisateur, le résultat n'a donc pas besoin d'être enregistré, mais seule la commande qui a créé le résultat est enregistrée. L'AOF de redis a cette idée contrairement à RDB, il enregistre les données de la base de données une par une.

Examinons d'abord le format du fichier AOF. Il stocke les commandes une par une. Tout d'abord, la longueur de la commande est stockée, puis vous pouvez étudier les délimiteurs spécifiques par vous-même. Quoi qu'il en soit, vous savez comment le fichier AOF est stocké. C'est la commande exécutée par le client Redis.

Il y a un sds aof_buf dans le serveur redis. Si la persistance aof est activée, chaque commande pour modifier la base de données sera stockée dans cet aof_buf (la chaîne au format de commande dans le fichier aof est enregistrée), puis la boucle d'événements le fera. pas une seule boucle. Dans le cron du serveur, appelez flushaofbuf, écrivez la commande dans aof_buf dans le fichier aof (en fait, ce qui est réellement écrit est le tampon du noyau), puis effacez aof_buf et entrez dans la boucle suivante. De cette façon, toutes les modifications de la base de données peuvent être restaurées via les commandes du fichier aof, obtenant ainsi l'effet de sauvegarder la base de données.

Il convient de noter que l'écriture appelée dans flushaofbuf écrit uniquement les données dans le tampon du noyau. L'écriture réelle du fichier est déterminée par le noyau lui-même et peut devoir être retardée pendant un certain temps. Cependant, redis prend en charge la configuration. Vous pouvez configurer la synchronisation après chaque écriture, puis appeler sync dans redis pour écrire les données du noyau dans le fichier. Cela ne nécessite qu'un appel système et prend du temps. Vous pouvez également configurer la stratégie pour qu'elle se synchronise une fois par seconde, puis redis démarrera un thread d'arrière-plan (donc redis n'est pas un seul thread, juste une seule boucle d'événement), et ce thread d'arrière-plan appellera la synchronisation toutes les secondes. Ici, je dois demander pourquoi la synchronisation n'a-t-elle pas été prise en compte lors de l'utilisation de RDB ? Étant donné que RDB est stocké une fois, contrairement à AOF plusieurs fois, appeler sync une fois pendant RDB n'a aucun effet, et lors de l'utilisation de bg save, le processus enfant se terminera (sortira) de lui-même et le tampon sera vidé dans la fonction de sortie à ce moment. ., il est automatiquement écrit dans le fichier.

Regardons cela à nouveau. Si vous ne souhaitez pas utiliser aof_buf pour enregistrer chaque commande de modification, vous pouvez également utiliser aof persistance. Redis fournit aof_rewrite, qui génère des commandes basées sur la base de données existante, puis écrit les commandes dans le fichier aof. Très étrange, non ? Oui, c'est génial. Lors de l'exécution de aof_rewrite, la variable redis est utilisée dans chaque base de données, puis différentes commandes sont générées en fonction du type spécifique de valeur dans la paire clé-valeur, telle que la liste, puis elle génère une commande pour enregistrer la liste, qui contient le informations requises pour enregistrer la liste. Les données requises, si les données de la liste sont trop longues, seront divisées en plusieurs commandes. Créez d'abord la liste, puis ajoutez des éléments à la liste. en sens inverse en fonction des données. Stockez ensuite ces commandes dans le fichier aof. Cela n'obtient-il pas le même effet que aof append ?

再来看,aof格式也支持后台模式。执行aof_bgrewrite的时候,也是fork一个子进程,然后让子进程进行aof_rewrite,把它复制的数据库写入一个临时文件,然后写完后用新号通知父进程。父进程判断子进程的退出信息是否正确,然后将临时文件更名成最终的aof文件。好了,问题来了。在子进程持久化期间,可能父进程的数据库有更新,怎么把这个更新通知子进程呢?难道要用进程间通信么?是不是有点麻烦呢?你猜redis怎么做的?它根本不通知子进程。什么,不通知?那更新怎么办? 在子进程执行aof_bgrewrite期间,父进程会保存所有对数据库有更改的操作的命令(增,删除,改等),把他们保存在aof_rewrite_buf_blocks中,这是一个链表,每个block都可以保存命令,存不下时,新申请block,然后放入链表后面即可,当子进程通知完成保存后,父进程将aof_rewrite_buf_blocks的命令append 进aof文件就可以了。多么优美的设计,想一想自己当初还考虑用进程间通信,别人直接用最简单的方法就完美的解决了问题,有句话说得真对,越优秀的设计越趋于简单,而复杂的东西往往都是靠不住的。

至于aof文件的载入,也就是一条一条的执行aof文件里面的命令而已。不过考虑到这些命令就是客户端发送给redis的命令,所以redis干脆生成了一个假的客户端,它没有和redis建立网络连接,而是直接执行命令即可。首先搞清楚,这里的假的客户端,并不是真正的客户端,而是存储在redis里面的客户端的信息,里面有写和读的缓冲区,它是存在于redis服务器中的。所以,如下图,直接读入aof的命令,放入客户端的读缓冲区中,然后执行这个客户端的命令即可。这样就完成了aof文件的载入。

// 创建伪客户端
fakeClient = createFakeClient();

while(命令不为空) {
   // 获取一条命令的参数信息 argc, argv
   ...

    // 执行
    fakeClient->argc = argc;
    fakeClient->argv = argv;
    cmd->proc(fakeClient);
}

整个aof持久化的设计,个人认为相当精彩。其中有很多地方,值得膜拜。

5. redis的事务

redis另一个比memcached强大的地方,是它支持简单的事务。事务简单说就是把几个命令合并,一次性执行全部命令。对于关系型数据库来说,事务还有回滚机制,即事务命令要么全部执行成功,只要有一条失败就回滚,回到事务执行前的状态。redis不支持回滚,它的事务只保证命令依次被执行,即使中间一条命令出错也会继续往下执行,所以说它只支持简单的事务。

首先看redis事务的执行过程。首先执行multi命令,表示开始事务,然后输入需要执行的命令,最后输入exec执行事务。 redis服务器收到multi命令后,会将对应的client的状态设置为REDIS_MULTI,表示client处于事务阶段,并在client的multiState结构体里面保持事务的命令具体信息(当然首先也会检查命令是否能否识别,错误的命令不会保存),即命令的个数和具体的各个命令,当收到exec命令后,redis会顺序执行multiState里面保存的命令,然后保存每个命令的返回值,当有命令发生错误的时候,redis不会停止事务,而是保存错误信息,然后继续往下执行,当所有的命令都执行完后,将所有命令的返回值一起返回给客户。redis为什么不支持回滚呢?网上看到的解释出现问题是由于客户程序的问题,所以没必要服务器回滚,同时,不支持回滚,redis服务器的运行高效很多。在我看来,redis的事务不是传统关系型数据库的事务,要求CIAD那么非常严格,或者说redis的事务都不是事务,只是提供了一种方式,使得客户端可以一次性执行多条命令而已,就把事务当做普通命令就行了,支持回滚也就没必要了。

我们知道redis是单event loop的,在真正执行一个事物的时候(即redis收到exec命令后),事物的执行过程是不会被打断的,所有命令都会在一个event loop中执行完。但是在用户逐个输入事务的命令的时候,这期间,可能已经有别的客户修改了事务里面用到的数据,这就可能产生问题。所以redis还提供了watch命令,用户可以在输入multi之前,执行watch命令,指定需要观察的数据,这样如果在exec之前,有其他的客户端修改了这些被watch的数据,则exec的时候,执行到处理被修改的数据的命令的时候,会执行失败,提示数据已经dirty。 这是如何是实现的呢? 原来在每一个redisDb中还有一个dict watched_keys,watched_kesy中dictentry的key是被watch的数据库的key,而value则是一个list,里面存储的是watch它的client。同时,每个client也有一个watched_keys,里面保存的是这个client当前watch的key。在执行watch的时候,redis在对应的数据库的watched_keys中找到这个key(如果没有,则新建一个dictentry),然后在它的客户列表中加入这个client,同时,往这个client的watched_keys中加入这个key。当有客户执行一个命令修改数据的时候,redis首先在watched_keys中找这个key,如果发现有它,证明有client在watch它,则遍历所有watch它的client,将这些client设置为REDIS_DIRTY_CAS,表面有watch的key被dirty了。当客户执行的事务的时候,首先会检查是否被设置了REDIS_DIRTY_CAS,如果是,则表明数据dirty了,事务无法执行,会立即返回错误,只有client没有被设置REDIS_DIRTY_CAS的时候才能够执行事务。 需要指出的是,执行exec后,该client的所有watch的key都会被清除,同时db中该key的client列表也会清除该client,即执行exec后,该client不再watch任何key(即使exec没有执行成功也是一样)。所以说redis的事务是简单的事务,算不上真正的事务。

以上就是redis的事务,感觉实现很简单,实际用处也不是太大。

6. redis的发布订阅频道

redis支持频道,即加入一个频道的用户相当于加入了一个群,客户往频道里面发的信息,频道里的所有client都能收到。

实现也很简单,也watch_keys实现差不多,redis server中保存了一个pubsub_channels的dict,里面的key是频道的名称(显然要唯一了),value则是一个链表,保存加入了该频道的client。同时,每个client都有一个pubsub_channels,保存了自己关注的频道。当用用户往频道发消息的时候,首先在server中的pubsub_channels找到改频道,然后遍历client,给他们发消息。而订阅,取消订阅频道不够都是操作pubsub_channels而已,很好理解。

同时,redis还支持模式频道。即通过正则匹配频道,如有模式频道p, 1, 则向普通频道p1发送消息时,会匹配p,1,除了往普通频道发消息外,还会往p,1模式频道中的client发消息。注意,这里是用发布命令里面的普通频道来匹配已有的模式频道,而不是在发布命令里制定模式频道,然后匹配redis里面保存的频道。实现方式也很简单,在redis server里面有个pubsub_patterns的list(这里为什么不用dict?因为pubsub_patterns的个数一般较少,不需要使用dict,简单的list就好了),它里面存储的是pubsubPattern结构体,里面是模式和client信息,如下所示,一个模式,一个client,所以如果有多个clint监听一个pubsub_patterns的话,在list面会有多个pubsubPattern,保存client和pubsub_patterns的对应关系。 同时,在client里面,也有一个pubsub_patterns list,不过里面存储的就是它监听的pubsub_patterns的列表(就是sds),而不是pubsubPattern结构体。

typedef struct pubsubPattern {
    redisClient *client;    // 监听的client
    robj *pattern;            // 模式
} pubsubPattern;

当用户往一个频道发送消息的时候,首先会在redis server中的pubsub_channels里面查找该频道,然后往它的客户列表发送消息。然后在redis server里面的pubsub_patterns里面查找匹配的模式,然后往client里面发送消息。 这里并没有去除重复的客户,在pubsub_channels可能已经给某一个client发过message了,然后在pubsub_patterns中可能还会给用户再发一次(甚至更多次)。 估计redis认为这是客户程序自己的问题,所以不处理。

/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

/* Send to clients listening for that channel */
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
            redisClient *c = ln->value;

            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.messagebulk);
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }
 /* Send to clients listening to matching channels */
    if (listLength(server.pubsub_patterns)) {
        listRewind(server.pubsub_patterns,&li);
        channel = getDecodedObject(channel);
        while ((ln = listNext(&li)) != NULL) {
            pubsubPattern *pat = ln->value;

            if (stringmatchlen((char*)pat->pattern->ptr,
                                sdslen(pat->pattern->ptr),
                                (char*)channel->ptr,
                                sdslen(channel->ptr),0)) {
                addReply(pat->client,shared.mbulkhdr[4]);
                addReply(pat->client,shared.pmessagebulk);
                addReplyBulk(pat->client,pat->pattern);
                addReplyBulk(pat->client,channel);
                addReplyBulk(pat->client,message);
                receivers++;
            }
        }
        decrRefCount(channel);
    }
    return receivers;
}

六. 总结

总的来看,redis比memcached的功能多很多,实现也更复杂。 不过memcached更专注于保存key-value数据(这已经能满足大多数使用场景了),而redis提供更丰富的数据结构及其他的一些功能。不能说redis比memcached好,不过从源码阅读的角度来看,redis的价值或许更大一点。 另外,redis3.0里面支持了集群功能,这部分的代码还没有研究,后续再跟进。

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn