为什么Redis快

Redis 内部做了非常多的性能优化,比较重要的有下面 4 点:

  1. 纯内存操作 (Memory-Based Storage) :这是最主要的原因。Redis 数据读写操作都发生在内存中,访问速度是纳秒级别,而传统数据库频繁读写磁盘的速度是毫秒级别,两者相差数个数量级。

  2. 高效的 I/O 模型 (I/O Multiplexing & Single-Threaded Event Loop) :Redis 使用单线程事件循环配合 I/O 多路复用技术,让单个线程可以同时处理多个网络连接上的 I/O 事件(如读写),避免了多线程模型中的上下文切换和锁竞争问题。虽然是单线程,但结合内存操作的高效性和 I/O 多路复用,使得 Redis 能轻松处理大量并发请求(Redis 线程模型会在后文中详细介绍到)。

  3. 优化的内部数据结构 (Optimized Data Structures) :Redis 提供多种数据类型(如 String, List, Hash, Set, Sorted Set 等),其内部实现采用高度优化的编码方式(如 ziplist, quicklist, skiplist, hashtable 等)。Redis 会根据数据大小和类型动态选择最合适的内部编码,以在性能和空间效率之间取得最佳平衡。

  4. 简洁高效的通信协议 (Simple Protocol - RESP) :Redis 使用的是自己设计的 RESP (REdis Serialization Protocol) 协议。这个协议实现简单、解析性能好,并且是二进制安全的。客户端和服务端之间通信的序列化/反序列化开销很小,有助于提升整体的交互速度。

与本地缓存比较

特性

本地缓存

Redis

数据一致性

多服务器部署时存在数据不一致问题

数据一致

内存限制

受限于单台服务器内存

独立部署,内存空间更大

数据丢失风险

服务器宕机数据丢失

可持久化,数据不易丢失

管理维护

分散,管理不便

集中管理,提供丰富的管理工具

功能丰富性

功能有限,通常只提供简单的键值对存储

功能丰富,支持多种数据结构和功能

持久化机制

Redis支持 3 种持久化方式:

  • RDB:快照(snapshotting)

  • AOF:只追加文件(append-only file)

  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

RDB

RDB就是一种数据副本文件。

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 同步保存操作,会阻塞 Redis 主线程;

  • bgsave : fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。

AOF

AOF就是保存每条更改数据的指令操作

开启 AOF 持久化后,每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 系统内核缓存区,最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

三种写盘策略:

  • appendfsync always:执行写操作后,会立刻调用 fsync 函数同步 AOF 文件(刷盘)。主线程会阻塞,直到 fsync 将数据完全刷到磁盘后才会返回。这种方式数据最安全,理论上不会有任何数据丢失。但因为每个写操作都会同步阻塞主线程,所以性能极差。

  • appendfsync everysec:执行写操作后立即返回,由后台线程( aof_fsync 线程)每秒钟调用 fsync 函数(系统调用)同步一次 AOF 文件(write+fsyncfsync间隔为 1 秒)。性能基本不受影响。在性能和数据安全之间做出了绝佳的平衡。不过,在 Redis 异常宕机时,最多可能丢失最近 1 秒内的数据。

  • appendfsync no:让操作系统决定何时进行同步。 性能最好,避免了 fsync 的阻塞。但数据安全性最差,宕机时丢失的数据量不可控,取决于操作系统上一次同步的时间点。

两种方案比较

RDB 优点

  • 存储占用小:RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。

  • 恢复速度快:使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

AOF 优点

  • 安全性好:AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。

  • 可读性好:AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

综上

  • Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。

  • 不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。

  • 如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化

生产问题

缓存穿透

大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中。导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接导致宕机。

解决方案:

  1. 缓存无效的key:将不存在的key缓存null值在redis,避免每次都去数据库查询。但也可能会导致redis缓存大量无效key。

  2. 布隆过滤器:由位数组一系列哈希函数组成。如果布隆过滤器未命中,则一定是无效查询,直接返回错误信息。存在则有可能是有效查询。

  3. 接口限流:根据用户或者IP进行限流,禁止异常频繁的访问。(对击穿和雪崩问题也有用)

缓存击穿

请求的 key 对应的是 热点数据,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接导致宕机。

解决方案:

  1. 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长。

  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。

  3. 互斥锁查询(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存,避免同时大量请求都去数据库查询。

  4. 逻辑过期:设置逻辑过期时间,不真正删除缓存。如果缓存过期,则使用互斥锁查询数据库更新缓存,竞争锁失败则直接返回逻辑缓存数据

缓存雪崩

缓存在同一时间大面积的失效, 好比雪崩一样,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力,可能直接导致宕机。

解决方案:

  1. 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险。

  2. 提前预热(推荐):同上。

  3. 永不过期(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。

  4. 逻辑过期:同上。

缓存预热实现

常见的缓存预热方式有两种:

  1. 使用定时任务,比如 xxl-job,来定时触发缓存预热的逻辑,将数据库中的热点数据查询出来并存入缓存中。

  2. 使用消息队列,比如 Kafka,来异步地进行缓存预热,将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务消费消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存。

一致性问题

采用旁路缓存模式(Cache Aside Pattern)解决:

  • 读操作

    1. 先尝试从缓存读取数据。

    2. 如果缓存命中,直接返回数据。

    3. 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。

  • 写操作

    1. 先更新数据库。

    2. 再直接删除缓存中对应的数据。

关键是写操作要先更新数据库,再删缓存

如果第二步删除缓存失败,则应该采用 重试 机制,直至成功为止。

重试:

重试面临可能会再次失败、重试次数多少次合理、重试会占用线程资源等问题。所以应该采用异步重试,可以把重试请求写到 消息队列 中,然后由专门的消费者来重试,直到成功。或者 订阅变更日志 ,如MySQL的binlog,根据操作的数据删除对应的缓存。

读写分离 + 主从库延迟 问题:

  1. 线程 A 更新主库 X = 2(原值 X = 1)

  2. 线程 A 删除缓存

  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)

  4. 从库「同步」完成(主从库 X = 2)

  5. 线程 B 将「旧值」写入缓存(X = 1)

最终 X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。

采用延迟双删策略:线程 A 可以生成一条「延时消息」,写到消息队列中,消费者延时「删除」缓存。

但问题来了,这个「延迟删除」缓存,延迟时间到底设置要多久呢?

  • 问题1:延迟时间要大于「主从复制」的延迟时间

  • 问题2:延迟时间要大于线程 B 读取数据库 + 写入缓存的时间

但是,这个时间在分布式和高并发场景下,其实是很难评估的很多时候,我们都是凭借经验大致估算这个延迟时间,例如延迟 1-5s,只能尽可能地降低不一致的概率。

结论:

推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。

参考

https://mp.weixin.qq.com/s?__biz=MzIyOTYxNDI5OA==&mid=2247487312&idx=1&sn=fa19566f5729d6598155b5c676eee62d&chksm=e8beb8e5dfc931f3e35655da9da0b61c79f2843101c130cf38996446975014f958a6481aacf1&scene=178&cur_album_id=1699766580538032128#rd

我自天空坠落