Redis深度历险读书笔记

筅森魡賤 提交于 2020-11-22 15:01:24

一、基础结构

  1. redis字符串:扩容,在1M内扩容都是加倍,超过就是1M,最大长度512M
  2. 整数的范围:signed long 的最大最小值,超过了这个值,Redis 会报错
  3. hash:渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash结构,查询时会同时查询两个 hash 结 构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到 新的 hash 结构中
  4. set 结构可以用来存储活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中奖两次
  5. zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按 关注时间进行排序
  6. 容器型数据结构,如果容器里元素没有了,那么立即删除元素,释放内存

二、架构

  1. 哨兵模式
  • 启动哨兵:Sentinel端口:26379,返回主从redis地址,监控主从节点是否正常,客观,主观下线节点 辅助
  1. codis:redis集群中间件
  • 中间代理:将key做hash运算,划分1024个槽位
  • 缺点:不支持事务,rename无法正确操作

image

  1. 集群模式Cluster
  • 相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负 责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对 等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息
  • Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分的更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,它不需要另外的分布式 存储来存储节点槽位信息。 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要 查找某个 key 时,可以直接定位到目标节点

三、使用场景

  1. 记录帖子的点赞数、评论数和点击数 (hash)
  2. 记录用户的帖子ID 列表 (排序),便于快速显示用户的帖子列表 (zset)
  3. 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)
  4. 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)
  5. 缓存近期热帖内容 (帖子内容空间占用较大),减少数据库压力 (hash)
  6. 记录帖子的相关文章 ID,根据内容推荐相关帖子(list)
  7. 如果帖子ID 是整数自增的,可以使用 Redis 来分配帖子ID(计数器)
  8. 收藏集和帖子之间的关系 (zset)
  9. 记录热榜帖子ID 列表,总热榜和分类热榜 (zset)
  10. 缓存用户行为历史,进行恶意行为过滤 (zset,hash)

四、应用

  1. 分布式锁:set lock:codehole true nx px 5000(存在业务时间超过锁的时间,redis宕机风险)
  2. 延时队列:使用list数据结构做异步消息队列使用,使用rpush/lpush 操作入队列,使用lpop 和 rpop 来出队列,(可以考虑使用阻塞读:blpop/brpop)(无法保证ack,队列消息被读出之后,业务异常了,消息就丢失)
  3. 位图:setbit、getbit、bitcount:减少存储空间(实际也是字符串,使用位做1,0存储,会自动扩容)(比如:记录用户365天签到情况)。bitfield:一次操作多个位
  4. HyperLogLog:不精准的去重统计(网页uv)指令:pfadd,pfcount,pfmerge。会占据12k的存储空间
  5. Bloom Filter:不精准的contains判断,插件式安装,指令:bf.add 添加元素, bf.exists 查询元素是否存在
  6. redis-cell:令牌桶限流,指令:cl.throttle laoqian:reply 15 30 60 1,(key,容量,令牌个数,时间,默认参数1)
  7. GeoHash:地理位置, 指令:
添加一个坐标:geoadd company 116.48105 39.996794 juejin
计算2个元素的距离:geodist company juejin ireader km
获取元素的经纬度:geopos company juejin
获取元素的hahs值:geohash company ireader
获取范围内附近的其他元素升序(会包括本身):georadiusbymember company ireader 20 km count 3 asc
根据坐标值来查询附近的元素:georadius company 116.514202 39.905409 20 km withdist count 3 asc
  1. scan:scan 0 match key99* count 1000 (cursor 整数值(第一次0),key 的正则模式,遍历的 limit hint(不是返回数量))

五、原理

  1. 线程 IO 模型:Redis 是个单线程程序,使用多路复用NIO
  2. 通信协议:RESP 是 Redis 序列化协议的简写(冗余字符串)。它是一种直观的文本协议,优势在于实现异常简单,解析性能 极好
  3. 持久化:第一种是快照,第二种是 AOF日志
  • 3.1. Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化。Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求
  • 3.2. AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。AOF重写:其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。fsync:当程序对 AOF日志文件进行写操作时,实际上是将内容写到了 内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。可以设置每隔1S左右执行fsync
  • 3.3. Redis 4.0 混合持久化,先从快照恢复数据,在执行增量的aof日志
  1. 管道:客服端实现,把指令合并,先写入缓冲区,在一起发送到服务端,在读取返回的结果
  2. 事务:分别是 multi/exec/discard。multi 指示事务的开始,exec 指示 事务的执行,discard 指示事务的丢弃。
  • 5.1. Redis 提供了这种 watch 的机制,它就是一种乐观锁
  • 5.2. 如果是由于语法错误或者error错误,那么整体会回滚(失效); 如果是由于参数类型错误,语法没有错误,那么整体中的部分会有效,其他会失效。(无法保证原子性)
  1. pubsub:发布订阅(无法持久化,独立项目:Disque)
  2. 小对象压缩:会动态变化(object encoding key)
  • 7.1. ziplist:紧凑的字节数组结构,当list或者hash的个数小于512并且任意元素长度小于64,
  • 7.2. intset:紧凑的整数数组结构,当zset个数128长度小于64,set个数小于512
  • 7.3. 内存分配算法:使用第三方库:jemalloc(facebook)库(默认),tcmalloc(google)。查看info memory
  1. 主从复制(CAP):网络分区发生时,一致性和可用性两难全
  • 8.1. 增量同步:主节点记录指令到内存buffer(环形数组),如果满了,就头部覆盖前面的内容,如果网络不好,会出现指令丢失
  • 8.2. 快照同步:非常耗资源,主库进行bgsave保存快照到磁盘进行同步从节点,从节点进行全量加载,同步过程,主节点buffer还会记录,如果太小,导致增量无法完成,还会触发快照同步,死循环(配置合适的buffer大小)
  • 8.3. 新增从节点到集群,先进行一次快照同步,完成后再进行增量同步
  • 8.4. 无盘复制:主节点一遍遍历生成快照,一边将内容发送到从节点,从节点存储到磁盘文件中,再进行一次性加载
  • 8.5. wait指令:wait 从库数量 等待时间ms (wait 1 0),会等待N个从库同步完成

六、拓展

  1. stream:redis5.0新增的消息队列(仿造kafka),弥补了pub/sub不能持久化消息的缺陷,不同于kafka,不能分区
  2. info指令:获取redis所有信息,可以按块获取(info memory)
  3. 分布式锁:在主从集群,加锁之后出现宕机切换,会出现重复加锁,在集群模式下,redlock算法,对一半以上实例进行加锁
  4. 过期策略:redis对key的过期检查和删除,维护了一个key的过期时间字典
  • 4.1. 定时扫描策略:每秒10次进行过期扫描,从字典随机20个key,判断是否过期,超过1/4,继续重复步骤,不过有时间上限:25ms
  • 4.2. 惰性策略:客服端访问key的时候,会进行过期时间判断
  • 4.3. 从库过期策略:不会自动删除,需要从库删除的时候,del指令同步过去才会删除
  1. LRU:内存超过物理内存限制时,会和磁盘产生交换,性能急剧下滑,可以设置maxmemory,超过的时候根据策略来腾出空间
  • noeviction(默认):不做写请求,直接报错
  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl: 从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据 淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰 LRU实现:每个key增加一个小字段保存时间戳,随机获取5(maxmemory_samples)个key,淘汰最旧的,如果内存还是不够,再来一次。3.0版本加入了淘汰池,淘汰结束的时候,会保留最旧的5个放到池里面,下次再随机5个跟池里面的融合淘汰
  1. 懒惰删除:不止有一个主线程,还有几个异步线程专门处理耗时的操作,包括删除,同步,LRU淘汰,过期key等
  • 比如:4.0版本: 异步删除:unlink(旧版:del) 、flushall async
  1. redis安全 指令改名:rename-command keys abckeysabc、禁用:rename-command flushall ""
  • SSL代理:(本身不支持)官方推荐:spiped

七、源码

  1. 字符串内部结构
  • 1.1. SDS,带长度信息的字节数组,类似java的ArrayList
struct SDS<T>{
 T capacity://数组容量
 T len;//数组长度
 byte flags;//特殊标示位
 byte[] content;//数组内容
}
  • 1.2. 在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储
  • 1.3. 扩容策略:长度小于1M之前,扩容采用加倍,超过之后,每次增加1M
  1. 字典内部结构
  • 2.1. dict 是 Redis 服务器中出现最为频繁的复合型数据结构,除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key 和 value 也组成了一个全局字典,还有带过期时间的 key 集合也是一个字典。 zset 集合中存储 value 和 score 值的映射关系也是通过 dict 结构实现
struct zset {
    dict *dict; // all values value=>score
    zskiplist *zsl;
}
  • 2.2. dict 结构内部包含两个hashtable,日常使用其中一个,宁外一个使用在扩容的时候
struct dict {
    ......
    dictht ht[2];
}
  • 2.3. hashtable结构等同于java的hashmap,也是用链地址解决hash冲突
struct dictEntry {
  void* key;
  void* val;
  dictEntry* next; // 链接下一个 entry
}

struct dictht {
  dictEntry** table; // 二维
  long size; // 第一维数组的长度
  long used; // hash 表中的元素个数
  ......
}
  • 2.4. 渐进式rehash:在扩容的时候,不是一次性把全部的键值都rehash到新的hashtable,将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量,同时会启动定时器做rehash 在字典中维持一个索引计数器变量 rehashidx,来记录rehash到第几个索引,全部结束-1 在rehash期间,字典的删除查找更新都会在2个hashtable进行,新增会直接更新到新的hashtable
  • 2.5. hash函数:siphash
  • 2.6. hash攻击:利用hash函数的偏向性,对服务器攻击,导致所有元素都到hash冲突的链表中,导致查询速度变成o(n)
  • 2.7. 扩容条件:hash表中元素等于数组长度时扩容,如果redis在做bgsave,会不去扩容,如果元素个数达到5倍,会强制扩容
  • 2.8. 缩容条件:当元素个数低于数组长度的10%,会进行缩容
  • 2.9. set 的结构:底层实现也是字典,只不过所有的 value 都是 NULL,其它的特性和字典一模一样
  1. 压缩列表ziplist:
  • 3.1. zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储
struct ziplist<T> {
  int32 zlbytes; // 整个压缩列表占用字节数
  int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
  int16 zllength; // 元素个数
  T[] entries; // 元素内容列表,挨个挨个紧凑存储
  int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
  • 3.2. prevlen 字段表示前一个 entry 的字节长度。当字符串长度小于254(0xFE) 时,使用一个字节表示,如果达到或超出 254(0xFE) 那就使用 5 个字节来表示
struct entry {
  int<var> prevlen; // 前一个 entry 的字节长度
  int<var> encoding; // 元素类型编码
  optional byte[] content; // 元素内容
}
  • 3.3. ziplist紧凑存储,没有冗余空间,插入新元素,就调用realloc 扩展内存,需要进行内容拷贝(看内存分配)。如果ziplist占据内存太大,重新分配和拷贝内容都很大消耗。所以不适合大型数据
  • 3.4. 级联更新:如果某个元素字节从小于254更新到大于,下一个元素的prevlen,从1字节扩大到5字节,如果这个元素本身也正好是253字节,就会导致它的下一个元素也更新
  • 3.5. intset:当 set 集合容纳的元素都是整数并且元素个数较小时, Redis 会使用 intset 来存储结合元素。 intset 是紧凑的数组结构,同时支持 16 位、 32 位和 64 位整数
struct intset<T> {
    int32 encoding; // 决定整数位宽是 16 位、 32 位还是 64 位
    int32 length; // 元素个数
    int<T> contents; // 整数数组,可以是 16 位、 32 位和 64 位
}
  1. 快速列表(quicklist) quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来,为了进一步节约空间, 会对ziplist 进行压缩存储,使用 LZF 算法压缩,可以选择压缩深度。 比如存储12个元素,ziplist存储3个,就会存在4个节点,每个节点的ziplist3个元素
list-max-ziplist-size:ziplist长度,默认8字节
listcompress-depth:压缩深度,首尾2个元素不压缩,深度1,以此类推。默认0
struct quicklist {
  quicklistNode* head;
  quicklistNode* tail;
  long count; // 元素总数
  int nodes; // ziplist 节点的个数
  int compressDepth; // LZF 算法压缩深度
  ...
}

struct quicklistNode {
  quicklistNode* prev;
  quicklistNode* next;
  ziplist* zl; // 指向压缩列表
  int32 size; // ziplist 的字节总数
  int16 count; // ziplist 中的元素数量
  int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
  ...
}

  1. 跳跃列表(skiplist)
  • 5.1. zset存储score做有序查找,正常按链表存储score,查找的复杂度就是o(n),如果把某些节点变成多层,查找就变成顶层遍历,找到大于元素的节点,退回上节点,在下一层继续查找,复杂度O(lg(n))
struct zsl {
    zslnode* header; // 跳跃列表头指针
    int maxLevel; // 跳跃列表当前的最高层
    map<string, zslnode*> ht; // hash 结构的所有键值对
}

struct zslnode {
    string value;
    double score;
    zslnode*[] forwards; // 多层连接指针(zslforward*[] forwards; )
    zslnode* backward; // 回溯指针
}
  • 5.2. 每新增节点都要随机计算层数,期望是50%,在代码中晋升概率只有 25%,因为层数一般不高,在顶层往下遍历会浪费,记录maxLevel,遍历时直接从maxLevel开始,提高很多性能
  • 5.3. score修改,直接删除再插入
  • 5.4. score值一样:会比较value值(字符串比较)
  • 5.5. 元素排名:在skiplist的forward指针上增加了span跨度属性,前一个节点到当前节点会跳过多少个节点,rank搜索路径的span累加
struct zslforward {
    zslnode* item;
    long span; // 跨度
}
  1. 紧凑列表(listpack)redis5.0引入
  • 6.1. 对ziplist结构的改进,在存储空间上会更节省,也更精简,删掉了zltail_offset字段,可以通过total_bytes 字段和最后一个元素的长度字段计算
  • 6.2. 长度字段使用 varint 进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节, listpack 元素长度的编码可以是 1、 2、 3、 4、 5 个字节。同 UTF8 编码一样,它通过字节的最高为是否为 1 来决定编码的长度
  • 6.3. 结构
struct listpack<T> {
    int32 total_bytes; // 占用的总字节数
    int16 size; // 元素个数
    T[] entries; // 紧凑排列的元素列表
    int8 end; // 同 zlend 一样,恒为 0xFF
}

struct lpentry {
    int<var> encoding;
    optional byte[] content;
    int<var> length;
}
  • 6.4. 级联更新就不存在,元素之间的独立的,目前只有stream数据结构使用了listpack
  • 6.5. 比对总结:ziplist设计问题,正向遍历可以,逆向遍历,无法直接定位到元素的开始,所以多了total_bytes,prevlen字段,listpack,逆向遍历,可以直接识别到长度,就定位到了元素位置。
  1. Rax(基数树 Radix Tree)
  • 7.1. 按照 key 的字典序排列,支持快速地定位、插入和删除操作
  • 7.2. 用在 Redis Stream 结构里面用于存储消息队列,在 Stream 里面消息 ID 的前缀是时间戳 + 序号,这样的消息可以理解为时间序列消息
  • 7.3. 结构:rax 中有非常多的节点,根节点、叶节点和中间节点,有些中间节点带有 value,有些中间节点纯粹是结构性需要没有对应的 value
struct raxNode {
    int<1> isKey; // 是否没有 key,没有 key 的是根节点
    int<1> isNull; // 是否没有对应的 value,无意义的中间节点
    int<1> isCompressed; // 是否压缩存储,这个压缩的概念比较特别
    int<29> size; // 子节点的数量或者是压缩字符串的长度 (isCompressed)
    byte[] data; // 路由键、子节点指针、 value 都在这里
}
压缩结构
struct data {
  optional struct { // 取决于 header 的 size 字段是否为零
   byte[] childKey; // 路由键
   raxNode* childNode; // 子节点指针
  } child;
  optional string value; // 取决于 header 的 isNull 字段
}

非压缩节点
struct data {
    byte[] childKeys; // 路由键字符列表
    raxNode*[] childNodes; // 多个子节点指针
    optional string value; // 取决于 header 的 isNull 字段
}

image

总结:

  • hash,set:存储根据数据量和大小判断使用ziplist还是dict结构
  • set:数据量都是整数同时个数少的时候用intset
  • zset:同时需要使用skiplist存储score
  • list:使用quicklist存储
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!