zoukankan      html  css  js  c++  java
  • redis设计与实现-读书笔记

    第一部分

    字符串

    /*
     * 保存字符串对象的结构
     */
    struct sdshdr {
        
        // buf 中已占用空间的长度
        int len;
    
        // buf 中剩余可用空间的长度
        int free;
    
        // 数据空间
        char buf[];
    };
    

    和C字符串不同,因为SDS在len属性中记录了SDS本身的长度,所以获取一个SDS长度的复杂度仅为O(1)。
    而C中获取长度要遍历一遍字符串

    设置和更新SDS长度的工作是由SDS的API在执行时自动完成的,使用SDS无须进行任何手动修改长度的工作。

    与C字符串不同,SDS的空间分配策略完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现前面所说的缓冲区溢出问题。

    1.空间预分配
    如果增长后长度小于1mb,则free和len大小一样
    如果增加后长度大于1Mb,则每次会分配1mb未使用空间

    通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

    2.惰性空间释放
    惰性空间释放用于优化SDS的字符串缩短操作:
    当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

    因为C中字符串遇到空白将被误认为是字符串结尾
    为了确保Redis可以适用于各种不同的使用场景,SDS的API都是二进制安全的(binary-safe),所有SDSAPI都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。

    这也是我们将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。

    • C字符串和SDS区别

    链表和链表节点

    /*
     * 双端链表节点
     */
    typedef struct listNode {
    
        // 前置节点
        struct listNode *prev;
    
        // 后置节点
        struct listNode *next;
    
        // 节点的值
        void *value;
    
    } listNode;
    

    /*
     * 双端链表结构
     */
    typedef struct list {
    
        // 表头节点
        listNode *head;
    
        // 表尾节点
        listNode *tail;
    
        // 节点值复制函数
        void *(*dup)(void *ptr);
    
        // 节点值释放函数
        void (*free)(void *ptr);
    
        // 节点值对比函数
        int (*match)(void *ptr, void *key);
    
        // 链表所包含的节点数量
        unsigned long len;
    
    } list;
    

    字典

    Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对

    /*
     * 哈希表
     *
     * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
     */
    typedef struct dictht {
        
        // 哈希表数组
        dictEntry **table;
    
        // 哈希表大小
        unsigned long size;
        
        // 哈希表大小掩码,用于计算索引值
        // 总是等于 size - 1
        unsigned long sizemask;
    
        // 该哈希表已有节点的数量
        unsigned long used;
    
    } dictht;
    
    • 哈希表节点
      key属性保存着键值对中的键,而v属性则保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数
    /*
     * 哈希表节点
     */
    typedef struct dictEntry {
        
        // 键
        void *key;
    
        // 值
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
    
        // 指向下个哈希表节点,形成链表
        struct dictEntry *next;
    
    } dictEntry;
    

    /*
     * 字典
     */
    typedef struct dict {
    
        // 类型特定函数
        dictType *type;
    
        // 私有数据
        void *privdata;
    
        // 哈希表
        dictht ht[2];
    
        // rehash 索引
        // 当 rehash 不在进行时,值为 -1
        int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    
        // 目前正在运行的安全迭代器的数量
        int iterators; /* number of iterators currently running */
    
    } dict;
    

    type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的:
    type属性 是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
    privdata属性 则保存了需要传给那些类型特定函数的可选参数。

    /*
     * 字典类型特定函数
     */
    typedef struct dictType {
    
        // 计算哈希值的函数
        unsigned int (*hashFunction)(const void *key);
    
        // 复制键的函数
        void *(*keyDup)(void *privdata, const void *key);
    
        // 复制值的函数
        void *(*valDup)(void *privdata, const void *obj);
    
        // 对比键的函数
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    
        // 销毁键的函数
        void (*keyDestructor)(void *privdata, void *key);
        
        // 销毁值的函数
        void (*valDestructor)(void *privdata, void *obj);
    
    } dictType;
    

    ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用

    除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。

    • 数组+链表的形式解决hash冲突

    • rehash
      rehash的步骤如下:

    1. 为ht[1]分配空间,如果是扩大则分配两倍的空间,如果是缩容则缩小一半的空间

    2. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。

    3. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

    • 渐进式rehash

    1)为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。

    2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。

    3)在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。

    4)随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。

    跳跃表

    跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的

    Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构

    /*
     * 跳跃表
     */
    typedef struct zskiplist {
    
        // 表头节点和表尾节点
        struct zskiplistNode *header, *tail;
    
        // 表中节点的数量
        unsigned long length;
    
        // 表中层数最大的节点的层数
        int level;
    
    } zskiplist;
    
    /*
     * 跳跃表节点
     */
    typedef struct zskiplistNode {
    
        // 成员对象
        robj *obj;
    
        // 分值
        double score;
    
        // 后退指针
        struct zskiplistNode *backward;
    
        // 层
        struct zskiplistLevel {
    
            // 前进指针
            struct zskiplistNode *forward;
    
            // 跨度
            unsigned int span;
    
        } level[];
    
    } zskiplistNode;
    

    zskiplistNode结构

    • 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
    • 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
    • 分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
    • 成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。

    节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
    节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。

    整数集合

    整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现

    整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。

    typedef struct intset {
        
        // 编码方式
        uint32_t encoding;
    
        // 集合包含的元素数量
        uint32_t length;
    
        // 保存元素的数组
        int8_t contents[];
    
    } intset;
    

    每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。
    升级整数集合并添加新元素共分为三步进行:
    1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
    2)将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。
    3)将新元素添加到底层数组里面。

    压缩列表

    压缩列表(ziplist)是列表键和哈希键的底层实现之一
    一个列表或者哈希只包含少数键值对
    会用到压缩列表做底层实现

    压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

    对象

    typedef struct redisObject {
    
        // 类型
        unsigned type:4;
        // 编码
        unsigned encoding:4;
        // 对象最后一次被访问的时间
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
        // 引用计数
        int refcount;
        // 指向实际值的指针
        void *ptr;
    } robj;
    
    • 对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。

    字符串对象

    字符串对象的编码可以是int、raw或者embstr。

    如果一个字符串对象保存的是整数值,类型为int
    如果字符串长度少于32字节.类型为embstr
    如果字符串长度大于32字节,类型为raw

    embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构

    embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。

    释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。

    因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。

    列表对象

    列表对象的编码可以是ziplist或者linkedlist。

    列表对象保存的所有字符串元素的长度都小于64字节;
    列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用linkedlist编码。

    哈希对象

    哈希对象的编码可以是ziplist或者hashtable。

    • 压缩列表

    • Hashtable:使用字典作为底层实现

    集合对象

    集合对象的编码可以是intset或者hashtable。
    intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。

    当集合对象可以同时满足以下两个条件时,对象使用intset编码:

    集合对象保存的所有元素都是整数值;
    集合对象保存的元素数量不超过512个。不能满足这两个条件的集合对象需要使用hashtable编码。

    有序集合对象

    有序集合的编码可以是ziplist或者skiplist。

    ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,
    第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。

    当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:
    ❑有序集合保存的元素数量小于128个;
    ❑有序集合保存的所有元素成员的长度都小于64字节;
    不能满足以上两个条件的有序集合对象将使用skiplist编码。

    skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:

    单机数据库

    通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能——这就是SELECT命令的实现原理。

    • redis数据库是一个键值对库

    Redis的过期键删除策略

    Redis服务器实际使用的是惰性删除和定期删除两种策略

    • 惰性删除
      所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查
      如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除
      如果输入键未过期,那么expireIfNeeded函数不做动作

    • 定期删除策略
      它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

    • 在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中

    • 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影响

    • 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响

    • 当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响

    • 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。

    • 举个例子,如果客户端使用GET message命令,试图访问过期的message键,那么服务器将执行以下三个动作:1)从数据库中删除message键。2)追加一条DEL message命令到AOF文件。3)向执行GET命令的客户端返回空回复。

    • 在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中

    • 通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。

    • 从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性

    rdb持久化

    save和bgsave都可以保存rdb文件

    • RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。

    因为AOF文件的更新频率通常比RDB文件的更新频率高,所以
    如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
    只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。

    • save命令执行时数据库会阻塞

    • 首先,在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个rdbSave调用,防止产生竞争条件。
      其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。

    服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

    • 服务器状态还维持着一个dirty计数器,以及一个lastsave属性
      Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。

    • RDB文件是一个经过压缩的二进制文件,由多个部分组成

    AOF

    AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的
    被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的
    服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态

    AOF持久化

    AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤

    • 不同appendfsync值产生不同的持久化行为

    • 因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态

    • AOF文件载入过程

    AOF重写

    新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

    • 虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为“AOF文件重写”,但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。

    • AOF直接读取数据库,批量生成写入语句, 如果一个列表键包含了超过64个项,那么重写程序会用多条RPUSH命令来保存这个列表,并且每条命令设置的项数量也为64个:

    • 为了解决重写期间造成的数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区

    • 新生成的AOF文件只包含还原当前数据库状态的所有必须命令,所以新的AOF文件不会浪费空间

    多数据库

    • redis主从复制



    主从断线后,从服务器需要重新执行所有的rdb文件
    所以sync命令是一个消耗资源很大的操作

    • Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作
      PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partialresynchronization)两种模式

    • 复制偏移量

    执行复制的双方——主服务器和从服务器会分别维护一个复制偏移量:
    主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
    从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N。

    通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态:

    • 复制积压缓冲区

    当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
    如果offset偏移量之后的数据(也即是偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分重同步操作。
    相反,如果offset偏移量之后的数据已经不存在于复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。

    Redis为复制积压缓冲区设置的默认大小为1MB,如果主服务器需要执行大量写命令,又或者主从服务器断线后重连接所需的时间比较长,那么这个大小也许并不合适。如果复制积压缓冲区的大小设置得不恰当,那么PSYNC命令的复制重同步模式就不能正常发挥作用,因此,正确估算和设置复制积压缓冲区的大小非常重要。

    Sentinel

    • 初始化服务器
      普通服务器在初始化时会通过载入RDB文件或者AOF文件来还原数据库状态,但是因为Sentinel并不使用数据库,所以初始化Sentinel时就不会载入RDB文件或者AOF文件

    • 使用Sentinel专用代码
      启动Sentinel的第二个步骤就是将一部分普通Redis服务器使用的代码替换成Sentinel专用代码
      在Sentinel模式下,Redis服务器不能执行诸如SET、DBSIZE、EVAL等等这些命令,因为服务器根本没有在命令表中载入这些命令。
      PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这七个命令就是客户端可以对Sentinel执行的全部命令了。

    • 初始化Sentinel状态

    • 创建连向主服务器的网络连接
      Sentinel会创建两个连向主服务器的异步网络连接
      一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
      另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道。

    Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息

    • 获取从服务器信息

    • Sentinel只是一个运行在特殊模式下的Redis服务器,它使用了和普通模式不同的命令表,所以Sentinel模式能够使用的命令和普通Redis服务器能够使用的命令不同。

    • Sentinel会读入用户指定的配置文件,为每个要被监视的主服务器创建相应的实例结构,并创建连向主服务器的命令连接和订阅连接,其中命令连接用于向主服务器发送命令请求,而订阅连接则用于接收指定频道的消息。

    • 在一般情况下,Sentinel以每十秒一次的频率向被监视的主服务器和从服务器发送INFO命令,当主服务器处于下线状态,或者Sentinel正在对主服务器进行故障转移操作时,Sentinel向从服务器发送INFO命令的频率会改为每秒一次

    • Sentinel以每秒一次的频率向实例(包括主服务器、从服务器、其他Sentinel)发送PING命令,并根据实例对PING命令的回复来判断实例是否在线,当一个实例在指定的时长中连续向Sentinel发送无效回复时,Sentinel会将这个实例判断为主观下线。

    • 当Sentinel将一个主服务器判断为主观下线时,它会向同样监视这个主服务器的其他Sentinel进行询问,看它们是否同意这个主服务器已经进入主观下线状态。

    • 当Sentinel收集到足够多的主观下线投票之后,它会将主服务器判断为客观下线,并发起一次针对主服务器的故障转移操作。

    集群

    Redis集群通过分片的方式来保存数据库中的键值对:
    集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
    数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)

    发布与订阅

    Redis的发布与订阅功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令组成。
    多个客户端:subscribe news
    一个客户端推送消息:publish news

    • 服务器将所有频道的订阅关系都保存在服务器状态的pubsub_channels属性里面

    发送消息

    当一个Redis客户端执行PUBLISH<channel><message>命令将消息message发送给频道channel的时候,服务器需要执行以下两个动作:
    1)将消息message发送给channel频道的所有订阅者。
    2)如果有一个或多个模式pattern与频道channel相匹配,那么将消息message发送给pattern模式的订阅者。

    查看订阅者数量 pubsub numsub news

    事务

    Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。
    在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

    watch

    WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

    • Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback),即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。

    事务期间宕机

    如果Redis服务器在执行事务的过程中停机,那么根据服务器所使用的持久化模式,可能有以下情况出现:
    如果服务器运行在无持久化的内存模式下,那么重启之后的数据库将是空白的,因此数据总是一致的。
    如果服务器运行在RDB模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的RDB文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的RDB文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。
    如果服务器运行在AOF模式下,那么在事务中途停机不会导致不一致性,因为服务器可以根据现有的AOF文件来恢复数据,从而将数据库还原到一个一致的状态。如果找不到可供使用的AOF文件,那么重启之后的数据库将是空白的,而空白数据库总是一致的。

    lua脚本

    script load "return 2*2"
    evalsha "4475bfb5919b5ad16424cb50f74d4724ae833e72" 0

    二进制位数组

    Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四个命令用于处理二进制位数组

    慢查询日志

    slowlog-log-slower-than选项指定执行时间超过多少微秒(1秒等于1 000 000微秒)的命令请求会被记录到日志上。举个例子,如果这个选项的值为100,那么执行时间超过100微秒的命令就会被记录到慢查询日志;如果这个选项的值为500,那么执行时间超过500微秒的命令就会被记录到慢查询日志。

    slowlog-max-len选项指定服务器最多保存多少条慢查询日志

    config set slowlog-log-slower-than 0
    config set slowlog-max-len 5
    slowlog get
    
  • 相关阅读:
    洛谷2444(Trie图上dfs判环)
    【做题】Codeforces Round #453 (Div. 1) D. Weighting a Tree——拆环
    【做题】POJ3469 Dual Core CPU——第一道网络流
    【做题】atc_cf17-final_E
    【做题】spoj4060 A game with probability——dp
    Matlab AlexNet 识别花
    minutia cylinder code MCC lSSR 匹配算法
    Gabor filter for image processing and computer vision
    MATLAB 去掉数组里面不要的元素
    NBIS指纹特征提取与匹配软件使用
  • 原文地址:https://www.cnblogs.com/Baronboy/p/15213110.html
Copyright © 2011-2022 走看看