一、基础结构
- redis字符串:扩容,在1M内扩容都是加倍,超过就是1M,最大长度512M
- 整数的范围:signed long 的最大最小值,超过了这个值,Redis 会报错
- hash:渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash结构,查询时会同时查询两个 hash 结 构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到 新的 hash 结构中
- set 结构可以用来存储活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中奖两次
- zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按 关注时间进行排序
- 容器型数据结构,如果容器里元素没有了,那么立即删除元素,释放内存
二、架构
- 哨兵模式
- 启动哨兵:Sentinel端口:26379,返回主从redis地址,监控主从节点是否正常,客观,主观下线节点 辅助
- codis:redis集群中间件
- 中间代理:将key做hash运算,划分1024个槽位
- 缺点:不支持事务,rename无法正确操作
- 集群模式Cluster
- 相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负 责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相互连接组成一个对 等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息
- Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分的更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,它不需要另外的分布式 存储来存储节点槽位信息。 当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要 查找某个 key 时,可以直接定位到目标节点
三、使用场景
- 记录帖子的点赞数、评论数和点击数 (hash)
- 记录用户的帖子ID 列表 (排序),便于快速显示用户的帖子列表 (zset)
- 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)
- 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)
- 缓存近期热帖内容 (帖子内容空间占用较大),减少数据库压力 (hash)
- 记录帖子的相关文章 ID,根据内容推荐相关帖子(list)
- 如果帖子ID 是整数自增的,可以使用 Redis 来分配帖子ID(计数器)
- 收藏集和帖子之间的关系 (zset)
- 记录热榜帖子ID 列表,总热榜和分类热榜 (zset)
- 缓存用户行为历史,进行恶意行为过滤 (zset,hash)
四、应用
- 分布式锁:set lock:codehole true nx px 5000(存在业务时间超过锁的时间,redis宕机风险)
- 延时队列:使用list数据结构做异步消息队列使用,使用rpush/lpush 操作入队列,使用lpop 和 rpop 来出队列,(可以考虑使用阻塞读:blpop/brpop)(无法保证ack,队列消息被读出之后,业务异常了,消息就丢失)
- 位图:setbit、getbit、bitcount:减少存储空间(实际也是字符串,使用位做1,0存储,会自动扩容)(比如:记录用户365天签到情况)。bitfield:一次操作多个位
- HyperLogLog:不精准的去重统计(网页uv)指令:pfadd,pfcount,pfmerge。会占据12k的存储空间
- Bloom Filter:不精准的contains判断,插件式安装,指令:bf.add 添加元素, bf.exists 查询元素是否存在
- redis-cell:令牌桶限流,指令:cl.throttle laoqian:reply 15 30 60 1,(key,容量,令牌个数,时间,默认参数1)
- 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
- scan:scan 0 match key99* count 1000 (cursor 整数值(第一次0),key 的正则模式,遍历的 limit hint(不是返回数量))
五、原理
- 线程 IO 模型:Redis 是个单线程程序,使用多路复用NIO
- 通信协议:RESP 是 Redis 序列化协议的简写(冗余字符串)。它是一种直观的文本协议,优势在于实现异常简单,解析性能 极好
- 持久化:第一种是快照,第二种是 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日志
- 管道:客服端实现,把指令合并,先写入缓冲区,在一起发送到服务端,在读取返回的结果
- 事务:分别是 multi/exec/discard。multi 指示事务的开始,exec 指示 事务的执行,discard 指示事务的丢弃。
- 5.1. Redis 提供了这种 watch 的机制,它就是一种乐观锁
- 5.2. 如果是由于语法错误或者error错误,那么整体会回滚(失效); 如果是由于参数类型错误,语法没有错误,那么整体中的部分会有效,其他会失效。(无法保证原子性)
- pubsub:发布订阅(无法持久化,独立项目:Disque)
- 小对象压缩:会动态变化(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
- 主从复制(CAP):网络分区发生时,一致性和可用性两难全
- 8.1. 增量同步:主节点记录指令到内存buffer(环形数组),如果满了,就头部覆盖前面的内容,如果网络不好,会出现指令丢失
- 8.2. 快照同步:非常耗资源,主库进行bgsave保存快照到磁盘进行同步从节点,从节点进行全量加载,同步过程,主节点buffer还会记录,如果太小,导致增量无法完成,还会触发快照同步,死循环(配置合适的buffer大小)
- 8.3. 新增从节点到集群,先进行一次快照同步,完成后再进行增量同步
- 8.4. 无盘复制:主节点一遍遍历生成快照,一边将内容发送到从节点,从节点存储到磁盘文件中,再进行一次性加载
- 8.5. wait指令:wait 从库数量 等待时间ms (wait 1 0),会等待N个从库同步完成
六、拓展
- stream:redis5.0新增的消息队列(仿造kafka),弥补了pub/sub不能持久化消息的缺陷,不同于kafka,不能分区
- info指令:获取redis所有信息,可以按块获取(info memory)
- 分布式锁:在主从集群,加锁之后出现宕机切换,会出现重复加锁,在集群模式下,redlock算法,对一半以上实例进行加锁
- 过期策略:redis对key的过期检查和删除,维护了一个key的过期时间字典
- 4.1. 定时扫描策略:每秒10次进行过期扫描,从字典随机20个key,判断是否过期,超过1/4,继续重复步骤,不过有时间上限:25ms
- 4.2. 惰性策略:客服端访问key的时候,会进行过期时间判断
- 4.3. 从库过期策略:不会自动删除,需要从库删除的时候,del指令同步过去才会删除
- 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个跟池里面的融合淘汰
- 懒惰删除:不止有一个主线程,还有几个异步线程专门处理耗时的操作,包括删除,同步,LRU淘汰,过期key等
- 比如:4.0版本: 异步删除:unlink(旧版:del) 、flushall async
- redis安全 指令改名:rename-command keys abckeysabc、禁用:rename-command flushall ""
- SSL代理:(本身不支持)官方推荐:spiped
七、源码
- 字符串内部结构
- 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
- 字典内部结构
- 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,其它的特性和字典一模一样
- 压缩列表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 位
}
- 快速列表(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 压缩存储
...
}
- 跳跃列表(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; // 跨度
}
- 紧凑列表(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,逆向遍历,可以直接识别到长度,就定位到了元素位置。
- 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 字段
}
总结:
- hash,set:存储根据数据量和大小判断使用ziplist还是dict结构
- set:数据量都是整数同时个数少的时候用intset
- zset:同时需要使用skiplist存储score
- list:使用quicklist存储
来源:oschina
链接:https://my.oschina.net/u/1261308/blog/3142716