предыдущий постПредставлен процесс запуска 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. Воспользуйтесь этой возможностью, чтобы поделиться тем, что вы узнали.Если вам нужно просмотреть другие реализации команд или других функций, вы можете использовать эту идею в качестве справочной информации.
Справочная статья:More Redis internals: Tracing a GET & SET
Оригинал статьи, ограниченный стиль написания, недостаток знаний и знаний, если есть неточности в статье, сообщите пожалуйста.
Для более интересного контента, пожалуйста, обратите внимание на личный публичный номер.