Redis框架原理

写在前面

  • 这里主要记录一下Redis的框架原理;

一、Redis概述

1. 定义

  • 是一种基于内存的数据库;
  • 对数据的读写操作都是在内存中完成的;

2. 特点

  • (1) 高性能:

    • 直接在内存读写,速度非常快;
  • (2) 高并发:

    • 采用I/O多路复用机制,可以用一个线程处理多个I/O流;
    • Redis6.0后,采用多个I/O线程处理网络请求;

二、数据类型

  • Redis支持的五种常用的value数据类型如下:
    • String:字符串;
    • Hash:哈希;
    • List:列表;
    • Set:集合;
    • Zset:有序集合;

数据类型

1. String

  • 可以存放字符串或者数字(整数或者浮点数);
  • 最大数据长度是512M
1.1 内部实现
  • 使用int和简单动态字符串SDS

内部实现

  • 有三种编码方式:

    • int:保存可以用long类型表示的整数;
    • embstr:保存小于等于32字节的短字符串;
    • raw:保存大于32字节的长字符串;
  • (1) int只需要保存redisObject和整数即可:

不需要SDS

  • (2) embstrredisObjectSDS空间是连续分配的:

连续分配空间

  • (3) rawredisObjectSDS空间是分别分配的;

分开分配空间

1.2 应用场景
  • (1) 缓存对象:

    • 直接用value缓存整个对象的JSON;
    • key = object.成员变量value拆分对象内容进行缓存;
  • (2) 常规计数;

    • 记录访问次数等;
  • (3) 共享Session信息;

    • 让分布式系统中的所有服务器都去Redis中取用户的Session信息;

2. List

  • 用于存放字符串数组
  • 列表的最大长度是2^32-1
2.1 内部实现
  • 在Redis3.2前:

    • 元素个数少于512,且每个元素值小于64字节,用压缩列表
    • 否则,使用双向链表
  • 在Redis3.2之后:

    • 只使用快速列表

下面介绍两种压缩列表和quicklist的数据结构;

  • (1) 压缩列表ziplist

    • 使用连续内存空间保存链表数据;
    • 只适合保存小规模数据;
    • 因为每个节点都记录前一个节点的长度,所以会有连锁更新问题;
      压缩列表
  • (2) 快速列表quicklist

    • 是压缩列表和双向链表的混合体;
    • 总体是双向链表,可以方便扩展元素;
    • 每个链表节点都是一个压缩列表,进而可以节约空间;

quicklist

2.2 应用场景
  • (1) 消息队列
    • 消息保序:使用LPUSH + RPOP或者RPUSH + LPOP
      • 但消费者只能忙查询,消息队列不会主动通知消费者有消息到来;
      • 可以用BRPOP或者BLPOP做阻塞查询;
      • 一直阻塞到有消息到来,避免忙查询;
    • 重复消息处理:生产者自行实现全局唯一 ID;
    • 消息的可靠性:使用BRPOPLPUSH
      • 在取消息的同时,将消息插入到另一个List中留存;
      • 可以避免消息未正常处理而丢失消息;
    • 但不能让多个消费者消费同一条消息;

3. Hash

3.1 内部实现
  • 在Redis7.0前:
    • 元素个数少于512,且每个元素值小于64字节,用压缩列表
    • 否则,使用哈希表
  • 在Redis7.0之后:
    • 只使用listpack

内部实现

下面介绍哈希表和listpack;

  • (1) 哈希表
    • 使用的是开链法;
    • dictEntry是桶数组,后面接的是dictEntry链表;
    • 使用渐进rehash方法对哈希数组扩容:
      • 仅当有哈希元素进行新增、删除、查找或者更新操作时,才将哈希元素迁移到扩容后的桶数组中;
      • 这样可以避免阻塞Redis导致可用性降低;

哈希表

  • (2) listpack
    • 类似于压缩列表,使用连续的内存空间保存元素信息;
    • 但每个节点只记录当前元素的长度,可以避免连锁更新问题;
    • 目的就是用于替代压缩列表的;

listpack

3.2 应用场景
  • (1) 缓存对象:

    • 按照对象id-对象成员变量-value的方式缓存对象;
  • (2) 购物车:

    • 按照用户id-商品id-value的方式缓存对象;

4. Set

  • 存放无序且非重复元素;
  • 一个集合最多可以存储2^32-1个元素;

内部实现

4.1 内部实现
  • 如果集合中的元素都是整数且元素个数小于512,则使用整数集合
  • 否则,使用哈希表

下面介绍整数集合;

  • (1) 整数集合
    • 本质上是一块连续的内存区域,由数组实现;
    • 可以保存163264位的整数;
    • 支持升级,也就是从保存短整数升级到支持长整数;
      • 扩容的时候先附加所需空间,然后从后向前拷贝已有元素到扩容后的位置;
      • 这样可以避免使用额外的空间和覆盖掉原有的数据;
        整数集合
4.2 应用场景
  • (1) 点赞:

    • 一个用户只能点一个赞;
    • 则某个文章的点赞用户可以用Set来保存;
  • (2) 共同关注:

    • 可以用集合的交集运算获取两个集合之间的交集;

5. Zset

  • 存放有序且非重复元素;

内部实现

5.1 内部实现
  • 在Redis7.0之前:
    • 如果有序集合的元素个数小于128个,且每个元素的值小于64字节,则使用压缩列表
    • 否则,使用跳表
  • 在Redis7.0后:
    • 只使用listpack

下面介绍跳表;

  • (1) 跳表
    • 相当于是一个多层双向链表;
    • 每一层链表的跨度(搜索粒度)都不一样,这样可以加速查找的过程;
      • 查找时先从最高层开始查找;
      • 最高层查找不到再往下一层查找,也就是往细粒度查找;
    • 决定每个节点的层数策略:
      • 随机生成每个节点的层数;
      • 如果rand < 0.25,则为当前节点增加一层,直到rand >= 0.25为止;
      • 最大层数一般设置为32层或者64层;
    • 和平衡树相比的优势:
      • 内存使用更少,每个节点平均只需要1/(1-p)个指针,p=0.25
      • 做范围查找时,操作更简单,只需要顺序访问第0层节点即可;
      • 实现也更加简单
    • 也可参考:

跳表

5.2 应用场景
  • (1) 排行榜:

    • 可以根据权重进行排序;
  • (2) 电话或者姓名排序;

6. 总结

  • 常用的五种数据类型和它们对应的底层结构如下:

数据结构

三、线程模型

1. 单线程模型

  • 其实就是单线程多路复用模型;
  • 根据不同的事件调用不同的事件处理函数;

单线程模型

2. 其他线程

  • 除了执行命令的Redis主线程外,还有一些其他的线程负责其他功能:
  • 三个后台线程:
  • (1) bio_close_file:异步关闭文件;
  • (2) bio_aof_fsync:AOF重写任务;
  • (3) bio_lazy_free:释放内存任务;
  • 以及三个I/O线程,负责处理网络I/O;

四、持久化

  • 目的:
    • 保证数据在Redis重启后不丢失;

1. AOF日志

  • AOF日志文件中包含了能够完整重建Redis数据库的所有写命令;
  • 逐一执行所有的写命令即可还原Redis的数据状态;
1.1 实现
  • 在执行完一条写操作命令后,将该命令以追加的方式写入到AOF日志文件中;
    • 先执行后写日志
    • 好处:
      • 不会阻塞写操作执行;
      • 避免写日志时的语法检查开销;
  • 恢复时,读取AOF日志文件中的内容进行恢复;

实现

执行过程

1.2 三种写回策略
  • (1) Always

    • 每次写操作命令执行完后,同步将AOF日志写入到硬盘中;
  • (2) Everysec

    • 每次写操作命令执行完后,先将命令写入到AOF文件的内核缓冲区;
    • 然后每隔一秒将缓冲区里的内容写回到硬盘;
  • (3) No

    • 由内核决定何时将缓冲区中的内容写回到硬盘;

总结

1.3 重写机制
  • 作用:
    • 降低AOF文件所占的空间;
    • 主要是避免记录多个相同的key写入命令,仅保存最新的一条写入命令;
  • 实现:
    • 当AOF文件超过一定大小时,进行重写;
    • 读取当前数据库中的所有键值对(这些键值对就对应最新的写入命令);
    • 将每一个键值对用一条命令记录到新的AOF文件中;
    • 最后用新的AOF文件替换旧的AOF文件;

2. RDB快照

  • RDB快照记录某一个瞬间的内存数据,而非写操作命令;
  • 恢复数据的效率比AOF文件高;
  • 但可能会丢失较多数据,因为快照不能实时更新;
2.1 实现
  • 记录快照时,直接fork()一个子进程;
  • 由于子进程和父进程共享内存数据,所以子进程为父进程进行后台的快照记录;
  • 由于父子进程之间的内存使用的是写时复制技术,所以父进程仍然可以正常更新内存数据,不影响子进程记录快照的过程执行;

复制快照

3. 混合持久化

3.1 实现
  • 在重写AOF日志时,先直接用RDB快照格式记录主要部分的数据;
  • 再将重写缓冲区中的增量数据按照AOF日志格式写入到文件中;
  • 这样AOF日志文件就分为两部分的内容:
    • 第一部分是RDB格式的内容;
      • 保存了主要的数据;
      • 加载速度快;
    • 第二部分是AOF格式的内容;
      • 保存了重写时的增量数据;
      • 加载速度慢,但可以保持数据的时效性;
  • 之后的AOF日志记录可以跟着第二部分继续添加;
  • 混合的AOF日志文件结构如下:

混合的AOF日志文件

五、高可用

  • Redis主要是通过以下方式实现高可用:
    • 主从复制;
    • 哨兵模式;
    • 切片集群;

1. 主从复制

  • 特点:
    • 一主多从;
    • 主从复制;
    • 读写分离:
      • 从服务器是响应客户端的读请求,主服务器是响应客户端的读写请求;

主从复制

2. 哨兵模式

  • 监控主从服务器;
  • 提供主从节点故障转移功能;

哨兵模式

3. 切片集群

  • 当Redis中缓存的数据在一台服务器上无法缓存时,就需要使用切片集群方案;
  • 即将缓存的数据分布到多台服务器上,以此降低系统对单台服务器的依赖;
  • 具体是通过求模运算实现不同key的分配的;

切片集群

六、键值过期删除

  • 过期删除策略是指:
    • key设置过期时间;
    • 在过期时间之后,删除过期的键值;
    • 通常是采用惰性删除+定期删除的策略;

1. 惰性删除

  • 实现:

    • 不主动删除过期的key
    • 等到访问该key时再验证是否过期;
    • 如果过期则删除该key值;
  • 优缺点:

    • 对CPU占用少,因为不需要专门进行过期key的扫描;
    • 对内存占用大,因为有些过期的key不能定期清理;

2. 定期删除

  • 实现:

    • (1) 随机选20key,检查是否已过期;
    • (2) 如果检查已过期的key的数量占比超过25%,则重复步骤(1),否则完成本次删除;
    • 定期删除的最大时间不超过25ms
  • 优缺点:

    • 对CPU占用大;
    • 对内存占用小;
    • 但定期删除的时间间隔要合理设置,以取得平衡;

七、缓存

  • 这里的缓存其实就是Redis数据库,因为它充当了MySQL的中间缓存;

1. 缓存引发的问题

1.1 缓存雪崩
  • 原因:
    • 大量缓存数据在同一时间过期或者Redis故障宕机
    • 大量用户请求无法通过Redis响应,必须访问数据库;
    • 进而导致数据库承压过大而崩溃;

缓存雪崩

  • 解决方法:
    • 针对大量缓存同时过期:
      • (1) 均匀设置键值的过期时间
      • (2) 设置互斥锁,让同一时间只有一个请求访问数据库来构建未击中的缓存;
      • (3) 后台更新缓存,不设置键值的过期时间;
    • 针对故障宕机:
      • (1) 服务熔断或者请求限流机制,如果Redis无法访问则直接返回错误,不访问数据库;
      • (2) 构建Redis集群,避免宕机;
1.2 缓存击穿
  • 原因:

    • 某个热点数据过期,导致大量请求直接访问数据;
    • 进而冲垮数据库;
  • 解决方法:

    • 和缓存雪崩的解决方法类似:
      • (1) 设置互斥锁,让同一时间只有一个请求访问数据库来构建未击中的缓存;
      • (2) 后台更新缓存,不设置热点数据键值的过期时间;
1.3 缓存穿透
  • 原因:

    • 用户访问的数据既不在Redis缓存中,也不在数据库中;
    • 所以每次请求都需要访问数据库进行验证,进而冲垮数据库;
  • 情景:

    • 业务误操作,导致数据被删除;
    • 黑客恶意攻击;
  • 解决方法:

    • (1) 限制非法请求
    • (2) 在Redis中缓存空值,这样只需要访问数据库一次;
    • (3) 使用布隆过滤器记录数据不存在,这样也只需要访问数据库一次;
      • 布隆过滤器是通过哈希值记录是否存在的;
      • 如果不存在,则真的不存在;
      • 如果存在,则可能不存在,因为会有多个哈希key映射到同一个值上;

布隆过滤器

1.4 总结

总结

2. 缓存一致性

  • 使用Redis的前提是保证Redis始终是数据库的实时缓存;
  • 一旦出现Redis和数据库数据不一致的情况,则Redis的使用是没有意义的;
  • 所以考虑缓存一致性是重中之重;
2.1 更新数据库时更新缓存
  • (1) 先更新数据库再更新缓存
    • 如果有两个并发的更新,会导致缓存和数据库不一致;
    • 不推荐使用;

并发更新

  • (2) 先更新缓存再更新数据库
    • 更不推荐,因为连数据库内的记录都无法保证是最新;

并发更新

2.2 更新数据库时删除缓存
  • 即旁路缓存策略(Cache Aside);
  • (1) 先更新数据库再删除缓存
    • 推荐使用;
    • 虽然也会导致缓存不一致,但发生的概率比先更新数据库再更新缓存小得多;

旁路缓存

  • (2) 先删除缓存再更新数据库
    • 因为更新数据库的速度远比更新缓存的速度慢,所以出现缓存不一致的概率很大;
    • 不推荐用这种方式;

并发读写

2.3 保证删除缓存成功
  • 更新数据库时,如果删除缓存不成功,则缓存将一直不一致;
  • 解决方法:
    • (1) 设置键值定期删除
    • (2) 增加消息队列保证缓存能够成功删除,如果删除失败则不断重试;

八、分布式锁实现

1. 实现原理

  • 加锁:

    • 多个Redis服务器插入键值;
    • 如果key不存在则插入,否则则插入失败;
    • 如果半数以上的服务器均插入成功且未超时,则加锁成功;
    • 如果加锁失败,还需要向各个Redis节点解锁;
  • 解锁:

    • value唯一标识加锁的客户端;
    • 只有加锁的客户端才能进行解锁;
    • 同时也应该为key设置过期时间,避免锁一直无法释放;
  • 19
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值