《redis设计与实现》-读书笔记

文章目录

  • 常见数据结构
  • SDS
  • 链表
  • 字典
    • 哈希算法
    • rehash与渐进式rehash
        • rehash步骤:
        • rehash条件
        • 渐进式rehash
  • 跳跃表
  • 整数集合
      • 集合升级
  • 压缩列表
  • 快速列表
        • 为什么使用快速列表?
        • ziplist切割大小
        • 压缩深度
  • 对象
    • 内存回收
    • 对象共享
  • 数据库
  • RDB持久化
    • 创建与载入
  • AOF持久化
        • AOF重写
  • 事件
  • 复制
    • 旧版主从复制实现
    • 新版主从复制实现
    • 主从复制实现
    • 心跳检测
  • 哨兵模式(Sentinel)
  • 集群
  • 发布和订阅
  • Stream
  • 事务
    • 事务实现
    • ACID性质
  • 排序
  • 慢日志
  • 监视器

常见数据结构

  1. 动态字符串SDS
  2. 链表
  3. 字典
  4. 跳跃表
  5. 整数集合
  6. 压缩列表
  7. 快速列表
  8. 对象

SDS

struct sdshdr {
    int len;
    int free;
    char buf[];
}

sds优点

  1. 常数复杂度获取字符串长度
  2. 防止缓冲区溢出
  3. 减少更新时内存重分配次数
    • 空间预分配
    • 惰性空间释放
  4. 二进制而全
  5. 兼容部分C字符串函数

链表

  • 列表键底层实现
  • 发布订阅、慢查询、监视器
struct listNode {
    prev *listNode;
    next *listNode;
    value interface{} 
}

struct linkedlist {
    head *listNode
    tail *listNode
    len int64
    dup func() // 节点值复制函数
    free func() // 节点值释放函数
    match func() // 节点值对比函数
}

字典

  • redis数据库底层实现
  • 哈希键底层实现
//哈希表结构
struct dictht {
    // 哈希表数组
    table []*dictEntry
    size int64 // 哈希表大小
    sizemask int64 // 哈希表大小掩码,用于计算索引值,总是等于size-1
    used int64 // 该哈希表已有节点数量
}

//哈希表节点结构
struct dictEntry {
    key interface{}
    v interface{}
    next *dictEntry //使用链地址法解决键冲突问题
}
struct dictType {
    hashFunction func //计算哈希值函数
    keyDup func // 复制键函数
    valDup func // 复制值函数
    keyCompare func // 对比键的函数
    valDestructor func // 销毁值的函数

}

// redis 字典数据结构
struct dict {
    type *dictType // 类型特定函数
    privdata interface{} // 私有数据
    ht [2]dictht // 哈希表,字典只使用ht[0]哈希表,ht[1]只会在对ht[0]进行rehash时使用
    rehashidx int //rehash索引时使用,标识rehash进度,未rehash时默认-1
}

哈希算法

字典插入一个新的键值对,根据key计算哈希值和索引值,根据索引值将新哈希表节点放在哈希表数组指定索引上:

// 使用字典哈希函数,计算key的哈希值
hash = dict.type.hashFunction(key)
//使用哈希表的sizemask和哈希值,计算索引值。
index = hash & dict.ht[x].sizemask

redis使用MurmurHash算法:https://github.com/aappleby/smhasher

rehash与渐进式rehash

为保证负载因子维持一定范围内,当哈希表键值对数量过多或过少,需要对哈希表大小进行相应扩容和缩容,通过rehash(重新散列)操作来完成

rehash步骤:
  1. 为ht[1]哈希表分配空间
    • 扩容操作,ht[1]大小是第一个大于等于ht[0].used * 2的2^n
    • 缩容操作,ht[1]大小是第一个大于等于ht[0].used的2^n
  2. 将ht[0]中键值对rehash到ht[1]上,即重新计算key的哈希值和索引值
  3. ht[0]数据全量迁移到ht[1]后,ht[1]替代ht[0],ht[1]清空
rehash条件

扩容条件:

  1. 未执行bgsave 或 bgrewriteaof命令,且负载因子>=1
  2. 正在执行bgsave或bgrewriteaof命令,且负载因子>=5
    缩容条件:
  3. 负载因子 < 0.1 进行缩容
// 负载因子=哈希表易保存节点数量/哈希表大小
load_factor=ht[0].used / ht[0].size
渐进式rehash

步骤:

  1. 为ht[1]哈希表分配空间
  2. 字典中设置索引计数器rehashidx=0,表示开始rehash
  3. rehash期间,每次对字典增/删/查/改,顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],完成后rehashidx++
  4. ht[0]所有数据rehashht[1]后,重置rehashidx = -1,标识rehash完成

渐进式rehash期间,删/查/改会在两个哈希表上进行;新增操作在ht[1]上

跳跃表

是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,达到快速访问节点目的。支持平均O(logN),最坏O(N)复杂度查找,可替代AVL树: 跳跃表原理和代码实现

  • 有序集合键的底层实现
  • 集群节点中用作内部数据结构
// 跳跃表节点
struct zskiplistNode {
    // 层
    level []zskiplistLevel
    backward *zskiplistNode //后退指针
    score float64 // 分值
    robj interface{} //成员对象
}

// 层节点
struct zskiplistLevel {
    forward *zskiplistNode // 前进指针
    span uint // 跨度
}

// 跳跃表结构
struct zskiplist {
    header *zskiplistNode // 表头节点
    tail *zskiplistNode // 表尾节点
    length int64 // 表中节点数量
    level int // 表中层数最大的节点的层数
}
    • 层的数量越多,访问其他节点速度越快
    • 新建节点时,根据幂次订阅随机1-32作为level数组大小,即层的高度
  1. 前进指针
  2. 跨度(span)
    • 跨度越大,节点间距越远
    • 指向null的前进指针跨度为0
    • 跨度用于计算排位(rank):查找某节点中,沿途访问过的所有层的跨度累计得到的结果就是目标节点在跳跃表中的排位
  3. 后退指针
    • 每个节点只有一个后退指针,每次后退至前一个节点
  4. 分值和成员
    • 跳表中所有节点按分值从小到大排列
    • 同一个跳表中,各节点成员对象必须唯一,但分值允许相同,分值相同节点按成员对象字典序排序

整数集合

  • redis集合键的底层实现(hashtable也是集合键的实现之一)
struct intset {
    encoding uint32 //编码方式
    length uint32 // 整数集合包含的元素数量
    // 整数集合元素都是contents的数组项,按值从小到大排列,不包含重复项
    // contents类型取决于encoding的值:
    // INTSET_ENC_INT16
    // INTSET_ENC_INT32
    // INTSET_ENC_INT64
    contents []encoding 
}

集合升级

新元素入整数集合前,若新元素类型比所有元素类型都要大时,整数集合需先升级,在添加
步骤:

  1. 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
  2. 将底层数组现有所有元素都转换成与新元素相同的类型,并将类型转换后元素放在正确位上,且保证数组有序性
  3. 将新元素添加到底层数组中
    好处:
  4. 提升类型灵活性
  5. 节约内存:既保证让集合能同时保存三种不同类型的值,又可确保升级操作只会在有需要时进行,尽量节省内存

整数集合不支持降级,对数组升级后,编码始终保持升级后状态

压缩列表

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

  • 列表键和哈希键的底层实现之一
  • 当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现
  • 当一个哈希只包含少量键值对,比且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希的底层实现
// 压缩列表结构
struct zip1ist {
    zlbytes uint32 // 记录整个压缩列表占用的内存字节数
    zltail uint32 // 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:根据该偏移量,可直接确定表尾节点地址
    zllen uint16 // 记录压缩列表包含的节点数量
    entryX []zip1istNode // 列表节点
    zlend uint8 //特殊值0xFF,标记压缩列表的末尾
}

struct zip1istNode {
    //记录了压缩列表中前一个节点的长度,该属性的长度可以是1字节或者5字节
    // 压缩列表从尾到头遍历操作使用该属性实现
    //前一节点长度 < 254字节,previous_entry_length=1字节
    //前一节点长度 >= 254字节,previous_entry_length=5字节
    previous_entry_length byte

    // 记录了节点的content属性所保存数据的类型和长度
    // 值的最高位是00、01、10,标识content属性类型是字节数组,数组长度由encoding编码除去最高两位后的其他位值
    // 值的最高位是11,标识content属性类型是数字
    encoding byte 

    // 保存节点的值,值的类型和长度由encoding决定
    content []byte or int
}

连锁更新

  • 向压缩列表插入或删除节点时,可能会触发连锁更新
  • 新插入节点后面的节点e1的previous_entry_length现在是1字节,无法保存5字节长度,需要进行空间重分配,e1长度变化后,那么e1后的节点也需要更新previous_entry_length,扩展e2也会引发对e3的扩展…知道eN。这种在特殊情况下产生连续多次空间扩展操作成为连锁更新。
  • 最坏情况下需要对压缩列表执行N次空间重分配,每次空间重分配的最坏复杂度是O(N),所以连锁更新最坏复杂度是O(N^2)

快速列表

  • redis3.2后列表键的底层实现之一,用来替代ziplist和链表(linkedlist)
  • quicklist是ziplist和linkedlist的结合体,将linkedlist按段切分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针串接
//快速列表节点结构
struct quicklistNode {
    prev *quicklistNode
    next *quicklistNode
    zi *ziplist // 指向压缩列表
    size int32 // ziplist的字节总数
    count int16 // ziplist的元素总数
    encoding byte // 存储形式,表示ziplist是否压缩和使用的压缩算法,1未压缩;2使用LZF算法压缩
}

// 压缩列表结构-略
struct ziplist{}

// 表示一个被压缩后的ziplist
struct ziplist_compressed {
    size int32 // 压缩后的ziplist大小
    compressed_data []byte  //存放压缩后ziplist字节数组
}

//快速列表结构
struct quicklist {
    head *quicklistNode // 列表头节点
    tail *quicklistNode // 尾节点
    count int64 // 元素总数
    nodes int  // ziplist节点个数
    compressDepth int  // lzf压缩算法深度
}
为什么使用快速列表?

链表的附加空间相对太高,prev和next占去16字节,另外每个节点内存都是单独分配,加剧内存碎片化。因此定义quicklist将链表和压缩列表结合起来。为进一步节约内存,还应用LZF算法对ziplist进行压缩存储。

ziplist切割大小
  • quicklist内部默认定义的ziplist大小为8k, 超出后重新创建ziplist
  • 切割大小由参数list-max-ziplist-size控制
压缩深度

压缩深度由参数list-compress-depth控制,默认为0
0: 所有节点都不压缩
1: quicklist两端第一个ziplist不进行压缩,支持快速push/pop
2: 两端各自2个ziplist不压缩,中间压缩
n: 两端各自n个ziplist不压缩,中间压缩

对象

对象类型和对应的编码

  • 字符串对象
    • int
    • raw
    • embstr
  • 列表对象
    • ziplist
    • linkedlist
    • quicklist
  • 哈希对象
    • ziplist: 保存同一键值对的两个节点相邻,键节点在前,值节点在后
    • hashtable
  • 集合对象
    • intset
    • hashtable: 字典键保存,字典值为null
  • 有序集合对象
    • ziplist: 同一元素在压缩列表中相邻,第一个节点保存元素成员,第二个节点保存元素分值,压缩列表内元素按分值从小到大排列
    • zskiplist
struct redisObject {
    type uint //对象类型 

    // 对象底层数据结构类型由encoding决定
    // REDIS_ENCODING_INT long类型的整数
    // REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
    // REDIS_ENCODING_RAW 简单动态字符串
    // REDIS_ENCODING_HT 字典
    // ...
    encoding uint 

    ptr *pointer // 指向底层实现数据结构的指针

    refcount int // 引用计数
    
    ...
}

OBJECT ENCODING [keyName] 查看对象的编码类型

内存回收

redis通过引用计数实现内存回收机制

  • 创建新对象,refcount被初始化为1
  • 对象被新程序使用,refcount+1;不再被程序使用,refcount-1
  • 当refcount=0,对象所占内存被释放

对象共享

引用计数refcount还具有对象共享作用,让多个键共享同一个值对象。

数据库

struct redisServer {
    // ...

    db  *redisDb // db数组,保存着服务器中所有数据库

    dbnum int //服务器的数据库数量,默认为16
}

struct redisDb {
    dict *dict // 数据库键空间,保存着数据库中的所有键值对
    
    // ecxpires为过期字典,保存数据库中所有键的过期时间
    // 过期字典的键为指针,指向键空间中某个键对象,值为long类型整数,为毫秒级时间戳
    ecxpires *dict
}

select [dbIdx] 切换目标数据库
flushdb 清空当前数据库的键空间中所有的键值对
expire/pexpire 设置生存时间
expireat/pexpireat 设置过期时间,时间戳格式
persist 移除过期时间

redis过期键删除策略:

  • 惰性删除:所有读写key时判断key是否已经过期,过期则删除键
  • 定期删除

RDB持久化

创建与载入

创建rdb

  • save 同步阻塞生成rdb文件
  • bgsave 子进程异步生成rdb文件。bgsave期间,禁止重复save/bgsave,防止产生竞争条件;bgrewriteaof会延迟到bgsave后执行

自动间隔性创建
通过save设置多个保存条件,只要有一个满足,服务器就会执行bgsave命令

// redis.conf 中可设置save
save 900 1 // 服务器在900s内,对数据库进行了 >= 1次修改
save 300 10 // 服务器在300s内,对数据库进行了 >= 10次修改
save 60 10000 // ...
// 就是【数据库】中提到的服务器数据结构呀
struct redisServer {
    // ...
    // db  *redisDb // db数组,保存着服务器中所有数据库
    // dbnum int //服务器的数据库数量,默认为16

    saveparams *[]saveparam // 记录了保存条件数组

    dirty int64 // 计数器,记录距离上次成功执行save/bgsave后服务器对数据库状态进行了多少次修改
    lastsave int // 时间戳,记录服务器上一次成功执行save/bgsave的时间
}

// save选项设置的保存条件结构
struct saveparam {
    seconds int // 秒数
    changes int //修改数
}

服务器周期性函数默认100ms执行一次,根据dirty、lastsave、saveparams检查save选项设置条件是否满足

载入rdb

  • 若服务器开启了AOF,优先使用AOF文件还原数据库状态
  • 若未开启AOF,服务器启动检测存在rdb文件,存在则自动载入,载入期间服务器处于阻塞状态

rdb文件结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IccLSR5a-1643116099954)(./rdb_file_struct.jpg)]
RDB持久化之RDB文件结构

od -c dump.rdb # 可以打印输出文件内容

AOF持久化

aof通过保存redis服务器执行的写命令来记录数据库状态

持久化实现

struct redisServer {
    // ...
    aof_buf sds // AOF缓冲区
}
  1. 命令追加:以协议格式将写命令追加到服务器aof_buf缓冲区末尾
  2. 文件写入:服务器将aof_buf缓冲区中内容写入OS缓冲区中
  3. 文件同步:将OS缓冲内容写入到AOF文件

文件的写入和同步行为有redis.conf中appendfsync控制:

  • always: 将aof_buf缓冲区中所有内容写入并同步到AOF文件
  • everysec: 默认选项,将aof_buf内容写入到OS buf, 如果上次同步AOF文件时间距离 - 当前时间 > 1s,将OS buf同步到AOF文件中,且这个同步由单独线程执行
  • no: 将aof_buf写到OS buf,但不进行同步,同步时间由OS决定

载入AOF文件

  • 服务器创建伪客户端(redis命令只能在客户端上下文中执行)
  • 伪客户端循环从AOF文件中读取一条写命令并执行,知道所有命令处理完毕
AOF重写
  • BGREWRITEAOF命令异步执行AOF文件重写
  • 实现原理:从数据库中读取键当前的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令
  • 重写过程是由单独子线程执行,服务器在重写期间不会阻塞

服务器当前状态和重写后AOF保存状态不一致?
服务器设置一个AOF重写缓冲区,重写期间,写命令会追加到AOF缓冲区(aof_buf)和重写缓冲区。当完成重写后,服务器主进程:

  1. 将aof重写缓冲区写入到新AOF文件中
  2. 对新AOF文件改名,并原子地覆盖现有AOF文件,完成替换

事件

  • 文件事件
    • 文件事件处理器:套接字、I/O多路复用程序、文件事件分排器、事件处理器
  • 事件事件
    • 定时事件
    • 周期性事件
    • 无序链表

复制

命令:SLAVEOF

旧版主从复制实现

  • 同步:从的数据库状态更新至主状态
    同步:SYNC命令,步骤:
  1. 从向主发送sync命令
  2. 主执行BGSAVE,生成RDB文件,并用一个缓冲区记录生成期间的写命令
  3. 主将rdb推给从,从载入rdb
  4. 主将缓冲区内容推给从,从执行
  • 命令传播:主变更,从保持一致

缺陷:主从断线后重复制,sync效率过低

新版主从复制实现

使用PSYNC替代SYNC来执行复制时的同步操作

  • 完整重同步:处理初次复制情况
  • 部分重同步 :主将主从断线期间的写命令发给从,从执行,恢复状态一致
    实现:
  • 复制偏移量:主从都维护各自的复制偏移量offset
  • 主的复制积压缓冲区:
    • 固定长度(1MB)的队列,命令传播时,主也会将写命令入到该缓冲区中,复制积压缓冲区会为队列中每个字节记录相应的复制偏移量
    • 从重连上主,从将自己offset给主, 主根据offset决定执行何种同步操作
      • offset之后的数据仍在复制积压缓冲区中,执行部分重同步操作
      • offset之后数据不再缓冲区中,执行完整重同步操作
  • 机器运行ID

主从复制实现

  1. 设置主服务器的地址和端口(slaveof)
  2. 建立套接字连接
  3. 发送PING命令
  4. 身份验证
  5. 从给主发送自己的端口信息
  6. 同步
  7. 命令传播

心跳检测

命令传播阶段,从服务器默认每秒一次频率,向主发送命令REPLCONF ACK

哨兵模式(Sentinel)

参考:
Redis 的 Sentinel 文档
Redis哨兵模式(sentinel)学习总结及部署记录
Redis Sentinel机制与用法

哨兵是redis高可用的解决方案:由一个或多个哨兵实例组成的哨兵系统可以监视任一多个主服务器,以及这些主服务器属下所有从服务器。
主要有三个作用:

  • 监控:通过心跳检查主/从服务器状态是否正常
  • 提醒:当某个服务器出现问题时,哨兵可通过api进行通知
  • 自动故障迁移:当一个主不可用时,进行一次自动故障迁移,将失效主的一个从变成新主,并让失效主成为新主的从
# 启动Sentinel
redis-sentinel /path/to/your/sentinel.conf
# or:
redis-server /path/to/your/sentinel.conf --sentinel
// sentinel.conf的配置demo
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallel-syncs resque 5

当一个snetinel初始化时,步骤如下:

  1. 初始化一个普通的redis服务器
  2. 将redis服务器使用的代码替换成sentinel专用代码(比如服务器默认端口是6379,而sentinel默认端口是26379)
  3. 初始化sentinel状态
  4. 根据给定的配置文件sentinel.conf,初始化sentinel的监视主服务器列表
  5. 创建连向主服务器的网络连接
// 一个sentinel实例结构
struct sentinelState {
    current_epoch uint64 // 当前纪元,用于实现故障转移

    // 保存了所有被这个sentinel监视的主服务器
    // 字典的key是服务器名,值是一个sentinelRedisInstance结构的指针
    masters map[string]*sentinelRedisInstance

    //。。。。
}

// 实例结构,代表一个被sentinel监视的redis服务器实例,该实例可以是主服务器、从服务器,或另一个sentinel
struct sentinelRedisInstance {
    // 标识,记录了实例的类型,以及当前状态
    flags int

    // 实例名字
    // 主服务器的名字由用户在配置文件中设置
    // 从服务器以及sentinel的名字由sentinel自动设置,格式为"ip:port"
    name string 

    // 配置纪元,用于实现故障转移
    config_epoch uint64

    // 实例地址
    addr *sentinelAddr

    // sentinel down-after-millisenconds 设定值
    // 实例无响应多少毫秒后会被判断为主观下限
    down_after_period int64 // 一个毫秒值

    // sentinel monitor     中quorum设定值
    // 判断这个实例为客观下线所需的支持投票数量
    quorum int

    // sentinel parallel-syncs  选项的值
    // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    parallel_syncs int

    // sentinel failover-timeout   选项值
    // 刷新故障迁移状态的最大时限
    failover_timeout int64 //一个毫秒值

    //。。。。
}

struct sentinelAddr {
    ip string
    port int
}

sentinel以10秒的频率向主/从服务器发送INFO命令,获取主/从服务器信息回复
sentinel以2秒的频率,通过redis发布订阅模式,sentinel与被监听服务器建立连接,获取所有被监听服务器信息,并更新自己的masters字典

主观下线

  • sentinel接受到服务器的无效回复,或超过down_after_period还没接收到回复

客观下线

  • 为确认主观下线服务器是真的下线,sentinel会向同样监视该服务器的其他sentinel询问,看他们是否也认为该服务器是下线状态,当sentinel从其他sentinel接收到足够数量的已下线票数后,sentinel就将该服务器判断为客观下线,并进行故障转移操作

选举领头sentinel

  • 当某主被认为客观下线,监视该主的各sentinel进行协商,选举一个领头,并由领头对该主执行故障转移
    大致选举规则:
  • 所有监视该主的且在线的sentinel都有资格参加
  • 一次选举后,所有sentinel的配置纪元进行自增,其实就是个计数器
  • 每个发现该主进入客观下线的sentinel都要求其他sentinel将自己设置为局部领头,一旦设置成局部领头,在该配置纪元中不能更改
  • 若有某sentinel被半数以上的sentinel设置成了局部领头,则该sentinel成为领头
  • 若超出给定时间未选出领头,则结束该纪元选举,一段时间后重新开始

故障转移

  1. 在已下线主属下从列表中,挑出一个从,并将该从转为主
  2. 让原主属下所有从改为复制新主
  3. 将原主设置为新主的从

选主步骤

  1. 删除从列表中处于下线/断线的服务器
  2. 删除从列表中最近5s内未回复sentinel的服务器
  3. 删除与已下线主服务器连接断开超过down_after_period * 10ms的从
  4. 根据从服务器的优先级,对剩下从列表进行排序,选出优先级最高的从
  5. 若多从具有相同最高优先级,则按照从的复制偏移量offset进行排序,选出offset最大的从
  6. 若多从具有相同的最高优先级和offset,则选出运行ID最小的

集群

深入学习Redis之Redis Cluster

cluster meet <ip> <port> // 指定节点握手

cluster addslots <slot> [slot ...] //将一个或多个槽指派给节点负责

计算key属于哪个槽 slot = CRC16(key) % 16383

重新分片

主从复制

故障检测
集群内节点间定期ping,过时认为节点故障

故障转移
从节点发现自己的主节点已下线,从节点对下线主节点进行故障转移:

  1. 下线主节点的所有从节点中选举出一个从节点N
  2. N执行slaveof no one命令,成为新的主节点
  3. N撤销所有对下线主节点的槽指派,并将这些槽指派给自己
  4. N向集群广播,告知其他节点自己成为新的主节点
  5. N开始接受和处理槽相关的命令请求,故障转移完成

发布和订阅

# 发布订阅常见命令
publish # 向频道中发布消息
subscribe # 订阅指定频道
unsubscribe # 退订指定频道
psubscribe # 订阅一个或多个模式,从而成为这些模式的订阅者
punsubscribe # 退订模式
  • 频道的订阅通过字典实现,字典key是频道名称,value是一个链表,链表里记录所有订阅该频道的client
  • 模式的订阅通过链表实现,链表节点结构包含pattern和client属性,pattern记录被订阅的模式

发布订阅的消息无法持久化,如果出现网络断开、服务器宕机等问题,消息就会丢弃

Stream

Redis Stream

  • 5.0版本新增特性,作用是MQ
  • stream提供了消息的持久化和主备复制功能,可以让任意客户端访问任意时刻的数据,并能记录每个客户端的访问位置,并保证消息不丢失
  • 每个stream有一个消息链表,将所有加入的消息都串起来,每个消息对应唯一ID

相关概念

  • Consumer Group :消费组,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer)。
  • last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
  • pending_ids :消费者(Consumer)的状态变量,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。

事务

// 事务相关命令

multi // 开启一个事务
exec // 事务提交
discard // 取消事务,放弃执行事务块内所有命令
watch // 乐观锁,在exec执行前,监视任意数量键,并在exec执行时,检查被监视的键是否至少有一个已经被修改过了,是的话服务器拒绝执行事务
unwatch // 取消watch对key的监视

事务实现

事务周期的三个阶段:

  1. 事务开始
    multi将客户端从非事务切到事务状态
  2. 命令入队
    每个client都有自己的事务状态
// 客户端结构体演示
struct redisClient {
    //....

    // 事务状态
    mstate *multiState
}

// 事务状态包含一个事务队列
struct multiState {
    // 事务队列,FIFO顺序
    commands *[]multiCmd
    count int
}

// 事务队列是一个multiCmd类型数组,每个multiCmd结构保存了已入对命令的相关信息
struct multiCmd {
    argv *obj // 参数
    argc int // 参数数量
    cmd *redisCommand // 命令指针
}
  1. 事务执行
    exec命令被服务器执行后,服务器会遍历客户端的multiState,并顺序执行,最后将执行命令结果全部返回给client

ACID性质

  • 原子性
    redis支持原子性,但是redis事务不支持回滚机制。事务队列中某个命令在执行期间出现错误,整个事务也会继续执行下去,直到事务队列中的所有命令都执行完毕
  • 一致性
    • 入队错误:命令不存在或格式不正确,事务会被拒绝
    • 执行错误:执行中错误的命令被识别且不会对数据库修改,因此不会对事务的一致性产生影响
    • 服务器停机:redis服务器在无持久化、RDB、AOF下,事务执行中发生停机都不会影响db一致性
  • 隔离性
    redis服务器是单线程方式执行事务,因此事务以串行方式执行,具有隔离型
  • 持久性
    依赖RDB或AOF持久化实现

排序

sort <key> //对列表键、集合键、有序集合键的值进行排序,默认升序排序

sort <key> alpha // alpha选项客队包含字符串值的键进行排序

sort <key> asc/desc //指定顺序

sort <key> limit <offset> <count> //返回一部分元素

慢日志

// -- 慢查询相关的服务器配置 -- 

slowlog-log-slower-then // 指定执行时间超过多少微秒的命令请求会被记录到日志上

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


// -- 查看服务器保存的慢查询日志命令 --
SLOWLOG GET

  • 慢查询日志保存在服务器状态的slowlog链表中,每个链表节点都包含一个slowlogEntry结构,slowlogEntry代表一条慢查询日志
  • 打印和删除慢查询日志通过遍历链表实现

监视器

通过执行MONITOR命令,客户端成为一个监视器,实时接收并打印服务器当前处理的命令的请求相关信息

你可能感兴趣的:(redis,redis,数据结构,数据库)