Redis学习日志(三)

16.类型检查与命令多态

redis操作键的命令分两种
①可对任意类型的键执行:DEL EXPIRE RENAME TYPE OBJECT
②只对特定类型键执行:
字符串键:SET GET APPEND STRLEN
哈希键:HDEL HSET HGET HLEN
列表键:RPUSH LPOP LINSERT LLEN
集合键:SADD SPOP SINTER SCARD
有序集合键:ZADD ZCARD ZRANK ZSCORE 

命令多态,会在执行时根据值对象的编码方式来选择正确的执行函数。
对列表对象执行LLEN时,若为ziplist编码,则使用ziplistLen函数,若为linkedlist编码,则用listLength函数

17.内存回收

Redis在对象系统中构建了一个引用计数实现内存回收机制,通过跟踪对象的引用计数信息,完成释放对象和内存回收
1)当新创建一个对象时,对象的引用计数值初始化为1
2)当对象被别处引用时,对象的引用计数值+1
3)当对象被引用解除时,对象的引用计数值-1
4)当对象的引用计数值=0,释放对象所占用的内存

18.对象空转时长

redisObject结构包含一个lru属性,记录对象最后一次被命令程序访问的时间
OBJECT IDLETIME命令可输出指定键的空转时长
例:
    redis> SET msg "hello world"
    OK
    //等一会儿
    redis> OBJECT IDLETIME msg 
    (integer)20
    //再等一会儿
    redis> OBJECT IDLETIME msg 
    (integer)120
    redis> GET msg 
    "hello world"
    redis> OBJECT IDLETIME msg 
    (integer)0
当redis服务器回收内存算法基于LRU时,当服务器占用内存超过某个界限,将优先释放空转时间较高的键

19.小结

Redis底层数据结构:简单动态字符串(SDS) 双端链表(linkedlist) 
                   字典(hashtable) 跳跃表(zskiplist) 
                   整数集合(intset) 压缩列表(ziplist)

对象系统结构:         编码方式:
string              int embstr raw
list                ziplist linkedlist
hash                ziplist hashtable
set                 intset hashtable
zset                ziplist skiplist

20.服务器数据库

Redis服务器所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中
db数组每个项都是redis.h/redisDb结构,每个redisDb结构代表一个数据库

struct redisServer{
            //....
            int dbnum;//服务器数据库数量 默认=16
            redisDb *db;//数组,保存所有数据库
            //....
        }
Redis客户端默认为0号数据库,通过SELECT命令切换目标数据库。
redisClient结构的db属性记录客户端当前的目标数据库,指向redisDb的指针
        typedef struct redisClient{
            //.....
            redisDb *db;
            //...
        }redisClient;

redisClient.db指向redisServer.db数组中其中一个元素,即客户端的目标数据库。

typedef struct redisDb {
        // 数据库键空间,保存着数据库中的所有键值对
        dict *dict;                 /* The keyspace for this DB */
        // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
        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 */
        // 正在被 WATCH 命令监视的键
        dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
        struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
        // 数据库号码
        int id;                     /* Database ID */
        // 数据库的键的平均 TTL ,统计信息
        long long avg_ttl;          /* Average TTL, just for stats */
    } redisDb;

数据库键空间:
服务器中的每个数据库都由一个redis.h/redisDb 结构表示,
其中redisDb中的dict字典保存了数据库中的所有键值对,
这个dict字典称为键空间。

对数据库键进行更新的具体操作:图9-8键空间HSET更新键
这里写图片描述
设置过期时间:
EXPIRE 将键key生存时间设置为ttl秒
PEXPIRE 将键key生存时间设置为ttl毫秒
EXPIREAT 将键key生存时间设置为timestamp指定的秒时间戳
PEXPIREAT 将键key生存时间设置为timestamp指定的毫秒时间戳
在执行时都会先转换调用PEXPIREAT命令实现

redisDb结构中 expires字典保存数据库中所有键的过期时间,此为过期字典
typedef struct redisDb{
//…
dict *expires ;//过期字典
//…
}redisDb;
过期字典的键是一个指针,指向键空间中的某个键对象
过期字典的值是一个long long 类型的整数,保存了对应的过期时间(毫秒精度的UNIX时间戳)

使用PERSIST命令移除过期时间,在过期字典中删去记录。
使用TTL命令以秒为单位返回键的剩余生存时间,PTTL返回以毫秒为单位
(通过在过期字典中搜索过期时间与当前时间相减获得返回)

过期键删除策略:
    1)定时删除:
        通过使用定时器使过期键尽可能快的被删除,释放内存,但对CPU时间不友好,
        在过期键多的情况下,可能会占有比较久的CPU时间,而内存不紧张时应该注重
        优先处理客户端请求
    2)惰性删除:
        在需要取出键时,才进行过期检查,但对内存不友好,占有内存累积不释放
        若有大量过期键没有被访问到,则会一直占用内存
    3)定期删除:
        每隔一段时间执行一次过期键操作,通过限制删除操作执行的时长和频率来减少对CPU时间影响
        难点在于确定时长和频率
Redis过期键策略实现:使用惰性删除和定期删除策略配合
    1)惰性删除策略的实现:
        所有读写数据库的Redis命令在执行前会调用db.c/expireIfNeeded函数对输入键进行检查
        若输入键已过去,则删除
    2)定期删除策略的实现:
        每当Redis的服务器周期性操作redis.c/serverCron函数执行时,redis.c/activeExpireCycle函数会被调用
        在规定的时间内分多次遍历服务器中的各个数据库,从expires字典(过期字典)中随机检查部分键的过期时间

伪代码过程如下:

    {
            DEFAULT_DB_NUMBERS=16 //默认检查的数据库数量
            DEFAULT_KEY_NUMBERS=20//默认每个数据库检查的键数量
            current_db=0 //全局变量 记录检查进度
            def activeExpireCycle():
                /*初始化要检查的数据库数量
                  优先以数据库实际数量和默认值中较小的为准
                */
                if server.dbnum < DEFAULT_DB_NUMBERS 
                    db_numbers = server.dbnum
                else
                    db_numbers = DEFAULT_DB_NUMBERS

                for i in range(db_numbers)://遍历各个数据库
                    /*
                        若current_db的值等于服务器数据库数量
                        将current_db重置为0,开始新一轮遍历
                        由于函数执行是以限定时长为标准的,
                        每次运行不一定都刚好遍历完所有数据库一次
                        即每次全局变量current_db的值不一定从0开始
                    */
                    if current_db == server.dbnum
                        current_db=0
                    redisDb = server.db[current_db] //获取当前要处理的数据库
                    current_db+=1 //数据库索引加一
                    for j in range(DEFAULT_KEY_NUMBERS)//检查数据库键
                        if redisDb.expires.size()==0 //若此数据库中无定时键
                            break
                        key_with_ttl = redisDb.expires.get_random_key()//随机获取一个定时键
                        if is_expired(key_with_ttl) //检查是否过期
                            delete_key(key_with_ttl)
                        if reach_time_limit()//达到函数运行时长了则退出
                            return 
                }   
activeExpireCycle工作模式如下:
    每次函数运行时从一定量的数据库中随机取出一定量的定时键进行过期检查并删除
    current_db记录当前检查进度(到哪个数据库了)

AOF、RDB和复制功能对过期键的处理
    1)生成RBD文件
        执行SAVE/BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存进去
    2)载入RDB文件
        服务器以主服务器模式运行,在载入RDB文件时会对文件中的键进行检查,忽略已过期的键
        服务器以从服务器模式运行,在载入RDB文件时会全部载入,当主从服务器进行数据同步时,从服务器的数据库会被清空
    3)AOF文件写入
        当过期键被删除后,会向AOF文件追加一条DEL命令,显式地记录该键已删除
        当客户端访问过期的message键时,执行以下3个动作
        删除message键、追加DEL message到AOF文件、返回客户端空回复
    4)AOF重写
        在对AOF进行重写时,会对数据库中的键进行检查,已过期的键不会被写入文件
    5)复制    
        服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制
            主服务器删除一个过期键后,显式地向所有从服务器发送DEL命令告知
            从服务器只有接收到主服务器的DEL命令后才删除过期键,否则不会对过期键进行删除操作
            注意:客户端对从服务器进行读命令,即便键已过期,从服务器还是将其当未过期对待,返回给客户端

过程图见图9-17主从服务器删除过期键
这里写图片描述

Q1:从服务器不会删除过期键,而是等待主服务器发现键过期后传来DEL命令,再删除则此过程中可能会返回给客户端过期键。对于此现象,何解?
猜测:无其他操作,客户端获得过期键,不过由于只是读,不会造成太大影响,因为在写操作时,会在主服务器进行检查并且反馈信息。
Q2:客户端在从服务器获得过期键值100 未过期键值80,累加后赋予新键写入到主服务器 怎么办? (本该是80,现在却是180)
猜测:与写有关的事务操作,全在主服务器进行,即在主服务器读,再赋值,再写入。即可解决过期键问题。
或是在主服务器操作的写事件执行前由业务执行,在主服务器再读一次。也可解决。

小结回顾:见图9-20数据库相关小结
这里写图片描述

21.RDB持久化

    通过保存数据库中的键值对来记录
    RDB持久化可以手动执行也可以根据服务器配置选项定期执行,将某时间点上的数据库状态保存到RDB文件中
    RDB文件是一个经过压缩的二进制文件

    RDB文件的创建与载入:
        生成RDB文件的两个命令-- SAVE BGSAVE
            SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止
            BGSAVE命令派生一个子进程,由子进程负责创建RDB文件
            创建的工作实际由rdb.c/rdbSave函数完成
        在服务器启动时会自动检测RDB文件,进行载入,由rdb.c/rdbLoad函数完成
        (当AOF持久化开启时,会优先使用AOF文件还原数据库状态)

    可以设置自动间隔性执行BGSAVE
        若在配置文件中设置
        save 900 1 //900秒内数据库进行了至少1次修改
        save 300 10//300秒内数据库进行了至少10次修改
        save 60 10000//60秒内数据库进行了至少10000次修改
        则只要满足其中一个就会执行BGSAVE命令。
    服务器状态redisServer结构的saveparams属性记录设置的save保存条件    
        struct redisServer{
            //...
            struct saveparam *saveparams;//记录保存条件的数组
            //...
        };
        struct saveparam{
            //秒数
            time_t seconds;
            //修改数
            int changes;
        };
    例:
        |redisServer|
        |...|            saveparams[0] saveparams[1] saveparams[2]
        |saveparams |--> |seconds=900| |seconds=300| |seconds=60|
        |...|            |changes=1  | |changes=10 | |changes=10000|

    服务器状态维持一个dirty计数器及lastsave属性
        dirty计数器记录距离上一次成功执行SAVE/BGSAVE后,服务器对数据库状态进行了多少次修改
        lastsave属性是一个UNIX时间戳,记录上次成功执行SAVE/BGSAVE命令的时间
        struct redisServer{
            //...
            long long dirty;//修改计数器
            time_t lastsave ;//上次执行保存的时间
            //...
        };
    检查保存条件是否满足:
        Redis服务器周期性函数serverCron默认每隔100毫秒执行一次
        其中一项功能是检查save选项设置的条件是否满足。
    RDB文件结构:
            |REDIS|db_version|databases|EOF|check_sum|
        db_version长度为4字节,一个字符串表示的整数记录RDB文件的版本号
        databases部分包含零或任意多个数据库,及其中的键值对数据
        EOF常量长1字节,标志文件正文内容的结束
    check_sum长8字节无符号整数,保存一个校验和,以供检查文件是否损坏

    database结构:
            |SELECTDB|db_number|key_value_pairs|
        SELECTDB长1字节,标志后面的是数据库号     
        db_number数据库号,服务器根据此号调用SELECT命令切换数据库,使后续键值对正确载入
        key_value_pairs保存数据库中的所有键值对数据
    例:两个非空的数据库 0号和3号
        |REDIS|db_version|SELECTDB|0|key_value_pairs|SELECTDB|3|key_value_pairs|EOF|check_sum|

    key_value_pairs结构:
            |EXPIRETIME_MS|ms|TYPE|key|value|
        EXPIRETIME_MS表示键值对是带过期时间的
        ms表示该键值对的过期时间
        根据TYPE类型保存value的结构类型
        具体过程与对象类型根据编码保存键值对方式相似

22.AOF持久化

通过保存Redis服务器所执行的写命令来记录数据库状态
AOF持久化功能实现分三步骤:命令追加 文件写入 文件同步

1)命令追加
        当服务器执行完一个写命令,会以协议格式将写命令追加到服务器状态的aof_buf缓冲区末尾
            struct redisServer{
                //...
                sds aof_buf;//AOF缓冲区
                //..
            };
2)AOF文件写入和同步
        Redis服务器进程是一个事件循环loop,
        循环中的文件事件负责接收客户端命令请求及回复
        时间事件负责执行定时运行的函数
        在服务器处理文件事件时可能会执行写命令,feedAppendOnlyFile函数使一些内容追加到aof_buf缓冲区
        所以服务器每次结束一个事件循环之前会调用flushAppendOnlyFile函数
        考虑是否需要将aof_buf缓冲区内容写入到AOF文件
伪代码过程如下:{
                    def eventLoop()
                    while true
                        /*
                            处理文件事件,接收命令请求以及发送命令回复
                            处理时可能会有新内容追加到aof_buf缓冲区
                        */
                        processFileEvents()
                        processTimeEvents()//处理时间事件
                        flushAppendOnlyFile()//考虑是否需要将aof_buf缓冲区内容写入到AOF文件
                }

flushAppendOnlyFile函数行为由服务器(redis.conf配置文件)配置的appendfsync选项决定

选项值行为
always将aof_buf缓冲区内容全写入同步到AOF文件
everysec(默认)将aof_buf缓冲区内容全写入AOF文件,与上次同步时间超过1秒则进行AOF文件同步
no将aof_buf缓冲区内容全写入到AOF文件,但此时不同步

三种选项值详细描述见图11-1AOF持久化效率和安全性
这里写图片描述

AOF文件载入与数据还原:
            1)创建一个不带网络连接的伪客户端(fake client),用来执行载入AOF文件
            2)从AOF文件中分析并读取出一条写命令
            3)用伪客户端执行写命令
            4)重复步骤2-3直到AOF文件中所有写命令被执行处理
        AOF重写:
            不断地向AOF文件中写入执行命令会导致文件体积不断加大,通过重写AOF文件以新文件代替旧文件不包含冗余命令
            如:
            redis> RPUSH mylist "A" "B" //{"A" "B"}
            (integer)2                   
            redis> RPUSH mylist "C"     //{"A" "B" "C"}
            (integer)3                   
            redis> RPUSH mylist "D" "E" //{"A" "B" "C" "D" "E"}
            (integer)5
            redis> LPOP mylist          //{"B" "C" "D" "E"}
            "A"
            redis> LPOP mylist          //{"C" "D" "E"}
            "B"
            redis> RPUSH mylist "F" "G" //{"C" "D" "E" "F" "G"}
            (integer)5
            需要向AOF文件写入六条命令(只记录写命令)
            可以通过直接从数据库中读取键mylist的值,用一条RPUSH mylist "C" "D" "E" "F" "G" 代替

aof.c/rewriteAppendOnlyFile函数 重写过程伪代码:

{
                    def aof_rewrite(new_aof_file_name)
                        f=creat_file(new_aof_file_name)//创建新AOF文件
                        for db in redisServer.db    //遍历数据库
                            if db.is_empty()        //忽略空数据库
                                break
                            f.write_command("SELECT"+db.id)
                            for key in db           //遍历数据库中所有键
                                if key.is_expired() //忽略已过期键
                                    continue
                                if      key.type==String    //根据键的类型进行重写
                                        rewrite_string(key)
                                elseif  key.type==List
                                        rewrite_list(key)
                                elseif  key.type==Hash
                                        rewrite_hash(key)
                                elseif  key.type==Set
                                        rewrite_set(key)
                                elseif  key.type==SortedSet
                                        rewrite_sorted_set(key)
                                if key.have_expire_time()   //对带有过期时间的定时键写入其过期时间
                                    rewrite_expire_time(key)
                        f.close()
                }
                {
                    def rewrite_list(key)
                        item1,item2....itemN=LRANGE(key,0,-1)//获取所有元素
                        f.write_command(RPUSH,key,item1,item2...itemN)//使用RPUSH命令重写
                } //其他相似操作过程伪代码见图11-2AOF重写过程函数伪代码

这里写图片描述

            AOF重写生成的新文件只包含当前数据库状态所必须的命令,不会造成硬盘空间浪费。

—-MARK—————-
/*
AOF重写条件:
(在循环检查事件servCron函数中进行判断执行rewriteAppendOnlyFileBackground())
(1)没有bgsave命令在进行。
(2)没有bgrewriteaof在进行。
(3)当前aof文件大小大于server.aof_rewrite_min_size,注意它的默认值为1MB
*/
—-MARK—————-



        注意:
            由于一份键值对的数据可能会很长,若重写时均采用以一条命令写入,可能会造成缓冲区溢出,
            因此在处理时会先检查键所包含的元素数量,若超过redis.h/REDIS_AOF_REWRITE_ITEM_PER_CMD常量
            则会使用多条命令记录键的值。
    AOF后台重写:
        aof_rewrite函数可以很好完成一个新AOF文件的任务,但会进行大量写入操作,调用此函数的线程会被长时间阻塞
        而Redis服务器是使用单个线程处理命令请求。因此,采用子进程调用执行的方式。

        aof.c/rewriteAppendOnlyFileBackground函数进行后台重写。
            1)子进程进行AOF重写期间,父进程可继续处理命令请求
            2)子进程带有父进程的数据副本,不使用线程可以避免使用锁。
        当子进程进行AOF重写期间,服务器可能继续对现有数据进行修改,因此设置一个AOF重写缓冲区
        当子进程开始使用后,Redis服务器的写命令会发送给"AOF缓冲区""AOF重写缓冲区"
        优点:
            1)AOF缓冲区会定时被写入和同步到AOF文件,对现有AOF文件处理工作正常进行
            2)创建子进程后,服务器执行的写命令记录到AOF重写缓冲区中,当子进程完成AOF重写后,
              向父进程发送信号,然后父进程将AOF重写缓冲区内容写入到新AOF文件并完成新旧AOF文件替换。
        后台重写完成后调用backgroundRewriteDoneHandler函数--> aofRewriteBufferWrite函数
        将aof重写缓冲区中的内容写入到aof重写文件中。

———————–Mark———————-
/*
子进程已经在重写AOF文件了
服务器为什么还要把写命令发给AOF缓冲区
(为什么不只发给AOF重写缓冲区)
*/
/*
个人理解,
在进行AOF文件持久化时对于新的写命令,
会记录到AOF缓冲区或AOF重写缓冲区
(具体是哪个缓冲区应该根据当前是否是选择采用重写AOF文件)
此理解的正确性,在之后看过源码再决定。此处先Mark
*/
/*
理解2:2017/7/26
aof.c/feedAppendOnlyFile函数中,确实有对两个缓存都加入写命令,
原因可能是aof写入是作为比较常规的方式,而重写aof在一定条件下进行,
因此对常规aof_buf中保留一份写命令,也可以防止重写失败等。
当重写aof进行完毕以后,即aof持久化成功,两者缓冲区都会清空的。
*/
———————–Mark———————-
“`

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值