[Чтение исходного кода Redis] Что делает Redis, когда вы вводите команду get/set

Redis задняя часть база данных сервер

предыдущий постПредставлен процесс запуска redis-сервера.После запуска сервера запускается механизм цикла событий для отслеживания поступления новых событий.В это время разные клиенты могут запрашивать сервер, отправляя команды и получая ответ результат обработки. В процессе разработки чаще всего используются команды get и set, так что же делает Redis, когда мы вводим команду get/set?

запуск redis-cli

Прежде чем понять, как используются команды, давайте разберемся, что делает redis-client при запуске. Существует несколько реализаций клиента Redis, и разные языки имеют свои собственные реализации.Вы можете увидеть различные версии здесь:Redis-версия, чаще всего в процессе отладки используется redis-client, то есть форма командной строки.Основной код реализации redis-client находится вredis-cli.hа такжеredis-cli.c. Запись запуска redis-client находится в основной функции.Если вы читаете код, вы можете видеть, что вы сначала устанавливаете свойства для конфигурации, а затем определяете, какой режим использует клиент для запуска.Режимы запуска: Задержка, Задержка распределенная , подчиненная библиотека, получение RDB, поиск Big Key, конвейер, статистика, сканирование, LRU, внутренняя задержка, интерактивный режим. Командная строка, которую мы используем, является интерактивным режимом.

Во время всего процесса подключения redis структура redisContext используется для сохранения контекста подключения.Взгляните на определение структуры:

/* 代表一个Redis连接的上下文结构体 */
typedef struct redisContext {
	int err;
	char errstr[128]; 
	int fd;
	int flags;
	char *obuf;
	redisReader *reader; 
	enum redisConnectionType connection_type;
	struct timeval *timeout;
	struct {
    	char *host;
    	char *source_addr;
    	int port;
	} tcp;

	struct {
    	char *path;
	} unix_sock;
} redisContext;
err:操作过程中的错误标志,0表示无错误
errstr:错误信息字符串
fd:redis-client连接服务器后的socket文件
obuf:保存输入的命令
tcp:保存一个tcp连接的信息,包括IP,协议族,端口

После ознакомления с используемыми структурами данных продолжите процесс подключения.В интерактивном режиме вызовы rediscliConnectфункция подключения.

Поток выполнения функции cliConnect:

  • 1. Вызовите функцию redisConnect для подключения к экземпляру сервера redis и используйте redisContext для сохранения контекста подключения.
  • 2. Чтобы избежать отключения, задайте свойство KeepAlive.Время KeepAlive по умолчанию составляет 15 с.
  • 3. После успешного подключения проверьте и выберите правильную БД.

После того, как соединение успешно установлено, запуск redis-cli завершен, в это время наступает фаза взаимодействия, redisContext инкапсулирует состояние клиента, подключающегося к серверу, и последующие операции, связанные с клиентом, будут оперировать этой структурой.

Отслеживайте весь процесс получения/установки команд

После успешного запуска клиента вы можете ввести команду для вызова команды redis. Ключ, используемый в этом тесте,username:1234, введите сначалаget username:1234.

Продолжайте читать код и обнаружите, что после того, как клиент входит в интерактивный режим, он вызывает repl для чтения команды терминала, отправки команды клиенту и возврата результата Функция repl является основной функцией интерактивного режима. Функция repl вызывает функцию linenoise для чтения команды, введенной пользователем.Метод чтения заключается в разделении нескольких параметров пробелами.После прочтения запроса команды он вызоветissueCommandRepeatФункция запускает выполнение команды,issueCommandRepeatвызов функцииcliSendCommandОтправьте команду на сервер.

cliSendCommandвызов функцииredisAppendCommandArgvФункция кодирует команду ввода с помощью протокола redis, а затем вызываетcliReadReplyФункция отправляет данные на сервер и считывает возвращенные данные с сервера. Когда я прочитал это, мне очень захотелось увидеть, как выглядят данные, закодированные с использованием протокола Redis, поэтому я подумал об использовании отладки точки останова gdb для просмотра данных и взаимодействия каждого шага.

подготовка к отладке GDB Redis

При отладке с помощью gdb вывод некоторых из этих переменных приведет к следующим результатам:

<value optimized out>

Это связано с тем, что опция оптимизации -O2 используется по умолчанию при компиляции.При этой опции компилятор оптимизирует некоторые переменные, которые он считает избыточными, поэтому конкретное значение не видно.Чтобы убрать эту оптимизацию, в gcc можно указать -O0 при компиляции.Для компиляции redis измените makefile, измените -O2 на -O0 или выполнитеmake nooptВот и все.

Введение в протокол связи Redis

Как мы все знаем, у HTTP есть собственный протокол, представляющий собой набор соглашений, которые должны соблюдать оба взаимодействующих компьютера. Например, как установить соединение, как идентифицировать друг друга и так далее. Только при соблюдении этого соглашения компьютеры могут взаимодействовать друг с другом. Для Redis, чтобы обеспечить нормальную связь между сервером и клиентом, он также определяет свой собственный протокол связи, и клиент, и сервер должны следовать этому протоколу при получении и анализе данных, чтобы обеспечить нормальную связь.

Общая форма протокола запроса Redis:

*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF

Соглашение об ответе:

状态回复(status reply)的第一个字节是 "+"
错误回复(error reply)的第一个字节是 "-"
整数回复(integer reply)的第一个字节是 ":"
批量回复(bulk reply)的第一个字节是 "$"
多条批量回复(multi bulk reply)的第一个字节是 "*"

Команда разбора

Из вышеприведенного описания видно, чтоissueCommandRepeatФункция является основной реализацией команды выполнения, и для функции создается точка останова.

(gdb) b issueCommandRepeat
Breakpoint 1 at 0x40f891: file redis-cli.c, line 1281.
(gdb) run
Starting program: /usr/local/src/redis-stable/src/redis-cli
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
127.0.0.1:6379> get username:1234
Breakpoint 1, issueCommandRepeat (argc=2, argv=0x68a950, repeat=1) at redis-cli.c:1281
1281            config.cluster_reissue_command = 0;

ПучокissuseCommandRepeatРаспечатываются параметры:

(gdb) p *argv
$1 = 0x685583 "get"
(gdb) p *(argv+1)
$2 = 0x68a973 "username:1234"

Известно, что введенная пользователем команда проходит черезlineinoiseПосле синтаксического анализа он передается через массив вissuseCommandRepeatфункция.

Продолжайте выполнять и введитеcliSendCommandфункция, главное, что делает эта функция — использует протокол redis для кодирования отправляемых команд (вызовredisAppendCommandArgvфункция), затем отправьте его на сервер и дождитесь ответа сервера (вызовcliReadReplyфункция).

Процесс синтаксического анализа команды заключается в использовании кодировки протокола redis, а затем сохранении результата в redisContext->obuf и просмотре кода в сочетании с ранее представленным протоколом связи redis, что очень интуитивно понятно.

/*
 * 使用Redis协议格式化命令,通过sds字符串保存,使用sdscatfmt函数追加
 * 函数接收几个参数,参数数组以及参数长度数组
 * 如果参数长度数组为NULL,参数长度会用strlen函数计算
 */
int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv,
	const size_t *argvlen)
{
	sds cmd;
	unsigned long long totlen;
	int j;
	size_t len;
	/* Abort on a NULL target */
	if (target == NULL)
		return -1;
	/* 计算总大小 */
	totlen = 1+countDigits(argc)+2;
	for (j = 0; j < argc; j++) {
		len = argvlen ? argvlen[j] : strlen(argv[j]);
		totlen += bulklen(len);
	}
	/* 初始化一个sds字符串 */
	cmd = sdsempty();
	if (cmd == NULL)
		return -1;
	/* 使用前面计算得到的totlen分配空间 */
	cmd = sdsMakeRoomFor(cmd, totlen);
	if (cmd == NULL)
		return -1;
	/* 构造命令字符串 */
	cmd = sdscatfmt(cmd, "*%i\r\n", argc); // *%i 表示包含命令在内,共有多少个参数
	for (j=0; j < argc; j++) {
		len = argvlen ? argvlen[j] : strlen(argv[j]);
		cmd = sdscatfmt(cmd, "%u\r\n", len); // %u 表示该参数的长度
		cmd = sdscatlen(cmd, argv[j], len); // 参数的值
		cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1); // 最后加上\r\n
	}
	assert(sdslen(cmd)==totlen);
	*target = cmd;
	return totlen;
}

В сочетании с процессом кодирования для команды ввода закодированный результат должен быть:

*2\r\n$3\r\nget\r\n$13\r\nusername:1234\r\n

Печать и проверка в gdb, а также печать context->obuf до и после выполнения функции redisAppendCommandArgv.Результаты следующие, и проверка прошла успешно:

(gdb)
984             redisAppendCommandArgv(context,argc,(const char**)argv,argvlen);
(gdb) p context->obuf
$3 = 0x6855a3 ""
(gdb) n
985             while (config.monitor_mode) {
(gdb) p context->obuf
4 = 0x68a9e3 "*2\r\n3\r\nget\r\n$13\r\nusername:1234\r\n"

Удалите \r\n и отобразите его более интуитивно:

*2 // общее количество аргументов команды, включая команды

$3 // длина первого аргумента

получить // значение первого параметра

$13 // длина второго параметра

username:1234 // значение второго параметра

отправить команду

После того, как клиент разбирает команду и кодирует ее, он переходит на следующий этап, отправляет команду на сервер и ломает функцию cliReadReply:

Breakpoint 3, cliReadReply (output_raw_strings=0) at redis-cli.c:840
840     static int cliReadReply(int output_raw_strings) {
(gdb) n
843         sds out = NULL;
(gdb)
844         int output = 1;
(gdb)
846         if (redisGetReply(context,&_reply) != REDIS_OK) {

называетсяredisGetReplyФункция получает параметр redisContext контекста подключения и записывает результат в _reply. В функции redisGetReply функция отправляет команду на сервер, а затем ждет возврата сервера.Внутри есть операция ввода-вывода, а базовые системные вызовы пишут и читают.

После вызова функции записи для отправки команды запрос доходит до сервера.В предыдущей статье рассказывалось о том, как запускается сервер redis.После запуска он входит в состояние цикла событий.Дальше посмотрим,как сервер обрабатывает запрос.

обработать запрос

Запуск сервера зарегистрирует файловые события, зарегистрированные обратные вызовыacceptTcpHandler, когда сервер доступен для чтения (т.е. клиент может писать/закрывать),acceptTcpHandlerВызывается, трассируем функцию, цепочка вызовов следующая:

acceptTcpHandler -> anetTcpAccept -> acceptCommonHandler

acceptTcpHandlerФункция вызовет функцию anetTcpAccept, чтобы инициировать принятие и получение клиентских запросов.После поступления запроса она вызоветacceptCommonHandlerобработка функций.acceptCommonHandlerЦепочка вызовов:

acceptCommonHandler-> createClient -> readQueryFromClient

acceptCommonHandlerвызов функцииcreateClientСоздайте клиент, пропишите callback-функцию, и если придет запрос, она будет вызванаreadQueryFromClientФункция читает запрос клиента. После того, как клиент будет успешно создан, Redis добавит его в список клиентов текущего сервера, а затем, если вам нужно взаимодействовать со всеми клиентами, будет использоваться этот список.

Разобрав всю цепочку вызовов, добавьте точку останова в функцию readQueryFromClient для просмотра полученных данных:

(gdb) b readQueryFromClient
Breakpoint 1 at 0x440b6b: file networking.c, line 1377.
1377        client c = (client) privdata;
(gdb) n
1383        readlen = PROTO_IOBUF_LEN;
(gdb) n
1390        if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
(gdb) n
1398        qblen = sdslen(c->querybuf);
(gdb) n
1399        if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
(gdb) n
1400        c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
(gdb) n
1401        nread = read(fd, c->querybuf+qblen, readlen);

Из шагов выполнения видно, что redis-server создает строковую структуру для запрошенной команды для сохранения, а затем инициирует системный вызов read для чтения данных из текущего сокета.После выполнения этого шага прочитанная строка и строковая структура печатаются.Хранение тела в памяти:

(gdb) p nread
$7 = 33
(gdb) p c->querybuf
8 = (sds) 0x7ffff6b3a345 "*2\r\n3\r\nget\r\n$13\r\nusername:1234\r\n"
(gdb) x/33 c->querybuf
0x7ffff6b3a345: 42 '*'  50 '2'  13 '\r' 10 '\n' 36 '$'  51 '3'  13 '\r' 10 '\n'
0x7ffff6b3a34d: 103 'g' 101 'e' 116 't' 13 '\r' 10 '\n' 36 '$'  49 '1'  51 '3'
0x7ffff6b3a355: 13 '\r' 10 '\n' 117 'u' 115 's' 101 'e' 114 'r' 110 'n' 97 'a'
0x7ffff6b3a35d: 109 'm' 101 'e' 58 ':'  49 '1'  50 '2'  51 '3'  52 '4'  13 '\r'
0x7ffff6b3a365: 10 '\n'

Если длина анализируемой строки вычислена по протоколу redis, а длина равна 33, выведитеc->querybufВ случае сохранения в памяти видно, что здесь находится строка байтов всей команды, и каждый байт сохраняется рядом друг с другом.

Поскольку Redis управляется событиями, каждый раз, когда приходят данные,readQueryFromClientФункция будет вызвана для чтения команды. Команда get, используемая для отладки, на этот раз относительно короткая, и redis-серверу требуется только один процесс цикла обработки событий для разбора всей команды.Сервер каждый раз считывает в буфер не более 1024*16 байтов символов.Если длина команды превышает буфер Максимальная длина, которая будет считана в нескольких событиях, а затем выполнена.

После прочтения команды следующим шагом будет ее разбор и выполнение. Команда кодируется в соответствии с протоколом redis, введенным ранее, и сервер также декодируется в соответствии с тем же протоколом, а затем сохраняется в redisClient.Процесс парсинга является обратным процессом кодирования, а именно в функции processMultibulkBuffer.Если вам интересно в деталях вы можете просмотреть эту функцию.

После разбора команды вызовитеprocessCommandФункция выполняет команду. Как упоминалось в предыдущей статье, таблица команд будет загружена на сервер при запуске сервера.processCommandФункция сначала ищет наличие команды в таблице команд.Метод поиска заключается в поиске словаря команд Redis с именем команды в качестве ключа.Для команды get формат определения выглядит следующим образом:

{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}

После чтения в command->proc будет установлена ​​функция getCommand, после чего сервер выполнит ряд проверок: корректны ли параметры, авторизованы ли, обрабатывается ли кластерный режим, превышен ли максимальный объем памяти, есть ли проблема с жестким диском, он не будет обработан и т. д., а затем вызовите функцию вызова для выполнения команды. Функция вызова — это основная функция Redis для выполнения команд.Основной код функции вызова:

void call(client *c, int flags) {
	-- 执行前检查 --
	/* 调用命令执行函数 */
	dirty = server.dirty;
	start = ustime();
	c->cmd->proc(c);
	duration = ustime()-start;
	dirty = server.dirty-dirty;
	if (dirty < 0) dirty = 0;

	-- 执行后处理 --
}

Взгляните на приведенный выше код, это реализация функции вызова команды динамического распределения redis, настройте функцию выполнения, соответствующую каждой команде, количество параметров и другую информацию в таблице команд, и загрузите команду в таблицу команд, когда сервер запускаетсяserver.commands, то командная функция в таблице команд будет сохранена вredisCommand.proc, поэтому в функции вызова просто выполнитеc->cmd->proc(c)Функция, соответствующая команде выполнения, может быть выполнена.

реализация getCommand

Для этой команды get посмотрите непосредственно на функцию getCommand и вызовите getGenericCommand

/*
* get命令的"通用"实现
*/
int getGenericCommand(client *c) {
	robj *o;
    // 调用lookupKeyReadOrReply函数查找指定key,找不到,返回
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
        return C_OK;
    // 如果找到的对象类型不是string返回类型错误
    if (o->type != OBJ_STRING) {
        addReply(c,shared.wrongtypeerr);
        return C_ERR;
    } else {
        addReplyBulk(c,o);
        return C_OK;
    }
}

/*
* get命令
* 调用getGenericCommand函数实现具体操作
*/
void getCommand(client *c) {
    getGenericCommand(c);
}

Вызывается функцией getGenericCommandlookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)переданными параметрами являются client c, key и shared.nullbulk.

shared.nullbulkЭто общая переменная, созданная Redis при запуске сервера.Поскольку она используется во многих местах, Redis создаст эти общие переменные, чтобы сократить повторяющийся процесс создания и уменьшить потерю памяти. Его значение:

shared.nullbulk = createObject(OBJ_STRING,sdsnew("$-1\r\n"));

Функция lookupKeyReadOrReply — это просто простая инкапсуляция, которая выглядит очень лаконично.На самом деле доступ к базе данных заключается в вызове функции db.c/lookupKey, которая является ядром реализации команды get:

/*
* 查找数据库中指定key的对象并返回,查询出来的对象用于读操作
*/
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);
    if (!o) addReply(c,reply);
    return o;
}

robj *lookupKey(redisDb *db, robj *key, int flags) {
	// 在字典中根据key查找字典对象
	dictEntry *de = dictFind(db->dict,key->ptr);
	if (de) {
		// 获取字典对象的值
		robj *val = dictGetVal(de);
		/* 更新key的最新访问时间 */
  		if (server.rdb_child_pid == -1 &&
  			server.aof_child_pid == -1 &&
  			!(flags & LOOKUP_NOTOUCH))
  		{
  			if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
  				unsigned long ldt = val->lru >> 8;
  				unsigned long counter = LFULogIncr(val->lru & 255);
  				val->lru = (ldt << 8) | counter;
  			} else {
  			val->lru = LRU_CLOCK();
  			}
		}
		return val;
  	} else {
  		return NULL;
	}
}

В Redis все пары ключ-значение хранятся в памяти с помощью встроенной хеш-таблицы, поэтому при реализации lookupKey сначала используйтеdictFindФункция находит, существует ли входящий ключ в хеш-таблице, и если находит, вызываетdictGetValПолучить атрибут value объекта хеш-узла, в противном случае вернуть NULL, временная сложность функции равна O(1).

После того, как функция lookupKeyRead получает результат, она определяет тип значения:

Если он равен NULL, вернуть клиенту параметр shared.nullbulk, полученный функцией. shared.nullbuk — это объект ответа, переданный функцией верхнего уровня, нулевой общий объект, который преобразуется в nil в соответствии с протоколом Redis.

Если функция не пуста, вызовите dictGetVal, чтобы получить значение найденного объекта, а затем вернитесь.

Два найденных результата — это, в конечном счете, вызов функции addReply для возврата результата клиенту, функции addReply для передачи ответа клиенту и функции addReply для записи результата ответа в buf клиента. , пока данные buf будут выводиться клиенту. После того, как клиент получает контент, он анализирует результат в соответствии с протоколом redis и выводит его. В этом примере ключ для поиска не существует, поэтому клиент отображает (ноль)

До сих пор был представлен весь процесс команды get, затем посмотрите ссылку выполнения команды set, введитеset username:1234.

установить команду

Процесс выполнения set почти такой же, как и у get, разница в том, что при обработке запросаsetCommand,setCommandСначала выполните некоторую проверку параметров, а затем выполните преобразование кодировки для значения, потому что существует два формата кодирования для сохранения строк Redis: embstr и sds. Использование embstr для кодирования строк может сэкономить место, что также делает Redis. продолжайте смотреть вниз и, наконец, позвонитеsetGenricCommandфункция:

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* 初始化,避免报错 */
    // 如果需要设置超时时间,根据unit单位参数设置超时时间
    if (expire) {
   		// 获取时间值
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
   		// 处理非法的时间值
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000; // 统一用转换成毫秒
    }
    /*
     * 处理非法情况
     * 如果flags为OBJ_SET_NX 且 key存在或者flags为OBJ_SET_XX且key不存在,函数终止并返回abort_reply的值
     */
    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);
        return;
    }
    // 设置val到key中
    setKey(c->db,key,val);
    // 增加服务器的dirty值
    server.dirty++;
    // 设置过期时间
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    // 通知监听了key的数据库,key被操作了set、expire命令
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);
    // 返回成功的信息
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

void setKey(redisDb *db, robj *key, robj *val) {
    /*
     * 如果key不在数据库里,新建
     * 否则,用新值覆盖
     */
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val); // 增加值的引用计数
    removeExpire(db,key); // 重置键在数据库里的过期时间
    signalModifiedKey(db,key); // 发送修改键的通知
}

setGenericCommandперечислитьsetKeyфункция будетkey-valueЧтобы добавить пару ключ-значение в базу данных, вызывается setKeydictFindФункция находит есть ли ключ в базе, если есть в базе, то перезаписывает старое значение значением, иначе добавляет ключ-значение в базу. Для примера в этой статье, потому чтоusername:1234Этот ключ не существует,dictFindПоиск ничего не возвращает, поэтому функцияdbAddназывается,dbAddФункция будет вызываться только в том случае, если ключ не существует в текущей базе данных.

Пары ключ-значение в Redis сохраняются в структуре данных dict (объект словаря). Введение в структуру данных dict можно найти в предыдущих статьях:Реализация словаря dict. Реализация API конкретной операции напрямую смотрит на код:dict.c.

Отслеживание деталей кода функции dbAdd можно найти, вызываяdictAddФункция выполняет определенное действие,_dictKeyIndexФункция возвращает соответствующий индекс массива словаря для ключа, затем выделяет память для сохранения нового узла, добавляет узел в хеш-таблицу и устанавливает конкретные значения ключа и значения. После успешной операции вернитесьDICT_OK, иначе возвратDICT_ERR.

Теперь для этого имени пользователя: 1234 установлено значение, если вы снова вызовете команду:get username:1234, процесс такой же, как описано выше, доdictFindэтап, функцию можно найти в базе данныхkey username:1234, результат, возвращаемый функцией, не пустой, поэтому вызовите функцию dictGetVal, чтобы получить значение ключа, а затем вызовитеaddReplyВозвращает значение объекта.

На этом весь процесс команды set/get завершен, и чтение может быть немного запутанным. Поэтому, в соответствии с содержимым, опубликованным на этот раз, добавьте еще одно изображение. После прочтения этого изображения и просмотра всего процесс, вы можете углубить свое понимание.Посмотреть увеличенное изображение

redis调用链路

Суммировать

В ходе этого исследования, от внешнего кода до реализации базового сетевого кода, я получил много знаний о сети и навыков упаковки кода и еще раз вздохнул от красоты кода Redis. Воспользуйтесь этой возможностью, чтобы поделиться тем, что вы узнали.Если вам нужно просмотреть другие реализации команд или других функций, вы можете использовать эту идею в качестве справочной информации.

Справочная статья:More Redis internals: Tracing a GET & SET

Оригинал статьи, ограниченный стиль написания, недостаток знаний и знаний, если есть неточности в статье, сообщите пожалуйста.

Для более интересного контента, пожалуйста, обратите внимание на личный публичный номер.