Подробное объяснение сохраняемости Redis RDB

Redis
Подробное объяснение сохраняемости Redis RDB

Redis — это база данных в памяти, которая хранит данные в памяти, а эффективность чтения и записи намного выше, чем у традиционных баз данных, которые хранят данные на диске. Но как только процесс завершится, данные Redis будут потеряны.

Чтобы решить эту проблему, Redis предоставляет две схемы сохранения, RDB и AOF, для сохранения данных в памяти на диск во избежание потери данных.

Антирез сказал в статье «Redis Persistence Decryption», что обычно существует три общих стратегии операций сохранения для предотвращения повреждения данных:

  • Метод 1 заключается в том, что база данных не заботится о сбоях и восстанавливается с помощью резервного копирования данных или моментальных снимков после повреждения файлов данных. Вот как работает постоянство RDB в Redis.

  • Метод 2 заключается в том, что база данных использует журнал операций и записывает поведение операций для каждой операции, чтобы восстановить согласованное состояние через журнал после сбоя. Поскольку журнал операций записывается в режиме последовательного добавления, нет ситуаций, в которых журнал операций нельзя было бы восстановить. Подобно журналам повторов и отмен Mysql, вы можете увидеть это для деталей.«Дисковой файл InnoDB и дисковый механизм»статья.

  • Способ 3 заключается в том, что база данных не изменяет старые данные, а только завершает операцию записи путем добавления, так что сами данные представляют собой журнал, чтобы никогда не возникала ситуация, когда данные невозможно восстановить. CouchDB — хороший пример такого подхода.

RDB — это первый метод, который представляет собой процесс создания моментального снимка данных текущего процесса Redis на момент времени и его сохранения на устройстве хранения.

Использование РБД

Механизм запуска RDB делится на ручной запуск с использованием инструкций и автоматический запуск через конфигурацию redis.conf.

Инструкции по ручному запуску Redis для сохранения RDB:

  • save , эта команда заблокирует текущий сервер Redis.Во время выполнения команды сохранения Redis не может обрабатывать другие команды, пока не завершится процесс RDB.
  • bgsave, когда эта команда будет выполнена, Redis выполнит операцию моментального снимка асинхронно в фоновом режиме, и в это время Redis все еще может отвечать на запросы клиентов. Конкретная операция заключается в том, что процесс Redis выполняетforkОперация создает дочерний процесс, а процесс сохраняемости RDB отвечает за дочерний процесс и автоматически завершается после завершения. Редис будет толькоforkБлокировка происходит в течение периода, но обычно это время очень короткое. Но если объем данных Redis особенно велик,forkВремя будет дольше, а память удвоится, что требует особого внимания.

Конфигурация по умолчанию для автоматического запуска RDB выглядит следующим образом:

save 900 1 # 表示900 秒内如果至少有 1 个 key 的值变化,则触发RDB
save 300 10 # 表示300 秒内如果至少有 10 个 key 的值变化,则触发RDB
save 60 10000 # 表示60 秒内如果至少有 10000 个 key 的值变化,则触发RDB

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

Функция цикла работы сервера RedisserverCronПо умолчанию она будет выполняться раз в 100 миллисекунд.Эта функция используется для обслуживания работающего сервера.Одна из ее задач – проверить, выполняется ли одно из условий, заданных параметром сохранения, и если да, выполнить команду bgsave. команда.

Общий процесс RDB

Поняв основы использования RDB, нам нужно продолжить изучение сохраняемости RDB. Перед этим можно подумать, как реализовать механизм персистентности, ведь это модуль, требуемый многими промежуточными программами.

Прежде всего, структура содержимого постоянно хранимых файлов должна быть компактной.Особенно для баз данных объем данных, которые должны быть постоянными, очень велик, и необходимо следить за тем, чтобы постоянные файлы не занимали слишком много памяти. Во-вторых, при персистентности промежуточное ПО также должно иметь возможность быстро реагировать на запросы пользователей, а работа с персистентностью должна как можно меньше влиять на другие функции промежуточного ПО. Наконец, в конце концов, персистентность потребляет производительность, как сбалансировать производительность и безопасность данных и как гибко настроить триггерные операции персистентности.

Далее мы рассмотрим эти вопросы и перейдем к исходному коду за ответами.

Исходный код в этой статье взят из Redis 4.0, а соответствующий исходный код процесса сохраняемости RDB находится в файле rdb.c. Общий процесс показан на рисунке ниже.

image.png

На приведенной выше диаграмме показана общая взаимосвязь между тремя способами запуска сохраняемости RDB. пройти черезserverCronАвтоматически запускаемый RDB эквивалентен процессу прямого вызова инструкции bgsave для обработки. После того, как поток обработки bgsave запускает дочерний процесс, вызывается поток обработки инструкции сохранения.

Ниже мы начинаемserverCronАвтоматическая логика триггера для начала исследования.

Автоматически активировать сохраняемость RDB

Как показано на фиг.redisServerструктурныйsave_paramsУказывает на массив из трех значений, которые соответствуют элементу конфигурации сохранения в файле redis.conf. соответственноsave 900 1,save 300 10иsave 60 10000.dirtyЗаписывает, сколько ключевых значений изменилось,lastsaveЗаписывает время последнего сохранения RDB.

иserverCronФункция состоит в том, чтобы пройтись по значениям массива и проверить, соответствует ли текущее состояние Redis условиям для запуска сохраняемости RDB.Например, с момента последнего сохранения RDB прошло 900 секунд и изменился хотя бы один фрагмент данных.

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ....
    /* Check if a background saving or AOF rewrite in progress terminated. */
    /* 判断后台是否正在进行 rdb 或者 aof 操作 */
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
        ldbPendingChildren())
    {
        ....
    } else {
        // 到这儿就能确定 当前木有进行 rdb 或者 aof 操作
        // 遍历每一个 rdb 保存条件
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;

            //如果数据保存记录 大于规定的修改次数 且距离 上一次保存的时间大于规定时间或者上次BGSAVE命令执行成功,才执行 BGSAVE 操作
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 CONFIG_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == C_OK))
            {
                //记录日志
                serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, (int)sp->seconds);
                rdbSaveInfo rsi, *rsiptr;
                rsiptr = rdbPopulateSaveInfo(&rsi);
                // 异步保存操作
                rdbSaveBackground(server.rdb_filename,rsiptr);
                break;
            }
         }
    }
    ....
    server.cronloops++;
    return 1000/server.hz;
}

Если условия для запуска сохраняемости RDB соблюдены,serverCronпозвонюrdbSaveBackgroundфункция, то есть функция, которую вызовет инструкция bgsave.

Дочерний процесс выполняет сохранение RDB в фоновом режиме.

При выполнении команды bgsave Redis сначала запускаетbgsaveCommandВыполните проверку текущего состояния перед вызовомrdbSaveBackground, логика которого показана на рисунке ниже.

示意图

rdbSaveBackgroundОсновная задача функции — вызватьforkКоманда генерирует подпроцесс, который затем выполняется в подпроцессеrdbSaveфункция, то есть функция, которую в конечном итоге вызовет команда сохранения.

int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    pid_t childpid;
    long long start;
    // 检查后台是否正在执行 aof 或者 rdb 操作
    if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
    // 拿出 数据保存记录,保存为 上次记录
    server.dirty_before_bgsave = server.dirty;
    // bgsave 时间
    server.lastbgsave_try = time(NULL);
    start = ustime();
    // fork 子进程
    if ((childpid = fork()) == 0) {
        int retval;
        /* 关闭子进程继承的 socket 监听 */
        closeListeningSockets(0);
        // 子进程 title 修改
        redisSetProcTitle("redis-rdb-bgsave");
        // 执行rdb 写入操作
        retval = rdbSave(filename,rsi);
        // 执行完毕以后
        ....
        // 退出子进程
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* 父进程,进行fork时间的统计和信息记录,比如说rdb_save_time_start、rdb_child_pid、和rdb_child_type */
        ....
        // rdb 保存开始时间 bgsave 子进程
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy();
        return C_OK;
    }
    return C_OK; /* unreached */
}

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

Однако следует отметить, что вилка будет занимать определенное количество времени, а память, занимаемая родительским и дочерним процессами, одинакова.Когда значение ключа Redis велико, время вилки будет очень большим, и Redis не сможет ответить другим командам в это время. Кроме того, объем памяти, занимаемый Redis, будет удвоен.

Создание файлов RDB и сохранение на жесткий диск

РедисrdbSaveФункция — это функция, которая фактически выполняет сохранение RDB.Ее общий процесс выглядит следующим образом:

  • Сначала откройте временный файл,
  • перечислитьrdbSaveRioфункция для записи текущей информации о памяти Redis в этот временный файл,
  • Тогда позвониfflush,fsyncиfcloseИнтерфейс записывает файл на диск,
  • использоватьrenameПереименуйте временный файл в официальный файл RDB,
  • последняя записьdirtyиlastsaveи другую информацию о состоянии. Эти сообщения о состоянии находятся вserverCronбудет использоваться.
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    char tmpfile[256];
    // 当前工作目录
    char cwd[MAXPATHLEN];
    FILE *fp;
    rio rdb;
    int error = 0;

    /* 生成tmpfile文件名 temp-[pid].rdb */
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    /* 打开文件 */
    fp = fopen(tmpfile,"w");
    .....
    /* 初始化rio结构 */
    rioInitWithFile(&rdb,fp);

    if (rdbSaveRio(&rdb,&error,RDB_SAVE_NONE,rsi) == C_ERR) {
        errno = error;
        goto werr;
    }

    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* 重新命名 rdb 文件,把之前临时的名称修改为正式的 rdb 文件名称 */
    if (rename(tmpfile,filename) == -1) {
        // 异常处理
        ....
    }
    // 写入完成,打印日志
    serverLog(LL_NOTICE,"DB saved on disk");
    // 清理数据保存记录
    server.dirty = 0;
    // 最后一次完成 SAVE 命令的时间
    server.lastsave = time(NULL);
    // 最后一次 bgsave 的状态置位 成功
    server.lastbgsave_status = C_OK;
    return C_OK;
    ....
}

вот краткоfflushиfsyncразница. Оба они используются для сброса кеша, но относятся к разным уровням.fflushфункция дляFILE*указатель, сброс кэшированных данных из кэша прикладного уровня в ядро ​​иfsyncЭта функция является более низкоуровневой, воздействует на файловый дескриптор и используется для сброса кэша ядра на физическое устройство.

Конкретные принципы Linux IO см.«Разговор о Linux IO»

Данные в памяти в файл RDB

rdbSaveRioДанные в памяти Redis будут записаны в файл в относительно компактном формате.Схема формата файла показана ниже.

rdbSaveRioОбщий процесс написания функции выглядит следующим образом:

  • Сначала записывается магическое значение REDIS, за которым следует версия файла RDB (rdb_version) и дополнительная вспомогательная информация (aux). Вспомогательная информация включает версию Redis, использование памяти и библиотеку репликации ( repl-id ), смещение ( repl-offset ) и т. д.

  • потомrdbSaveRioОн будет проходить по всем базам данных текущего Redis и по очереди записывать информацию о базах данных. напиши первымRDB_OPCODE_SELECTDBID и номер БД, потом пишемRDB_OPCODE_RESIZEDBИдентификационный код, количество ключей базы данных и количество ключей, которые должны быть признаны недействительными, и, наконец, все значения ключей будут пройдены и записаны последовательно.

  • При записи значения ключа, когда у значения ключа есть время истечения срока действия, оно будет записано первым.RDB_OPCODE_EXPIRETIME_MSИдентификационный код и срок действия, затем напишите идентификационный код типа ключ-значение и, наконец, напишите ключ и значение.

  • После записи информации о базе данных также будет записана информация, связанная с Lua, и, наконец, записанаRDB_OPCODE_EOFИдентификатор терминатора и контрольное значение.

int rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) {
    snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
    /* 1 写入 magic字符'REDIS' 和 RDB 版本 */
    if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
    /* 2 写入辅助信息  REDIS版本,服务器操作系统位数,当前时间,复制信息比如repl-stream-db,repl-id和repl-offset等等数据*/
    if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr;
    /* 3 遍历每一个数据库,逐个数据库数据保存 */
    for (j = 0; j < server.dbnum; j++) {
        /* 获取数据库指针地址和数据库字典 */
        redisDb *db = server.db+j;
        dict *d = db->dict;
        /* 3.1 写入数据库部分的开始标识 */
        if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
        /* 3.2 写入当前数据库号 */
        if (rdbSaveLen(rdb,j) == -1) goto werr;

        uint32_t db_size, expires_size;
        /* 获取数据库字典大小和过期键字典大小 */
        db_size = (dictSize(db->dict) <= UINT32_MAX) ?
                                dictSize(db->dict) :
                                UINT32_MAX;
        expires_size = (dictSize(db->expires) <= UINT32_MAX) ?
                                dictSize(db->expires) :
                                UINT32_MAX;
        /* 3.3 写入当前待写入数据的类型,此处为 RDB_OPCODE_RESIZEDB,表示数据库大小 */
        if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr;
        /* 3.4 写入获取数据库字典大小和过期键字典大小 */
        if (rdbSaveLen(rdb,db_size) == -1) goto werr;
        if (rdbSaveLen(rdb,expires_size) == -1) goto werr;
        /* 4 遍历当前数据库的键值对 */
        while((de = dictNext(di)) != NULL) {
            sds keystr = dictGetKey(de);
            robj key, *o = dictGetVal(de);
            long long expire;

            /* 初始化 key,因为操作的是 key 字符串对象,而不是直接操作 键的字符串内容 */
            initStaticStringObject(key,keystr);
            /* 获取键的过期数据 */
            expire = getExpire(db,&key);
            /* 4.1 保存键值对数据 */
            if (rdbSaveKeyValuePair(rdb,&key,o,expire) == -1) goto werr;
        }

    }

    /* 5 保存 Lua 脚本*/
    if (rsi && dictSize(server.lua_scripts)) {
        di = dictGetIterator(server.lua_scripts);
        while((de = dictNext(di)) != NULL) {
            robj *body = dictGetVal(de);
            if (rdbSaveAuxField(rdb,"lua",3,body->ptr,sdslen(body->ptr)) == -1)
                goto werr;
        }
        dictReleaseIterator(di);
    }

    /* 6 写入结束符 */
    if (rdbSaveType(rdb,RDB_OPCODE_EOF) == -1) goto werr;

    /* 7 写入CRC64校验和 */
    cksum = rdb->cksum;
    memrev64ifbe(&cksum);
    if (rioWrite(rdb,&cksum,8) == 0) goto werr;
    return C_OK;
}

rdbSaveRioПри записи значения ключа он вызоветrdbSaveKeyValuePairфункция. Функция будет последовательно записывать время истечения срока действия ключа, тип ключа, ключ и значение.

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime)
{
    /* 如果有过期信息 */
    if (expiretime != -1) {
        /* 保存过期信息标识 */
        if (rdbSaveType(rdb,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;
}

Различные форматы записываются в соответствии с различными типами ключей.Типы и форматы различных значений ключей следующие.

Redis имеет огромную систему объектов и структур данных.Он использует шесть базовых структур данных для построения объектной системы, включающей строковые объекты, объекты-списки, хеш-объекты, объекты-коллекции и упорядоченные объекты-коллекции. Заинтересованные студенты могут обратиться к«Двенадцать фотографий помогут вам понять структуру данных и объектную систему Redis»одна статья.

Разные структуры данных имеют разные форматы сохранения RDB. Сегодня мы рассмотрим только то, как сохраняются объекты коллекций.

ssize_t rdbSaveObject(rio *rdb, robj *o) {
    ssize_t n = 0, nwritten = 0;
    ....
    } else if (o->type == OBJ_SET) {
        /* Save a set value */
        if (o->encoding == OBJ_ENCODING_HT) {
            dict *set = o->ptr;
            // 集合迭代器
            dictIterator *di = dictGetIterator(set);
            dictEntry *de;
            // 写入集合长度
            if ((n = rdbSaveLen(rdb,dictSize(set))) == -1) return -1;
            nwritten += n;
            // 遍历集合元素
            while((de = dictNext(di)) != NULL) {
                sds ele = dictGetKey(de);
                // 以字符串的形式写入,因为是SET 所以只写入 Key 即可
                if ((n = rdbSaveRawString(rdb,(unsigned char*)ele,sdslen(ele)))
                    == -1) return -1;
                nwritten += n;
            }
            dictReleaseIterator(di);
        } 
    .....
    return nwritten;
}

Оригинальный текст официального аккаунта

Оригинальный пост в блоге