redis

redis对象头
struct RedisObject {
int4 type; // 4bits
int4 encoding; // 4bits
int24 lru; // 24bits; 结合过期策略。lru的缺陷看
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
}- 不同的对象具有不同的类型 type(4bit),
- 同一个类型的 type 会有不同的存储形式encoding(4bit)
- lru属性:缓存淘汰策略;
- Redis 通过维护一个全局的 LRU 时钟来跟踪对象的最近访问时间。当一个对象被访问时,Redis 会更新该对象的 lru 属性,将其设置为当前的 LRU 时钟值
- 当 Redis 需要回收内存或进行淘汰时,它会选择 lru 属性值最小(最近最少使用)的对象作为淘汰的候选对象
- lru 属性只是 Redis 内部使用的属性,并不直接暴露给 Redis 用户
- refcount : 记录对象的引用计数(jvm引用计数法?)
- *ptr:用于指向实际数据的指针
数据结构
- 数据结构
- String(微博中的粉丝数)
- redis的字符串是以字节数组形式存在的,是可被修改的字符串;redis对字符串的存储有单独的数据结构
- 数据结构叫:SDS:"Simple Dynamic String" 的中文翻译是"简单动态字符串" 于存储和处理可变长度的字符串数据
- ;
javastruct SDS<T> { T capacity; // 数组容量 T len; // 数组长度 byte flags; // 特殊标识位,不理睬它 byte[] content; // 数组内容 } 
- 可修改的字符串所以支持append操作
- 扩容方式先拓展,再复制,再append
- 字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间
- 当超过1m以后为了节省空间,每次增加空间1m
- sds 使用范性支持short或是byte类型空间极致压缩
- Redis 规定字符串的长度不得超过 512M 字节
- 字符串的 存储方式 两种emb / raw
- 如果字符串长度超过44个字符就用raw否则emb
- 因为SDS存储头得3个字节;redisObject得16个字节;因为内存对象统一分配2,4,8,16 .. 64,等 44是64 - 19 -1
- Redis中底层保证字符串的存储必然以0结尾,虽然是44个字符但是obj显示确是45
- 如果字符串长度超过44个字符就用raw否则emb
- 操作:
- java
> set name codehole OK > get name "codehole" > exists name (integer) 1 > del name (integer) 1 > get name (nil); - mget 合并返回
- setex name 5 zhangfan :5s 过期删除key为name ,value为zhangfan的值
- setnx name zhangfan : name不存在就创建name为zhangfan的值
- list(微博的粉丝列表)
- 异步队列实现
- rpush books python java golang 从右边放入
- rpop / lpop 可以从左右提取
- 消息对列实现,可以用blpop,阻塞的方式拉数据
- hash(微博中的商品信息)
- hset books java "think in java"
- hgetall books # entries(),key 和 value 间隔出现
- hget books java -> "think in java"
- set(微博中的共同喜好和好友)
- sadd books python
- spop books # 弹出一个 -- > python
- zset(打赏排行榜)
- zadd books 9.0 "think in java"
- zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围
- zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获取 value 列表的功能
- String(微博中的粉丝数)
压缩列表
概念:压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙 zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储 
数据结构
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个
节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
entry 块随着容纳的元素类型不同,也会有不一样的结构。
struct entry {
int<var> prevlen; // 前一个 entry 的字节长度;可以倒着遍历找到前一个元素
int<var> encoding; // 元素类型编码,用于决定content中的编码格式
optional byte[] content; // 元素内容
}理解:他是一块连续的内存空间频繁的扩容修改不太适合,涉及到扩容及复制
快速列表
早期版本存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表linkedlist,也就是元素少时用 ziplist,元素多时用 linkedlist
概念:quicklist 是 压缩列表 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来 
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
int2 encoding; // 存储形式 2bit,原生字节数组还是 LZF 压缩存储
}struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 算法压缩深度
...
}快速列表中的ziplist 默认单个长度为8k,超过8k会将ziplist压缩为 ziplist_compressed ziplist_compressed :
- 是一种压缩编码格式用于在有效区间内高效存储zset和hash数据
- ziplist用于存储紧凑连续存储数据结构,用于存储较小的zset和hash,存储量数据变大就会变成compress,在快表中默认为8k
跳表
首先理解Zset的实现:内部实现是一个 hash 字典加一个跳跃列表 (skiplist) 
基本结构
Redis 的跳跃表共有 32 层 如图只有上下四层,可以存储 2^64 次方个元素
kv由zslnode构成,kv之间通过双向链表链接,同一层的kv通过指针串联
struct zslnode {
string value;
double score;
zslnode*[] forwards; // 多层连接指针
zslnode* backward; // 回溯指针
}查找过程
- 如果只有一层,那么寻找元素,时间将是o(n);跳表与二分查找类似,可以一层层向下递归,查找时间复杂度为o(logn)
- 搜索路径:从最顶层到最底层找到期望节点的路径为搜索路径
- 节点高度:前面说过跳表最高为64层,那如果要插入新的元素,该元素的高度怎么判断
- 随机分配机制:50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层 2^-63,因为这里每一层的晋升概率是 50%(层数是32层,最多存储节点是2的63次方个)
- 插入:通过搜索路径找到节点在最底层的位置,通过随机分配测策略是否上升到上层节点,这样新节点就可以参与到更高级的搜索节点中
- 删除:通过搜索路径找到节点,若要删除,需要删除该节点在所有层级中的节点,
- 更新:先删除再插入
总结:
- 对于内存的占用:跳跃列表的每个节点都需要额外的指针来构建层级结构,因此占用的内存空间相对较大。对内存比较敏感
- 对跳表节点的更新麻烦:
- 按照搜索路径找到节点
- 更新节点的值
- 更新上层链表的指针:遍历该节点的上层节点,修改上层链表的前后指针
- 如果涉及到层级的变更,那么需要相应修改该层级上的相关节点的前后指针
- 但是因为读写操作远大于写操作,所以依然抗用
- 上面每一层都是下面一层节点数的(1/p),redis定义的P是0.25,多层次的连表结构非常类似于二分查找;使得时间负责度降低胃0logn

基数树 rax
Redis 五大基础数据结构里面,能作为字典使用的有 hash 和 zset。
- hash 不具备排序功能,
- zset 则是按照 score 进行排序的。
- rax 跟 zset 的不同在于它是按照 key 进行排序的。

struct raxNode {
int<1> isKey; // 是否没有 key,没有 key 的是根节点
int<1> isNull; // 是否没有对应的 value,无意义的中间节点
int<1> isCompressed; // 是否压缩存储,这个压缩的概念比较特别
int<29> size; // 子节点的数量或者是压缩字符串的长度 (isCompressed)
byte[] data; // ***路由键***、子节点指针、value 都在这里
}这个树:如果一个中间节点有多个子节点,那么路由键就只是一个字符。如果只有一个子节点,那么路由键就是一个字符串。后者就是所谓的「压缩」形式 
- 非压缩节点 如果子节点有多个,那就不是压缩结构,存在多个路由键,一个键是一个字符。
- 像es有两个儿子
- 而aster只有一个儿子
- 如果是叶子节点,child 字段就不存在

redis的线程io模型
前提
- Redis 是个单线程程序
- 所有的运算都是内存级别的运算
- Redis 单线程如何处理那么多的并发客户端连接?
- reactor线程模型 selector,理解reactor
管道
(Pipeline) 本身并不是 Redis 服务器直接提供的技术,这个技术本质上是由客户端提供的 实际上理解就是合并操作
- 读写读写 两次网络传输
- 读读写写 -> 一次网络传输
数据持久化
- RDB :redis databse
- 简单明了快照方式存储,存储的磁盘文件是2进制文件
- 有save和bgsave两种方式,sava直接停止进行数据快照存储,bgsave通过fork子进程进行存储
- fork是调用c库glibc的函数创建一个子进程进行数据存储处理
- 创建时子进程与父进程内存数据保持一致
- 当有客户端提交数据时候,父进程会采用copy on right的方式进行数据读取,将数据复制一份进行修改,修改后的数据存储在父进程中当存储完成最后进行修改源文件
- 子进程的数据在诞生那一刻就不会进行修改
- RDB的特点
- 包含着数据库完整的状态,可在数据库重启时候进行恢复
- 紧凑和高效的:rdb是二进制文件,有紧凑的格式和高效的存储性能
- 可手动触发,定时触发以及写入触发
- AOF: Append-Only File
- AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。
- 假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
- aof是将命令添加到aof文件后
- 但是当更改命令很多的时候,会造成aof文件特别的大,这个时候需要对aof文件进行缩水,减少占用空间
- 使用命令bgrewriteaof,同样开辟一个子进程进行修改
- 遍历当前数据库的kv对,重写aof文件,保存最少的更新修改命令
- 重写aof后将,这期间产生的命令添加到aof文件后面
- aof是将指令先写到内存文件中去,然后通过操作系统的定时刷新到磁盘上
- aof写到内存但是没刷到磁盘,数据库重启数据丢失
- 利用操作系统的fsync 命令强制将文件数据刷到磁盘上
- 可以指定周期1s一次/
- 不执行(利用操作系统自动执行)/
- 来一次修改命令就执行一次
- 关于aof和RDB文件是不是都是二进制文件
- 是的
- aof文件主要是修改redis库的命令语句,但是包含redis的数据结构和编码信息所以用二进制进行存储
- rdb是纯粹的二进制文件,只存储键值对,更加的紧凑
- 两者区别联系
- rdb文件通常比aof小,一是只存kv一是紧凑二进制
- rdb恢复较快,aof要逐条遍历命令恢复慢
- aof恢复完整,rdb因为是快照所以恢复可能漏
- 现在一般都是rdb然后追加aof的形式。(混合持久化)

- 这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小
数据同步
- redis 符合最终一致不符合强一致性;异步同步数据
- 但是redis3.0支持write命令,将异步改为同步
- set zhang fan
- write 1 0 (1:一个slave. 0 ms等待时间,若为0则无限等待)
- 但是redis3.0支持write命令,将异步改为同步
- 主备节点同步方式
- 增量同步
- 就是一个环形的buffer,主节点将新增命令写到环形buffer中,异步将buffer中的数据发给slave
- 问题如果满了,buffer会覆盖掉之前的数据,咋办:有了全量快照同步
- 快照同步(全量)
- slave更不上master了,master将自己的数据进行快照备份,将快照写到buffer。
- slave首先同步rdb文件,然后同步快照
- 还是会出现,slave还没同步完rdb结果master的buffer有被重复覆盖了,反复反复
- ---- > 合理设计buffer数组

- 增量同步
多节点redis
sentinel 哨兵
- 一个高可用集群来监控主节点是否异常,并在异常的时候切换主备节点

- redis采用的是主从同步的方式可能会数据丢失,sentinel模式下对此采用的方案是提供两个参数
- min-slaves-to-write 1 : 至少有一个节点在进行正常的复制,否则不对外提供写相应,redis失去可用性
- min-slaves-max-lag 10 : 在10s内slave有与哨兵进行节点反馈
cluster 集群

- 去中心化的三个节点,分别存储不同的数据
- Cluster 将所有数据划分为 16384 的 slots;每个节点负责其中一部分槽位
- 槽位定位算法
- Cluster 默认会对 key 值使用 crc32 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
- 16384 个槽点是为了实现数据均匀分布、方便节点的增加和移除、支持数据的并行处理和故障转移等目标而选择的
- redis的键值对是如何部署到各个槽中的
- 将键进行CRC16hash算法计算成一个hash值,然后对该hash惊醒16384的取余,该键就找到了对应的slot
- 通常情况下,集群上的各个redis节点也具备主备节点
- 主备节点切换是通过gossip 病毒传播协议类似~~ 超过半数节点任务你下线了你就是下线了,cluster进行主备切换
- 当有新的redis加入cluster或者删除了某个cluster
- 会从已有的redis节点中均匀分出部分slot给该节点
- 然后进行数据迁移
- 在迁移过程中此时由client进行访问:
- 老节点给你一个moved,让你重新访问,会告诉你去访问那个节点
- client带着asking指令重新访问数据迁移到的节点

- 面试失败没答上 5. 重点
- 初始新增节点不包含任何slot,
- 使用 MIGRATE 命令逐键迁,

- Cluster 默认会对 key 值使用 crc32 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。
codis 中国人研发集群
过期策略
对过期key的处理
- 定时删除
- 将设置了expire的key进行单独hash表存储
- 定时扫描策略
- 从字典中找到20个key
- 删除这20个key中已经超时的key
- 如果删除比例超过1/4则继续筛选
- 由此可以看出不要把过期时间设置在一块
- 从库没有过期策略,主库在key过期回新增del语句在aof文件上,从库跟着删除,会出现数据不一致场景
- LRU机制
- 实际内存超出最大内存、
- (maxmemory-policy) 最大内存淘汰机制(key的清楚策略)
- noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行
- volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰
- volatile-ttl 剩余寿命最小的(剩余时间最小的)
- volatile-random 不过淘汰的 key 是过期 key 集合中随机的 key(设置随机时间的随机淘汰)
- allkeys-lru 最近最少使用的key,包含没有设置过期时间
- allkeys-random 随机淘汰所有的
- 设置了lfu:最不频繁使用的key删除
- 设置了lfu并且设置了expire的最不频繁使用的key值进行删除
- 惰性删除
- 存在的必要:当del删除的节点过大的时候,会导致主线程阻塞,为了解决卡顿,redis4.0 提供unlink,开启一个异步线程后台进行数据删除
- 机制:azyfree的原理不难想象,就是在删除对象时只是进行逻辑删除,然后把对象丢给后台,让后台线程去执行真正的destruct,避免由于对象体积过大而造成阻塞
- unlink
- FLUSHALL、FLUSHDB命令 --- 清空数据库 后面加async
- lazyfree线程
- 虽然redis把处理网络收发和执行命令这些操作都放在了主工作线程,但是除此之外还有许多bio后台线程也在兢兢业业的工作着,比如用来处理关闭文件和刷盘这些比较重的IO操作,这次bio家族又加入了新的小伙伴——lazyfree线程。
LRU的缺陷:
- 有一种场景就是一个key偶然的被访问一次,这样这个key就不会删除
- 理解文章开头有一个redisObject头中有一个lru字段
- LRU 和 LFU 都是根据lru字段进行比较
- LRU模式下,该字段存储的是reids的server.lruclock,每次key被访问,都会触发该值的更改,根据该值来比对多久时间没访问
- LFU模式下,lru该字段的24bit是用于存储两个值,分别是ldt 和 logc
- logc(count)用于存储范围频次(计数器),如果值过小会被回收掉,所以新建的key值会设置成一个默认的值 = 5(避免刚被创建就被回收)
- ldt:用于存储上一个logc的更新时间
- 区别:
- lru : 访问的时候更新lru字段
- lfu :删除数据的时候进行 count值的递减,根据衰减系数进行设置;衰减成0则删除
如何保证缓存和持久化数据(DB)的数据一致性
- 若发生数据更新的时候如何处理
- 延迟双删除
- 先删除缓存,再删除数据库。再删除缓存
- 如果不第二次删除缓存会导致,并发状态下,其他的线程在第一个线程删除缓存数据的时候去读数据库的旧值,单在第一个线程写缓存的时候先将旧的数据写到缓存,导致缓存及db数据不一致
- 先删db再删除redis:出现删除失败缓存的场景就会数据不一致;给reids加expire是一种处理,但在expire时间范围内数据会不一致
- 延迟双删除
一个单机redis缓存需要注意哪些东西
- 对象存储上限
- 删除策略
- 过期时间
- 线程安全
- 阻塞机制:如果有其他线程读取数据库了,该线程应该阻塞,而不是频繁读取数据库
- 实用的接口,简单明了易懂
- 是否需要持久化
请问怎么保持redis缓存和数据库强一致性?有好的解决方案吗?
- 先更新数据库,再删除缓存,删除缓存失败不断重试删除(重试删除,异步删除)
- 使用延时双删。就是删除完redis,更新完数据库过一段时间再删除一次
不隆过滤器

问题
为什么是16384?
很显然,我们需要维护节点和槽之间的映射关系,每个节点需要知道自己有哪些槽,并且需要在结点之间传递这个消息。
为了节省存储空间,每个节点用一个Bitmap来存放其对应的槽: 2k = 2 * 1024 * 8 = 16384,也就是说,每个结点用2k的内存空间,总共16384个比特位,就可以存储该结点对应了哪些槽。然后这2k的信息,通过Gossip协议,在结点之间传递。
crc16得出来最多的槽是65535 消息头就是65535/8/1024是8k;16384是2k
一个是为了传输,2k不是很大,高速别的节点自己有什么
一个是为了存储,用2k存储所有的slot,bitmap格式就知道那个slot有数据
客户端存储路由信息
对于客户端来说,维护了一个路由表:每个槽在哪台机器上。这样存储(key, value)时,根据key计算出槽,再根据槽找到机器。

