Redis 2 面向底层与工程
吃水不忘挖井人, 教程视频:https://www.bilibili.com/video/BV1if4y1R7ns?p=13(这个视频虽然有点东西但是排序极烂)
首先我们来看一下Redis的用途:
- 缓存,可以将DB中的数据读到Redis中应对大范围的响应,容量更大的同时速度也更快
- 计数器。 Redis本身是单线程的,读写性能很高,同时支持String自增加减
- 分布式ID生成。与上面的的计数器相同
- 海量数据统计: Redis的bitmap, 见1的数据结构
- 会话缓存:Redis同一存储堕胎应用服务器的会话信息。
- 分布式队列: 参见Redis数据结构的List
- 分布式锁: 参见https://blog.youkuaiyun.com/ugg/article/details/41894947
Redis本身的结构(写太多的结构体太过杂乱,放这张关系图一目了然)
Redis 数据结构重修
有比较清晰的博客先镇楼:https://www.cnblogs.com/ysocean/p/9080940.html
Redis.key、String
在Redis中,所有的key都是String类型。Redis底层使用C语言实现的,但是其String类型与C的 char * 略有不同。
Redis <= 3.2
class sds(): #SDS simple dynamic string 可变长度字符串
self.len(int) #数据长度
seld.buf = [] #缓冲,用于存储数据
self.free(int) #剩余空间(如果不够用就将buf扩容。!扩容一般扩len的长度,如超过1024扩容每次只扩1024)
Redis >= 3.2
#具体优化的地方(代码太长了不放了):
取消了 free 剩余空间,增加了 alloc 分配的最大空间
uint8/16/32 len
uint8/16.32 alloc
# 节省了len,free(32 bit)的孔家,更灵活地根据字符的长度选择。最优解
String key存储类型流程:
key - > 求hash - > 放到hash数组(角标为hash % len) -> 如果出现hash冲突 -> 在此地址头插法插入key来搞定hash重读
String 的编码:
- 判断String 是否小于 2 ** 64
- 如果小于,就可以用ember编码,日后也可以用INCR 增长和指定步长增成长
- 否则就直接存储String的地址啦
String 的编码查看的方法:
- object encoding key
Redis Bitmap
问题提出:如何统计连续打卡的用户
针对这个问题,我们第一反应就是在用户中新增一个打卡天数用来记录用户那天打卡了,这样的话我们锁要耗费的空间资源就是 (len(time)) * len(struct(date))。那么问题来了,对应date这种绝对不可能重复的数据,且结果为True(打卡成功)/ False(打卡失败)的boolen类型,我们有没有方法优化呢?
答案是显然的,由于结果只有0,1两种选择,我们将struct(date)数据结构缩减为 1 bit就可以了。实现方法如下
日期 | 周一 | 周二 | 周三 | 周四 | 周五 | 周六 | 周日 |
---|---|---|---|---|---|---|---|
是否打卡 | × | √ | √ | √ | √ | × | × |
优化表示 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
这样我们的7天就可用7bit来表示了,从而压缩了数据量。这种表示方法就是bitmap
在Redis中可以采用 setbit, getbit的方法对bit的层次进行操作, 更方便我们进行Bitmap的操作
(未完)
Redis List
List是一个有序(按加入的时序排序)的数据结构。 Redis采用 quicklist(双端连边) 和ziplist 作为List的底层实现。 可以通过设置每个ziplist的最大容量, quicklist的数据压缩范围,提升数据存取效率。
首先明确链表的概念。链表是一种在物理上不相连的,长度可变的数据结构,在插入,删除节点的时候链表相当灵活,但是对特定偏移量的查找时略微力不从心。此外,链表属于碎片化存储,有可能将内存原来连续的空间分割为大小不一的离散空间。
当然,抛开上面那些假大空的提升效率之流,我们要面对的第一个问题就是ziplist是啥
struct ziplist(){
int zlbytes; # int 32bit zbytes表示整个ziplist占用的空间大小
int zltail; # 找到队尾的偏移量
uint16_t zllen; # ziplist的长度
entry * entrys; # 中间的实体元素
uint8_t zlend = 255; # 表示队列结束
}
struct entry(){
uint8_t * prerawien; # 这是一个可变的前一个元素修饰符 大小取决于前一个元素的大小:
(if len(before_entrys) < 254 byte >>> prerawien: 1 byte else prerawien: 5 byte)
uint8_t * len; # 长度,但是多长有具体的编码,太复杂了放不下
int<len> data; # 根据长度的数据切片
}
讲完了ziplist, 我们就可以来看看quicklist 。 quicklist本身就是一个双向的链表,其中的节点的数据就是使用 ziplink(上面的结构)进行存储。结构图如下
就此, Redis的List结构就已经搞定了。
Redis Hash
Hash数据结构底层就是一个字典,也就是RedisBb存储的数据结构,当数据量比较下,或单个元素比较少的时候,底层也是用ziplist存储的,可以看见下图 ,意思就是empty(n) 是 empty(n+1) 的键值 (当然,数据量大或单个元素超过entry的data阈值的时候就直接退化成字典了)
Hash的弊端:
- Hash的filed字段不允许设定TTL, 有一些冗余的字段会长时间的存储浪费空间
- 在Hash中不断加key, 当超过原来的存储上上限时就会扩容,每次扩容的大小是当前容量的一倍,造成内存的大范围消耗
Redis Set
Set 是无序的,自动去重的集合数据类型,Set数据结构底层是一个default value = Null 的字典 当数据可以用整形表示是,Set 集合被编码为intset结构(有序)。否则Set 将用字典存储数据(注,当元素个数较多时(> set-max-itset-entries ),Set也会用字典存储(无序))
- 所以说Set不一定是无需哒(趾高气昂)
intset数据结构:
struct intset(){
uint32_t encoding; # 编码类型
uint32_t length; # 元素个数
int8_t contents[]; # 元素存储
}
Redis Zset:
Zset 在Set的基础上给每个Member加上了 score并基于此排序,即具有了有序且自动去重的特性
Zset的数据底层应该为为dict 与 **skiplist (跳表)**除非数据较少时会用ziplist。~看看,什么叫做万能数据结构 。使用ziplist的时候其本身如同Hash一样的字典存储就可。
首先我们要知道什么叫跳表。(参考:https://zhuanlan.zhihu.com/p/68516038)
首先明确:跳表是一个链表(排好序的),存在的目的是为了减小查找时间O(logn)。本质的原理就是给链表建立多层索引,在索引范围内进行向下的索引查找,可以见下图。当然这不是毫无代价的, 插入的时间复杂度也从O(1) 到了O(logn)
Redis的实现
# 跳表的Node
struct zskiplisNode{
sds ele;
double score;
# 这里是保存上个索引节点的地址
struct zskiplisNode * backNode;
# 这里是下一层的索引
struct zskiplistLevel{
struct zskiplistNode *forward;
unsigned long span;
} lever[];
}
# 跳表本身
struct zskiplist{
# 头尾节点
struct zskpilistNode * header, * tail;
unsigned long length;
int level;
}
# Zset的结构
struct zset{
# 字典
dict * dict;
# 跳表
zskiplist * zsl;
}
# 跳表的创建
zskiplist *zslCreate(void){
int j;
zskplist *zs;
# 分配空间
zsl.zmalloc(sizeof(*zsl));
# 设置其实层次
zsl-> level = 1;
zsl-> length = 0;
#ZSKIPLIST_MAXLEVEL 应该是提前definf好的,创建初识最高层
zsl-> header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
# 遍历一层一层创建
for (j=0;j<ZSKIPLIST_MAXLEVEL;j++){
zsl-> header->level[j].forward
= NULL;
zsl-> header->level[j].span = 0;
= NULL;
}
# 设置表头后退指针为NULL, 表尾为NULL
zsl -> header -> backward = NULL;
zsl -> tail = NULL
return zsl;
}
上面的代码可能还有点抽象,我们可以用如下的图来表示,可以把概念不太明晰的level结构题数组和节点间的联系就串到一起了:
这样,基本数据类型的底层就差不多了解了大概。
Redis持久化
Redis虽然是一个缓存型数据库,但是amazing的是竟然提供了持久化的能力(但是感觉没什么用)
目的: 为了预防服务宕机内存数据丢失。
Redis提供了两种持久化数据的机制:RDB, AOF
RDB机制
RDB 全称 Redis Database, 是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照(数据库中所有键值对数据)。恢复时是将快照文件直接读到内存里。因为Redis定时拍快照的方法,可以将某一时间节点(快照时间点)之前的所有数据进行恢复。
RDB的除法方式有两种:
-
配置文件,定时生成快照(配置文件的阈值为 time, exec num)
save 60 3 # 在60秒内执行了三次exec生成快照 save 300 50 # **300*******五十次*********
-
手动执行命令: bgsave(fork一个子进程完成持久化) / save(阻塞主进程直至快照制作完毕)
RDB的过程中可能出现的问题:
- RDB快照中Redis 是否会执行新的命令(bgsave可以不阻塞进行快照制作,通过fork的子进程进行数据的持久化)
- 在快照是如果接受了新的写命令,操作系统会把主进程的内存也修改并给他一个副本,子进程不受影响。快照创建结束后子进程应有主进程回收
- COW CopyOnWrite: 多个调用者同时请求相同的资源,它们会共同获取相应的指针指向相同的资源,知道某个调用者试图修改资源内容时,系统才会真正赋值一个专用副本给调用者,而其他调用者所见到的最初的资源任然保持不变。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。参见https://zhuanlan.zhihu.com/p/114547256
RDB的数据结构:
Conclusion:
Advantage:
- 数据非常紧凑,重启数据时Redis解析RDB二进制数据,这些数据恢复到内存中效率更高,保证Redis的高性能
- Fork子进程保证业务的连续性
Disad:
- 数据安全性低,保存的是时间点而不是即时存储。
- 在fork()时如果产生大量写操作就会导致COW的共享内存页优势荡然无存,资源消耗巨量
AOF机制
既然上述讲了RDB的致命缺陷:秒级甚至分钟级的备份缺失,那另一个策略必然是用来解决这个问题的,不然要你何用
AOF的机制完全可以一言蔽之:
命令 – > 写入AOF缓冲 --> 同步策略 --(sync) --> AOF文件 <= |
|____________________________|(重写策略 rewrite)
同步策略:
- always 只有写成功才执行。 零错误,但是性能影响(IO太多)
- everysec 每秒,在数据备份和性能上中和
重写策略:不可能只备份不删除,那文件不是大到炸了
auto-aof-rewrite-percentage 100 #增量阈值百分比
auto-aof-rewrite-min-size 64 #AOF文件阈值
- 当AOF文件的大小大于预定的法制且增量大于增量阈值百分比会触发AOF重写。
Conclusion:
Ad:
- 数据持久化更加安全
Dis:
- 数据集大的时候会进行指令重放,效率与RDB比较差
4.0后的优化版本 RDB mix AOF, 兼顾RDB速度与AOF安全
Redis处理过期数据
什么时国企过期数据,过期数据指手动对对胡菊设定了TTL且TTL超时的数据。Redis使用两种策略删除过期键:
- 惰性删除,为了保证单线程的Redis不被阻塞,Redis在见值超时后并不主动删除,而是再次访问/发现其超时时才进行删除:
- Ad: Easy, Not Block Main Thread
- Dis: some space abused
- 定期删除:周期性的随机测试一批过期时间key进行处理, 测试到的key会被删除(机制太长,放个链接https://www.cnblogs.com/itplay/p/10162935.html)
- 立即删除。在设置键的过期时间时,创建一个回调事件,当过期时间达到时,由时间处理器自动执行键的删除操作。
Redis内存淘汰
当Redis的内存超过允许的最大内存后,Redis会触发内存淘汰机制.
在不同版本中采用不同机制:
版本 < 4.0:
- noeviction: 不淘汰任何数据,内存不足时执行新增操作报错, Redis默认的淘汰策略
- allkeys-lru: 淘汰最久未使用的键值(见LRU知结局)
- allkeys-random: 随机抽出幸运观众枪毙(误)
- volatile-lru: 淘汰所有设置了过期时间的键值中最久未使用的键值
- volatile-random: 上面random的翻版,范围缩小到有国企时间的KEY, 即种族歧视(不是)
- volatile - ttl: 淘汰即将最早过期的键值
版本 >= 4.0:
新增:
- allkeys- lfu : 算法换了flu ,还是一眼看穿结局的类型
- volatile-lfu: 同上 + 1
缓存穿透,缓存雪崩,明天一定
淘汰策略**
- allkeys-lru: 淘汰最久未使用的键值(见LRU知结局)
- allkeys-random: 随机抽出幸运观众枪毙(误)
- volatile-lru: 淘汰所有设置了过期时间的键值中最久未使用的键值
- volatile-random: 上面random的翻版,范围缩小到有国企时间的KEY, 即种族歧视(不是)
- volatile - ttl: 淘汰即将最早过期的键值
版本 >= 4.0:
新增:
- allkeys- lfu : 算法换了lfu ,还是一眼看穿结局的类型
- volatile-lfu: 同上 + 1
缓存穿透,缓存雪崩,明天一定