中级开发的经验之谈(redis篇)

一、前言

在我读书的时候,我曾经很喜欢redis,听了相关的分享、看了相关的博客、读了相关的书、看了喜欢的源码,然后我写了一个总结:《这是全网最硬核redis总结,谁赞成,谁反对?》六万字大合集

现在,这篇文章大概五万多阅读,三千多收藏,三千多点赞。

但是,随着时间的推移,我渐渐的有点看不上这篇文章,主要是觉得写的太多太全了,像流水账一样,好像什么都说了,又好像什么都没说。

所以,现在我有了一些开发经验,有了一些心得体会,我准备写一个浓缩全是精华的分享,写一个初中高级开发看了,都能有收获的分享。

本文主要是告诉你为什么我们使用redis,以及使用时可能遇到的问题。

下文括号中,都是我在公司讲课时,详细讲到的地方,非括号是文章正文。

二、简介

2.2 应用举例

1、延时队列(股票定投/)

2、布隆过滤器

  • 缓存,解决本地缓存命中率太低的问题

(举例发布时读db超时挂掉)

三、性能好

3.1 数据结构

3.1.1 字典

字典,是一种用于保存键值对的抽象数据结构,在Redis中的应用相当广泛,比如Redis的数据库就是使用字典作为底层实现的,对数据库的增删改查等操作也是构建在对字典的操作之上。

特点:

  • 使用的哈希算法:murmurhash,链表处理冲突

(哈希打单点事故的案例分享)

(即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,计算速度非常快,使用简单。因此在多个开源项目中得到应用,包括libstdc、libmemcached、nginx、hadoop等。)

  • rehash

(元素个数=数组长度扩容/bgsave时五倍,小于10%缩容 1)为ht[1]分配合理空间。2)将ht[0]中的数据rehash到ht[1]上。3)释放ht[0],将ht[1]设置为ht[0],ht[1]创建空表,为下次做准备。)

  • 渐进rehash

(我们维持一个变量rehashidx,设置为0,代表rehash开始,然后开始rehash,在这期间,每个对字典的操作,程序都会把索引rehashidx上的数据移动到ht[1]。

随着操作不断执行,最终我们会完成rehash,设置rehashidx为-1.)

3.1.2 简单动态字符串(SDC)

Redis没有直接使用C语言传统的字符串表示,而是自己构建了名为简单动态字符串(simple dynamic string ,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。(除了保存数据库中的字符串值之外,SDS还被用作缓冲区,例如AOF的AOF缓冲区、客户端状态中的输入缓冲区等。)

struct sdshdr {

int len;//buf已使用字节数量

int free;//未使用的字节数量

char buf[];//用来保存字符串的字节数组

};

数据结构如图表示:

相对c的改进:

(1)常数复杂度获取字符串长度

(2)杜绝缓冲区溢出(这里是指相加的时候)

(3)减少修改字符串时带来的内存重分配次数(空间预分配、惰性空间释放)

(还有二进制安全之类的)

3.1.3 skiplist

(Redis只在两个地方用到了跳跃表,一个是实现有序集合,另一个是集群节点中用作内部数据结构。)

跳跃表是一种有序数据结构,在大多数情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树更为简单,所以有不少程序使用跳跃表代替平衡树。

数据结构如图表示:

Redis为何不用红黑树?优越性和特殊性

todo:演示两个数据结构的操作

3.1.4 链表/整数集合/压缩列表

  • 链表:带表头、双端无环、带长度记录、多态

  • 整数集合:各个项在数组中从小到大有序排列,并且数组中不包含任何重复项。

  • 压缩列表

占空间小,由一系列特殊编码的连续内存块组成的顺序型数据结构,为了节省空间,Previous_entry_length甚至大小可以变化

todo:数据结构精讲\布隆过滤器\HyperLogLog等

3.2 对象

3.2.1 组成

对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象五种类型的对象,每种对象都用到了至少一种前面介绍的数据结构。

数据结构如图表示:

通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率。

这种思想在其它语言/组件/项目中也有体现。因地制宜,根据场景选择适用的方案才是正路。

事实上redis新版加入了压缩表+链表的结构,也是基于这种思想。

3.2.2 内存回收/对象共享

1.Redis在自己的对象系统中构建了一个引用计数技术(并未采用可达性分析)实现内存回收机制。

2.通过引用计数技术,实现对象共享机制,通过让多个数据库键共享一个对象来节省内存。数据库中相同值对象越多,对象共享机制就能节约越多的内存。

(引用计数:(1)在创建一个新对象时,引用计数的值会被初始化为1;

(2)当对象被一个新程序使用时,它的引用计数值会被增一;

(3)当对象不再被一个程序使用时,它的引用计数值会被减一;

(4)当对象的引用计数值变为0时,对象所占用的内存会被释放;

引用简单,可达性(图论,可达即有用)需要遍历。

循环引用的问题,可能是简洁的思想和对系统的自信吧)

3.3 单机数据库实现

3.3.1 访问框架

todo:动态链接库和网络框架

3.3.2 线程模型

我们之前所说的Redis 是单线程,主要是指 对外提供键值存储服务的主要流程是单线程的,也就是网络 IO 和键值对读写。其它功能如持久化、异步删除、集群数据同步等很早就是多线程。

那么Redis起初并不是多线程模型,主要原因:没必要

  • 性能瓶颈不在 CPU

  • 单线程模型,可维护性更高,开发/调试/维护的成本更低

  • 单线程模型避免了线程间切换带来的性能开销

  • 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率(虽然本质还是阻塞的)

Linux多路复用技术,就是多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。

(科普:多线程的目的,就是通过并发的方式提升I/O利用率和CPU利用率。)

Redis 将所有数据放在内存中,内存的响应时长大约为 100 纳秒,对于小数据包,Redis 服务器可以处理 80,000 到 100,000 QPS。

todo:redis6.0

(redis6.0还是真香了。顺序、分身、监听。在select、poll、epoll这些调用会阻塞,收发消息不会阻塞。挖坑下次填)

四、可靠

4.1 单机可靠

4.1.1 空间管理

机制一、设置最大空间:Redis 提供参数 maxmemory 配置 Redis 最大使用内存。

机制二、设置生存时间:可以给键设置生存时间(UNIX时间戳),到时间自动删除这个键。

redisdb结构的expires字典保存了所有的键的过期时间,我们称这个字典为过期字典。

机制三、过期键删除策略:

1)定时删除:创建一个定时器,到时间立即执行删除操作(对内存友好,因为能保证过期了立马删除,但是对cpu不友好)

2)惰性删除:键过期不管,每次获取键时检查是否过期,过期就删除(对cpu友好,但是只有在使用的时候才可能删除,对内存不友好)

3)定期删除:隔一段时间检查一次(具体算法决定检查多少删多少,需要合理设置)

机制四、淘汰策略

当Redis占用内存超出最大限制 (maxmemory) 时,采用可配置的一些策略 (maxmemory-policy) ,让Redis淘汰一些数据:

•volatile-random: 在设置了过期时间的key中,随机选择一些key,将其淘汰;

•allkeys-1Lru: 在所有的key中,选择最少使用的key (LRU) ,将其淘汰;

•allkeys-random: 在所有的key中,随机选择一些key,将其淘汰;

4.1.2 持久化

4.1.2.1 RDB(Redis DataBase)

通过生成数据集的时间点快照(point-in-time snapshot)来实现持久化。

  • 自动触发:

自动触发使用 save 相关配置触发,比如 “save m n”,表示在 m 秒内数据库存在 n 次修改时,自动触发 BGSAVE 。

  • 手动触发:

SAVE:执行一个同步保存操作,将当前实例的所有数据快照以 RDB 文件的形式保存到磁盘中。

BGSAVE:用于在后台异步保存当前数据库的数据到磁盘。

命令savebgsve
IO类型同步异步
是否阻塞是(fork内)
复杂度O(n)O(n)
客户端是否阻塞阻塞客户端命令不阻塞客户端命令
是否消耗资源不额外消耗资源需要fork消耗资源

4.1.2.2 AOF(Append-only file)

AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态,重启时再重新执行 AOF 文件中的命令以完成数据恢复。AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。

todo:自动重写

rdb优点:RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。

RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

缺点:发生故障时会丢失数据。虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。

每次保存 RDB 的时候,Redis 都要 fork出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。

优点:

  • 使用 AOF 持久化会让 Redis 变得非常耐久,可以设置多种策略,发生故障停机,也最多只会丢失一秒钟的数据

  • AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令,也可以使用工具进行修复

  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写。

  • AOF 文件有序地保存了对数据库执行的所有写入操作, (如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。)

缺点:

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此

  • AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样

todo:RDB/AOF的详细介绍

todo:事务

4.2 多机可靠

todo:多机可靠

4.2.1主从

4.2.2 哨兵

可扩展

todo:

一些坑

比如说,有做缓存的,有做数据库的,也有用做分布式锁的。不过,他们遇见的“坑”,总体来说集中在四个方面:

CPU 使用上的“坑”,例如数据结构的复杂度、跨 CPU 核的访问;

内存使用上的“坑”,例如主从同步和 AOF 的内存竞争;

存储持久化上的“坑”,例如在 SSD 上做快照的性能抖动;

网络通信上的“坑”,例如多实例时的异常网络丢包。

附录:参考资料

USFCA算法图像模拟器

压缩列表参考

小灰:什么是跳表

更严谨的跳表文章

知乎:redis为什么这么快

在线模拟redis

Redis 命令参考

《Redis设计与实现》

《Redis实战》

《Redis入门指南(第2版)》

  • 245
    点赞
  • 230
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兔老大RabbitMQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值