Redis 设计与实现(第九章) -- 数据库

概述



1.数据库结构

2.数据库键空间

3.键生存时间

4.持久化对过期键处理

5.数据库通知

1.数据库结构

Redis服务器将所有server状态都保存在数据结构中的db数组,服务器会根据dbnum来决定创建多个个数据库,默认为16个。

struct redisServer {  //数据结构里面有很多属性,这里只取了相关的两个来说明
    /* General */
    redisDb *db;
    int dbnum;
}redisServer;

创建db后,如下所示:

同样的在redisClient的数据结构中,也有一个指向当前db的属性,当在客户端执行select x时,指针就会指向对应的db

typedef struct redisClient {
    redisDb *db;
}redisClient;

如下图所示,客户端选择db 2的时的结构图:

2.数据库键空间

Redis是一个键值对数据库,数据库的所有键值对都保存在字典中,可以看下redisDb的数据结构中,有个dict的属性,称这个字典属性为键空间;

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

键空间和用户所见的数据库是对应的,比如set msg aaa,对应每个键都是一个字符串对象,每个值可以为字符串对象、列表对象、hash表对象、集合对象等;

比如在客户端命令中,设置几个key对象,一个字符串,一个列表,一个哈希表,如下:

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo }
span.s1 { }

127.0.0.1:6379[12]> set msg 1

OK

127.0.0.1:6379[12]> rpush list "a" "b" "c"

(integer) 3

127.0.0.1:6379[12]> hset book author "Jony"

(integer) 1

127.0.0.1:6379[12]> hset book name "c++"

对应的关系如下:

读写键空间的时候,还有一些其他的维护操作,比如:

1.在读取一个键后(读和写都要读取键空间),服务器会根据键是否存在来更新键空间命中或未命中的次数,可以通过info stats命令查看;

p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 11.0px Menlo }
span.s1 { }

keyspace_hits:12322422

keyspace_misses:1843426

2.在读取一个键后,服务器会更新这个键的LRU(最后一次使用时间),使用object idletime key可以查看这个key的空闲时间,前面章节有讲到过;

3.服务器读取的时候,如果发现key已经过期,则会先删除这个key,再执行余下操作;

4.如果有客户端使用WATCH 命令监视了某个键,服务器会在这个键被修改后,将这个键表尾dirty,从而让事务程序意识到这个键已经被修改了;

5.服务器每次修改一次键,就会将dirty数+1,这个键会触发服务器的持久化及复制操作;

6.如果服务器开启了数据库通知功能,则会在键被修改后,按照配置发送相应的数据库通知。

3.键的生存时间

在Redis中可以通过expire来设置键的过期时间,我们也可以通过ttl key命令来查看键的过期时间。具体是怎样实现的?如果保存的?如何超时的?接下来会分段来讲解。

设置超时

可以通过四种命令来设置key的过期时间:

expire key ttl:设置key的过期时间为ttl秒

pexpire key ttl:设置key的过期时间为ttl毫秒

expireat key timestamp:设置key的过期时间为timestamp所指定的秒数时间戳

pexpireat key timestamp:设置key的过期时间为timestamp所指定的毫秒数时间戳

这个不多说了,四个命令大体类似,最终执行结果和pexpireat一样;

保存过期时间

怎么保存的呢? 看下redisDb的数据结构,有个expires的dict来保存过期的键值对:

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

dict的键为指向键空间的某个键对象,值为超时时间,比如为某个key通过expire设置了超时时间,那么保存结构如下(键空间的地址其实只有一个),保存的为时间戳:

移除过期时间

通过persist key来移除key的超时时间,即将key对应的键值对从expires字典中移除

返回剩余生存时间

通常通过ttl或者pttl来获取key的剩余生存时间,具体实现为过期时间减去当前时间时间戳

过期键的判断

如果不通过ttl来返回剩余时间,就时间访问expires的dict字典:

1.先检查key是否在expires字典中存在;如果存在则获取时间;

2.获取unix的当前时间戳,如果大于过期时间的话,则过期,否则未过期;

过期键的删除

这个也是面试中比较常问的,给redis键设置超时时间后,到期后怎么就自动删除了?

Redis的删除机制,理论上来将可以有如下考虑:

1.设置定时机制

在给键设置过期时间的时候,同时设置一个定时器,定时器到期后,自动删除;这样做的优点是可以保存过期键能够及时被删除,并释放占用的内存;但是缺点也比较明显,当大量的键存在的时候,对cpu消耗会比较大,这样回导致无法响应正常的请求;

2.惰性删除

key到期后,不去删除它,当需要取key的时候,先获取过期时间,如果超时了,则先删除再进行下一步处理。这样对cpu是最友好的,但是会占用内存,如果过期key比较多,又没有程序使用这些key的话,就会一直占着内存。

3.定期删除

前面两种策略要不就是耗CPU,要不就是耗内存。定期删除每隔一段时间执行过期键的删除操作,并通过限制删除操作执行的时长和频率来减少对cpu的影响。定期删除极大的减少了内存浪费的情况。那定期删除以什么频率和执行时间来操作呢?

redis中过期键的删除策略

redis中采用的删除策略为惰性删除和定期删除两种,配合这两种策略,服务器可以很好的在使用cpu时间和避免内存浪费之间取得平衡。

惰性删除的实现

所有对数据库的读写命令执行之前都会先调用expireIfNeed函数来判断是否超时,具体代码如下:

int expireIfNeeded(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    mstime_t now;

    if (when < 0) return 0; /* No expire for this key */

    /* Don‘t expire anything while loading. It will be done later. */
    if (server.loading) return 0;

    /* If we are in the context of a Lua script, we claim that time is
     * blocked to when the Lua script started. This way a key can expire
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
    now = server.lua_caller ? server.lua_time_start : mstime();

    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller,
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    if (server.masterhost != NULL) return now > when;

    /* Return when this key has not expired */
    if (now <= when) return 0;

    /* Delete the key */
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    return dbDelete(db,key);
}

执行流程如下:

定期删除的实现

通过activeExpireCycle函数来实现,该函数会分多次遍历服务器中的各个数据库,从数据库的expires字典中随机取出一部分键的过期时间,并删除其中的过期键;

void activeExpireCycle(int type) {
    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    long long start = ustime(), timelimit;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don‘t start a fast cycle if the previous cycle did not exited
         * for time limt. Also don‘t repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit) return;
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }

    /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 1) Don‘t test more DBs than we have.
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don‘t want
     * expired keys to use memory for too much time. */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;  //执行时间限制
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);  //从0db开始,依次往上

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we‘ll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        current_db++;

        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;

            /* If there is nothing to expire try next DB ASAP. */
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. */
            expired = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl < 0) ttl = 0;
                ttl_sum += ttl;
                ttl_samples++;
            }

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                /* Smooth the value averaging with the previous one. */
                db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
            }

            /* We can‘t block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            iteration++;
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                long long elapsed = ustime()-start;

                latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
                if (elapsed > timelimit) timelimit_exit = 1;
            }
            if (timelimit_exit) return;
            /* We don‘t repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);  //循环,如果过期键超过25%
    }
}

该函数怎么调用?  会有个serverCron,默认100ms一次,serverCron会调用databaseCron,databaseCron会调用这个函数进行定期删除key

4.持久化对过期键处理

对于RBD文件,在执行SAVE或BGSAVE命令时,对过期的键不会进行保存到RBD文件中。同理在恢复时,程序会对键的过期时间进行检查,如果过期则不会载入到数据库。

对于AOF文件,如果某个键已经过期,但是还没有到定期删除和惰性删除,对AOF文件没有任何影响。因为当键执行定期删除或惰性删除后,程序会往AOF中追加一个DEL命令。来显示记录该键已经被删除。

在执行AOF重写时,不会将过期的键保存到重写后的AOF文件中。

5.数据库通知

当客户端有订阅某个key时,数据库通知可以在key发生变化时,通知给key的订阅者。

时间: 2024-10-11 15:28:20

Redis 设计与实现(第九章) -- 数据库的相关文章

Redis Essentials 读书笔记 - 第九章: Redis Cluster and Redis Sentinel (Collective Intelligence)

Chapter 9. Redis Cluster and Redis Sentinel (Collective Intelligence) 上一章介绍了复制,一个master可以对应一个或多个slave(replica), 在以下的情况下是够用的: 1. master有足够内存容纳所有key 2. 通过slave可以扩展读,解决网络吞吐量的问题 3. 允许停止master的维护窗口时间 4. 通过slave做数据冗余 但复制解决不了自动failover和自动resharding的问题,在以下的情

第九章 企业项目开发--分布式缓存Redis(1)

注意:本章代码将会建立在上一章的代码基础上,上一章链接<第八章 企业项目开发--分布式缓存memcached> 1.为什么用Redis 1.1.为什么用分布式缓存(或者说本地缓存存在的问题)? 见<第八章 企业项目开发--分布式缓存memcached> 1.2.有了memcached,为什么还要用redis? 见<第一章 常用的缓存技术> 2.代码实现 2.1.ssmm0 pom.xml 只在dev环境下添加了以下代码: <!-- redis:多台服务器支架用什么

mySQL教程 第1章 数据库设计

第1章 数据库设计 E-R设计 很多同学在学SQL语句时,觉得非常困难,那是因为你在学一个你根本不了解的数据库,数据库中的表不是你设计的,表与表之间的关系你不明白.因此在学SQL语句之前,先介绍一下数据库设计. 下面举例说明数据库设计: 学校需要开发一个系统记录有学生.课程和成绩信息.数据库如何设计? 这里面涉及到两个实体,学生表.课程,这些表为实体表. 这些表之间有什么关系呢?.学生考试出成绩,成绩记录在成绩表. 一个学生可以参加多门课程,关系是1对多. 数据库设计实例 设计数据库和表 安装m

第九章 Redis过期策略

注:本文主要参考自<Redis设计与实现> 1.设置过期时间 expire key time(以秒为单位)--这是最常用的方式 setex(String key, int seconds, String value)--字符串独有的方式 具体的使用方式:查看"java企业项目开发实践"的XXXXXXXXXXXXXXXXXXXXXX 注意: 除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠expire方法来设置时间 如果没有设置时间,那缓存就是永不过期 如果设置了过

数据库学习笔记:第九章 存储数据:磁盘和文件

第九章 数据以磁盘块为单位存储在磁盘上.块分布于一张或多张盘片的同心环形磁道上.磁道可以在盘片的单面或双面上录制. 同一直径的所有磁道的集合称为柱面. 磁盘块的大小在磁盘初始化时可以被设置成扇区大小的倍数. 每一个记录的表面都有一个磁盘头阵列.读写一块时,磁头必须定位在块头位置. 不能并行读写的主要原因是很难保证所有磁头被精确定位在相应的磁道上. 磁盘控制器是磁盘驱动器与计算机的接口. 当数据被写到扇区时,需要计算校验和并存储在扇区上,当扇区上的数据被独处时,需要再次校验和. 寻道时间是用于移动

使用Java实现数据库编程—01 第一章 数据库的设计

 1.        数据库设计:将数据库中的数据实体及这些数据实体之间的关系进行规划和结构化的过程: 良好的数据库设计: 节省数据的存储空间 能够保证数据的完整性 方便进行数据库应用系统的开发 糟糕的数据库设计: 数据冗余.存储空间浪费 内存空间浪费 数据更新和插入的异常  2.        数据库设计的步骤: 1.  需求分析阶段:分析客户的业务和数据处理需求 2.概要设计阶段:设计数据库的E-R模型图,确认需求信息的正确和完整 3. 详细设计阶段:将E-R图转换为多张表,进行逻辑设计,确

《Redis设计与实现》- 数据库

1. 服务器中数据库结构 Redis 服务器将所有数据库都保存在服务器状态 redisServer 结构的 db 数组中,由 redisDb 结构代表一个数据库 struct redisServer { // ... // 一个数组,保存着服务器中的所有数据库 redisDb *db; } Redis 服务器默认会创建16个数据库,默认情况下,Redis客户端的目标数据库是0号数据库. 2. 切换数据库 SELECT 命令用来切换数据库 redis> SELECT 2 OK redis[2]>

数据库系统原理(第三章数据库设计 )

一.数据库设计概述 数据库的生命周期  数据库设计的目标: 满足应用功能需求(存.取.删.改), 良好的数 据库性能(数据的高效率存取和空间的节省 共享性.完整性.一致性.安全保密性) 数据库设计的内容  数据库设计的方法 直观设计法( 最原始的数据库设计方法) 规范设计法:(新奥尔良设计方法:需求分析.概念结构设计.逻辑结构设计.物理结构设计 : 基于E-R模型的数据库设计方法 :基于第三范式的设计方法,是一类结构化设计方法) 计算机辅助设计法( 辅助软件工程工具) 数据库设计的过程 二.数据

《Redis设计与实现》读书笔记

花了几天时间把<Redis设计与实现>读完了,把一些心得记下来给大家分享. 第2章 简单动态字符串 redis里面的字符串对象都采用SDS结构实现.SDS有别于C风格的字符数组和java的String(定长).这种结构更像C++的String或者java的ArrayList<Character>.长度动态可变. redis的所有键值及字符串字面量都采用这种结构. 这一章节花了十几页去讲这个SDS结构,感觉全是废话,有时间建议去看ArrayList的源码. 第3章 链表 redis的