Redis 核心原理

·  阅读 84

Redis 为什么这么快?

redis 为什么这么快,我觉得主要是三个原因,一个是基于内存的读写操作,一个是io模型,一个是高效的数据结构。

基于内存数据库

redis 所有操作都是基于内存的,内存的io效率甩磁盘十几条大街。

io多路复用

Redis单线程模型,指的是 redis 的 io 操作和数据读写是单线程的,redis 基于IO多路复用,可以用一个线程监听多个io连接,并且可以向io事件注册相应的回调事件,也就是react响应式编程。这样一个线程可以处理多个客户端连接,单线程也避免了由于线程切换带来的开销,让所有请求都能够串行处理,保证了redis请求的有序性及高效性。

高效数据结构

说到内存数据库,我们知道 redis 并不是最先出现的,最先出现的是 memcache,memcache 是先出现的一种key,value格式的内存数据库,在 redis 没出现前,缓存一般都是用的 memcache ,那么,为什么 Redis 能取代 memcache 呢?就是因为 redis 数据类型的支持更加符合我们的需求。

举个例子:

存储的是一个用户信息,需要取用户中的某一个属性

memcache:由于 memcache 只支持简单的key value, 客户端需要把整个对象全取回来,解析,获取指定属性;

redis:redis 有 hash 数据结构存储,客户端直接发送命令取回指定属性。

简单的说,就是操作向数据迁移,减少了 io 交互。

redis 的数据类型

  1. string
  2. hash
  3. list
  4. set
  5. sortset

redis 数据类型的底层实现

  1. 简单动态字符串(sds)

    记录了字符串长度,已用空间信息,是二进制安全的,可保持图片和音视频信息。

  2. 哈希

  3. 双向链表

  4. 压缩列表

    压缩链表类似于一个数组,与数组不同的是,压缩列表表头有三个字段,zlbytes,zltail,zllen,分别列表长度,列表尾的偏移量,和列表中 entry 的个数;压缩列表在表尾还有一个zlend,标识列表结束。

  5. 整型数组

  6. 跳表

    跳表是在有序链表的基础上增加了多级索引,通过索引位置的跳转,快速定位数据。

    例如:

    1 --------------- 5 一级索引

    | |

    1------ 3------- 5 ------ 7 二级索引

    | | | |

    1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 数据

tips

redis 虽然快,但是我们要了解一些可能对 redis 产生阻塞的慢操作,毕竟 redis 是单线程模型,对集合的范围查询,keys * 等全量查询,时间复杂度(logn),当 redis 里面数据量特别大的时候,需要谨慎使用。

Redis 如何防止数据不丢?

RDB

什么是 RDB ?

RDB 是 redis 默认开启的一个持久化机制,如果执行了 save(阻塞) 命令或者 bgsave(非阻塞的)命令,那么,redis 会生成这一时刻的数据快照,持久化到redis数据盘的RDB文件里。后续如果redis故障重启,可通过 RDB 文件快速恢复 redis 中的缓存数据。

save 和 bgsave 指令的区别?

save 是阻塞的,执行此命令会导致 redis 处于一个阻塞状态,知道数据备份完成。所以我们一般是用 bgsave 命令,执行了 bgsave 命令,redis 会 fork 出一个子进程,去处理生成RDB文件的任务,不会阻塞 redis 。在子进程没有完成 数据落盘的操作期间,如果 redis 发生了写操作,那么 redis利用了cow(写时复制)技术,对于写操作的单独分配内存操作,不影响子进程,待子进程生成完 rdb 文件后,会将 redis 在此期间接收到的所有写操作发送给子进程,将新指令也持久化到 RDB 文件中,完成持久化操作。

AOF

什么是AOF

AOF日志是对redis的每一个操作命令的日志记录,是命令执行成功后写的。我们可以在redis中开启AOF日志来启用AOF。

AOF重写

redis 的每条操作命令都会记录一条日志,运行时间久了,起始会产生庞大的数据操作日志,尽管是对同一个key的反复操作,起始,我们只需要这个key最终的保存结果即可,所以,AOF日志会有AOF重写这么一个说法,重写的时机是可以在配置文件调整的,AOF重写也需要分析key并整合最终的key对应的结果命令,例如:执行了 set k1 111;set k1 222;set k1 333,重写后只会有一条:set k1 333 的命令,这种分析也是需要耗费相当的cpu资源的,所有 AOF 重写也是会导致redis阻塞的,是会影响redis性能的一个因素,需要权衡。

Redis 单机问题?

假设我们只有一个redis服务,这会存在哪些问题?

  1. 单点故障问题,redis挂了,不能提供服务。
  2. 压力问题,高并发请求下压力增大。
  3. 容量问题,一个redis存不下我需要存储的业务数据时,有没有类似于mysql的分库分表解决方案,或者es的分片机制?

那么,我们前面讲的 redis 复制可以解决单点问题和部分压力问题,主从复制,主负责写,从负责读,所有的写操作都必须由主节点进行操作。

搭建一个redis集群,用 slaveof 或者 replicof 追随相应节点,构成redis集群,每个redis通过主从复制保持数据一致性,这样就解决了单点故障问题,我们称这种集群成为复制集群,因为每个redis实例保存的是全量数据,我们只需要保证主从节点数据一致性即可,这种集群一般写操作由主节点执行,从节点可以分担主节点的读压力,这种集群方式一定程度上解决了单点问题和部分压力问题。但是还存在几个问题:

如何保证数据一致性呢?

答案是主从复制,那么 redis 主从复制是怎么做的呢,

  1. 从节点向主节点发送 psync 命令

  2. 主节点收到命令后根据参数判断是全量同步还是部分同步,全量同步会生成 RDB 文件发送给从节点,并返回 主库 runid 和 复制偏移量 offset

  3. 从节点收到数据后,记录主库返回信息,若是增量同步,则同步对应数据,若是全量同步,会执行 flashdb 命令清空数据库,开始同步全量数据。

  4. 从节点同步完后,主节点将同步期间产生的操作命令记录到 replication buffer 中,发送给从节点,同步完成。

tips

补充两个知识点,repl_backlog_bufferreplication_buffer

为了防止每次全量同步,主节点中有一个环形缓冲区(repl_backlog_buffer),主节点会记录自己写到的位置,从节点会记录自己读到的位置,一般来说,主节点的偏移量和从节点的偏移量最终会重合,但是随着主节点接受的写操作越多,又或者是从节点由于网络波动重连主节点的时候,这个主节点和从节点的偏移量会拉大,可以根据偏移量的差值去同步差异部分的数据。

但是,当从节点的同步速度跟不上主节点的时候(可能是由于从节点网络断线,或是主节点产生大量写操作),导致环形缓冲区的主节点的偏移量追上了从节点的偏移量,也就是从节点和主节点通讯后发现自己在repl_backlog_buffer中的偏移量没了,这时候从节点就必须执行一次全量同步的命令了。

一般来说,主从节点建立连接后,主节点会给从节点分配一个内存缓冲区(replication_buffer),其实每个客户端连接主节点都会分配一个内存缓冲区,从节点也相当与一个客户端,主节点会将自己接受到的写操作发送到每个从节点的replication_buffer中,最终发送给从节点执行。这个replication_buffer也是有大小限制的,redis配置文件中通过 client-output-buffer-limit 参数限制这个buffer的大小,如果超过限制,主节点会强制断开这个client连接,也就是说从节点处理的慢导致主节点内存buffer的积压达到限制后,主节点会强制断开从节点的连接,此时主从复制会中断,中断后从库再次发起复制请求,恶性循环,导致复制风暴,所以我们要合理调整这个参数。没设置会导致buffer持续增长,甚至产生 OOM。

复制集群主节点挂了怎么办?

因为复制集群强调主节点才能执行写操作,那么,当主节点挂了后,redis服务岂不是无法执行客户端的写命令了,这不符合redis高可用设计,那么redis是如何解决这个问题的呢?

  1. 人工解决,当发现主节点挂了之后,手动将一个从节点设置为主节点,其他节点追随主节点,这样集群就恢复了。当然,其他节点不一定需要全部追随主节点,由于主从复制的原因,主节点向从节点同步数据是会影响一定的性能的,可以将从节点追随健康的从节点,这样能够分摊主节点主从复制的压力。
  2. 哨兵机制,哨兵是一种运行在特殊模式的redis实例,他们的职责主要是监控redis各个节点的健康状态,以及选取主节点,当发现一个节点异常是,会将节点设置为主观下线,哨兵之间也会相互通讯,当认为该节点主观下线的哨兵数量超过我们配置的阈值时,节点会变成客观下线状态,禁用该节点。当主节点客观下线时,哨兵会通过投票选取一个合适的主节点。保证了redis的高可用。

复制集群并没有解决我们的容量问题,因为每个redis实例保存的都是全量数据,如何解决容量问题呢?

要解决容量问题,还是的数据切分。

分片集群

redis 支持分片集群功能,可以将业务数据分布到不同的分片中,简单的说,就是在分片集群下,redis 默认有 16384 个槽位,然后你可以启动一个 redis 集群,然后指定每个节点承担的槽位数量,这样就可以将大量的业务数据拆分到不同的 redis 实例中,然后每个分片集群通过主从复制集和哨兵机制保证高可用。

AKF理论(微服务拆分原则)

  1. X 轴 - 镜像复制(redis 中的主从复制,mysql 中的读写分离都属于镜像复制)

  2. Y 轴 - 业务拆分 (根据业务将不同的数据放到不同的库中)

  3. Z 轴 - 分片存储 (将同一个库下的数据分片存储)

Redis 使用场景

缓存

缓存击穿

问题描述:key过期,大量请求打到这个key上,造成数据库压力过大。

解决方案:先到达的请求先判断缓存有没有值,没有值,执行setnx命令,setnx成功,查询数据库设置缓存,setnx失败,sleep100,重新查询。

缓存穿透

问题描述:大量不存在的key访问缓存系统,导致都请求都打到数据库。

解决方案:布隆过滤器,缓存 null 值

缓存雪崩

问题描述:大量key同时失效,导致请求打到数据库。

解决方案:

随机失效时间或永久保存,一般来说缓存系统存的数据一般都是需要一定的淘汰机制的,

根本方法:处理方案和缓存击穿差不多

数据淘汰策略

默认为no-eviction策略

  • volatile-lru

从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

  • allkeys-lru

从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰

  • volatile-ttl

从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

  • volatile-random

从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

  • allkeys-random

从数据集(server.db[i].dict)中任意选择数据淘汰

  • no-enviction

禁止驱逐数据,永远不过期,仅对写操作返回一个错误,默认为该项

分布式锁

redis实现分布式锁依赖 setnx 命令

  1. 获取锁线程挂了怎么办?

    锁过期释放

  2. 任务没执行完怎么办?

​ 开启一个锁续期的线程,监控任务执行情况,每隔一段时间给锁续期。例如:redisson中的看门狗线程

分类:
后端
标签:
分类:
后端
标签: