前言

Redis是一种开源的内存数据结构存储系统,以其高速读写性能和多种数据结构支持而备受开发者青睐。作为一种NoSQL数据库,Redis不仅能用作数据库,还可用于缓存、消息队列等多种场景。本文将介绍Redis的核心概念、数据结构、高可用架构及实际应用实践,帮助读者全面了解和有效使用Redis。

常规问题

什么是 redis,为什么要使用它

Redis,Remote Dictionary Server(远程数据服务)是一种支持 key-value 等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
使用它的原因

  • 读写性能优异:Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s
  • 数据类型丰富:Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作
  • 原子性:Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作全并后的原子性执行
  • 丰富的特性:Redis 支持 publish/subscribe, 通知, key 过期等特性
  • 持久化:Redis 支持 RDB, AOF 等持久化方式
  • 发布订阅:Redis 支持发布/订阅模式
  • 分布式:Redis Cluster

redis 一般有哪些使用场景

  • 热点数据缓存
  • 限时业务使用(如手机验证码)
  • 计时器(秒杀、库存)
  • 分布式锁
  • 延时操作(延时队列)
  • 排行榜
  • 点赞、好友互相关系的存储
  • 简单队列

redis 为什么快

  • 内存存储:Redis 将所有数据存储在内存中,而不是磁盘上。内存的读写速度远快于磁盘,这使得 Redis 可以非常快速地处理大量的数据请求。
  • 单线程架构:Redis 采用单线程的事件循环机制来处理请求。这样可以避免多线程编程中的锁竞争问题,减少了上下文切换的开销,从而提高了处理效率。
  • 高效的数据结构:Redis 支持多种高效的数据结构,如字符串、哈希表、列表、集合、有序集合等。这些数据结构经过精心设计,能够在内存中高效地存储和检索数据。
  • I/O 多路复用:Redis 使用 I/O 多路复用技术(如 epoll、kqueue),可以同时处理大量客户端连接,而不会因等待 I/O 操作而阻塞。
  • 优化的序列化协议:Redis 使用自定义的 RESP(REdis Serialization Protocol)协议进行数据通信,这种协议简单高效,能快速地进行数据的序列化和反序列化。
  • 内存优化:Redis 在内存管理方面进行了大量优化,包括内存分配策略、内存压缩等,以尽可能减少内存使用量,提高内存访问速度。
  • 持久化和复制:虽然 Redis 的数据主要存储在内存中,但它也支持持久化(如 RDB 和 AOF)和复制(主从复制)功能。这些功能在不影响内存性能的前提下,保证了数据的持久性和高可用性。
  • 社区和生态系统:Redis 拥有一个活跃的社区和丰富的生态系统,各种工具和扩展使其在各种应用场景中都能保持高性能。

数据类型和数据结构

redis 有哪些数据类型

5 种基础类型

  • String 字符串:缓存、计数器、session
  • List 列表:TimeLine、消息队列
  • Set 集合:标签、点赞等
  • Hash 散列:缓存
  • Zset 有序集合:排行榜

3 种特殊类型

  • HyperLogLogs(基数统计):统计各种计数,如注册 IP 数
  • Bitmap(位存储):统计活跃不活跃等2 个状态的信息
  • geospatial(地理位置):推算两地之间的距离, 方圆几里的人

redis 数据类型有哪些命令

String

List

Set

Hash

Zset

谈谈 redis 的对象机制(redisObject)

  • type:对象所保存的值的类型
  • encoding:对象所保存的值的编码
  • ptr:实际保存值的数据结构
  • lru:记录了对象最后一次被命令程序访问的时间
  • refcount:对象的引用计数,显然计数 0 就是可以回收

redis 会把一些常用的值放到一个共享对象中,避免了重复分配的麻烦,也节约一些 CPU 时间

redis 数据类型有哪些底层数据结构

SDS:用于存储二进制数据的一种结构, 具有动态扩容的特点


  • len 保存了 SDS 保存字符串的长度
  • buf[] 数组用来保存字符串的每个元素
  • alloc 分别以 uint8、uint16、unit32、unit64 表示整个 SDS,除过头部与末尾的\0,剩余的字节数
  • flags 始终为一字节,以低 3 位标示着头部的类型,高 5 位未使用

QuickList:以 ziplist 为结点的双端链表结构,宏观上,quicklist 是一个链表;微观上,链表中的每个结点都是一个 ziplist

ZipList:是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储

  • zlbytes 字段的类型是 uint32_t,这个字段中存储的是整个 ziplist 所占用的内存的字节数
  • zltail 自动的类型是 uint32_t,它指的是 ziplist 中最后一个 entry 的偏移量,用于快速定位最后一个 entry,以快速完成 pop 等操作
  • zllen 字段的类型是 unit16_t,它指的是整个 ziplist 中 entry 的数量,这个值只占 2bytes(16 位);如果 ziplist 中 entry 的数目小于 65535(2 的 16 次方),name 该字段中存储的就是实际 entry 的值,若等于或超过 65535,name 该字段的值固定位 65535,但实际数量需要一个个 entry 去遍历所有 entry 才能得到
  • zlend 是一个终止字节,其值全为 F,即 0xFF,ziplist 保证任何情况下,一个 entry 的首字节都不是 255

HashTable

IntSet:当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现

  • encoding 表示编码方式,取值有 3 个:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64
  • length 表示其中存储的整数的个数
  • contents 指向实际存储数值的连续内存区域, 就是一个数组;整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值得大小从小到大有序排序,且数组中不包含任何重复项

ZSkipList:跳表,作为 Zset 的底层存储,利用空间换时间的数据结构

  • 头节点不持有任何数据,其 level[] 的长度为 32
  • 每个节点
  • ele 字段,持有数据,是 sds 类型
  • score 字段,标示结点的得分,结点之间凭借得分来判断先后顺序,跳表中的结点按结点的得分升序排列
  • backward 指针,指向结点的前一个紧邻结点
  • level 字段用以记录所有结点(除头节点外)
  • forward 字段指向比自己得分高的某个节点
  • span 字段代表 forward 字段指向的节点,距离当前节点的距离,紧邻的两个节点之间的距离定义为 1

为什么要设计 sds?

  • 常数复杂度获取字符串长度:由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。
  • 杜绝缓冲区溢出:我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。
  • 减少修改字符串的内存重新分配次数:C 语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
  • 二进制安全:因为 C 字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此 C 字符串无法正确存取;而所有 SDS 的 API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。
  • 兼容部分 C 字符串函数:虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库 <string.h> 中的一部分函数。

一个字符串类型的值能存储最大容量是多少?

512M

为什么会设计 Stream

  • 传统的 PUB/SUB 模式无法持久化数据
  • 基于 List 的 LPUSH+BPOP 实现了持久化但不支持多播和分组消费

Stream 用在什么样场景

  • 实时通信
  • 大数据分析
  • 异地数据备份等

消息 ID 的设计是否考虑了时间回拨的问题

  • 1553439850328-0,Redis 生成的消息 ID,由两部分组成:时间戳 - 序号
  • Redis 的每个 Stream 类型数据都维护一个 latest_generated_id 属性,用于记录最后一个消息的 ID。若发现当前时间戳退后(小于 latest_generated_id 所记录的),则采用时间戳不变而序号递增的方案来作为新消息 ID

持久化和内存

Redis 的持久化机制是什么?各自的优缺点?一般怎么用?

RDB

  • RDB 持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。
  • 分为手动触发(save 和 bgsave)和自动触发(redis.conf 配置的 save m n、主从复制、执行 debug reload、默认情况下执行 shutdown)。
  • 优点
    • RDB 文件是某个时间节点的快照,默认使用 LZF 算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;
    • Redis 加载 RDB 文件恢复数据要远远快于 AOF 方式;
  • 缺点
    • RDB 方式实时性不够,无法做到秒级的持久化;
    • 每次调用 bgsave 都需要 fork 子进程,fork 子进程属于重量级操作,频繁执行成本较高;
    • RDB 文件是二进制的,没有可读性,AOF 文件在了解其结构的情况下可以手动修改或者补全;
    • 版本兼容 RDB 文件问题;

AOF

  • AOF 日志采用写后日志,即先写内存,后写日志
  • 需要到 redis.conf 中开启 AOF 持久化

Redis 过期键的删除策略有哪些

  • 不淘汰
    • noeviction (v4.0 后默认的)
  • 对设置了过期时间的数据中进行淘汰
    • 随机:volatile-random
    • ttl:volatile-ttl
    • lru:volatile-lru
    • lfu:volatile-lfu
  • 全部数据进行淘汰
    • 随机:allkeys-random
    • lru:allkeys-lru
    • lfu:allkeys-lfu

Redis 内存淘汰算法有哪些

  • LRU:Redis 会记录每个数据的最近一次被访问的时间戳。在 Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让 Redis 不用维护一个巨大的链表,也不用操作链表,进而提升性能
  • LFU:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。

Redis 的内存用完了会发生什么?

如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。

Redis 如何做内存优化?

  • 避免存储 bigkey,降低释放内存的耗时
  • 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)
  • 拆分实例,把淘汰 key 的压力分摊到多个实例上
  • 如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)

Redis key 的过期时间和永久有效分别怎么设置?

  • EXPIRE 和 PERSIST 命令

Redis 中的管道有什么用?

  • 一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
  • 这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。

事务

什么是 redis 事务

redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis 事务相关命令

  • MULTI :开启事务,redis 会将后续的命令逐个放入队列中,然后使用 EXEC 命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个 key,如果事务在执行前,这个 key(或多个 key) 被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消 WATCH 对所有 key 的监视。

Redis 事务的三个阶段

  • 开启:以 MULTI 开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  • 执行:由 EXEC 命令触发事务

watch 是如何监视实现的呢

  • Redis 使用 WATCH 命令来决定事务是继续执行还是回滚,那就需要在 MULTI 之前使用 WATCH 来监控某些键值对,然后使用 MULTI 命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。
  • 当使用 EXEC 执行事务时,首先会比对 WATCH 所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis 都会取消执行事务前的 WATCH 命令。

为什么 Redis 不支持回滚

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

Redis 对 ACID 的支持性理解

  • 原子性 atomicity:运行期的错误是不会回滚的,很多文章由此说 Redis 事务违背原子性的;而官方文档认为是遵从原子性的。Redis 官方文档给的理解是,Redis 的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。
  • 一致性 consistency:redis 事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非 redis 进程意外终结。
  • 隔离性 Isolation:redis 事务是严格遵守隔离性的,原因是 redis 是单进程单线程模式 (v6.0 之前),可以保证命令执行过程中不会被其他客户端命令打断。但是,Redis 不像其它结构化数据库有隔离级别这种设计。
  • 持久性 Durabilityredis 事务是不保证持久性的,这是因为 redis 持久化策略中不管是 RDB 还是 AOF 都是异步执行的,不保证持久性是出于对性能的考虑。

Redis 事务其他实现

  • 基于 Lua 脚本,Redis 可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

集群

主从复制

Redis 集群的主从复制模型是怎样的?

全量复制的三个阶段?

为什么会设计增量复制?

  • 如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

增量复制的流程? 如果在网络断开期间,repl_backlog_size 环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?

  1. 一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer 的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。
  2. 每个从库会记录自己的 slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过 psync 命令把自己记录的 slave_repl_offset 发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。

为什么不持久化的主服务器自动重启非常危险呢?

  • 我们设置节点 A 为主服务器,关闭持久化,节点 B 和 C 从节点 A 复制数据。
  • 这时出现了一个崩溃,但 Redis 具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。
  • 节点 B 和 C 从节点 A 进行复制,现在节点 A 是空的,所以节点 B 和 C 上的复制数据也会被删除。
  • 当在高可用系统中使用 Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于 Sentinel 都无法检测到这次失败,那么上面说的这种失败的情况就发生了。

为什么主从全量复制使用 RDB 而不使用 AOF?

  • RDB 文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而 AOF 文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个 key 的多次冗余操作。在主从全量数据同步时,传输 RDB 文件可以尽量降低对主库机器网络带宽的消耗,从库在加载 RDB 文件时,一是文件小,读取整个文件的速度会很快,二是因为 RDB 文件存储的都是二进制数据,从库直接按照 RDB 协议解析还原数据即可,速度会非常快,而 AOF 需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比 RDB 会慢得多,所以使用 RDB 进行主从全量复制的成本最低。
  • 假设要使用 AOF 做全量复制,意味着必须打开 AOF 功能,打开 AOF 就要选择文件刷盘的策略,选择不当会严重影响 Redis 性能。而 RDB 只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启 AOF 的。

为什么还有无磁盘复制模式?

  • Redis 默认是磁盘复制,但是如果使用比较低速的磁盘,这种操作会给主服务器带来较大的压力。Redis 从 2.8.18 版本开始尝试支持无磁盘的复制。使用这种设置时,子进程直接将 RDB 通过网络发送给从服务器,不使用磁盘作为中间存储。

为什么还会有从库的从库的设计?


将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上

哨兵机制

Redis 哨兵机制?哨兵实现了什么功能呢?

哨兵的核心功能是主节点的自动故障转移。

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

哨兵集群是通过什么方式组建的?

哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。

哨兵是如何监控 Redis 集群的?

由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。

哨兵如何判断主库已经下线了呢?

  • 主观下线:任何一个哨兵都是可以监控探测,并作出 Redis 节点下线的判断;
  • 客观下线:有哨兵集群共同决定 Redis 节点是否下线;

    如果赞成票数(这里是 2)是大于等于哨兵配置文件中的 quorum 配置项(比如这里如果是 quorum=2), 则可以判定主库客观下线了。

哨兵的选举机制是什么样的?

哨兵的选举机制其实很简单,就是一个 Raft 选举算法: 选举的票数大于等于 num(sentinels)/2+1 时,将成为领导者,如果没有超过,继续选举

  • 任何一个想成为 Leader 的哨兵,要满足两个条件
    • 第一,拿到半数以上的赞成票;
    • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

Redis 1 主 4 从,5 个哨兵,哨兵配置 quorum 为 2,如果 3 个哨兵故障,当主库宕机时,哨兵能否判断主库 " 客观下线 "?能否自动切换?

  1. 哨兵集群可以判定主库 " 主观下线 "。由于 quorum=2,所以当一个哨兵判断主库 " 主观下线 " 后,询问另外一个哨兵后也会得到同样的结果,2 个哨兵都判定 " 主观下线 ",达到了 quorum 的值,因此,哨兵集群可以判定主库为 " 客观下线 "
  2. 但哨兵不能完成主从切换。哨兵标记主库 " 客观下线后 ",在选举 " 哨兵领导者 " 时,一个哨兵必须拿到超过多数的选票 (5/2+1=3 票)。但目前只有 2 个哨兵活着,无论怎么投票,一个哨兵最多只能拿到 2 票,永远无法达到 N/2+1 选票的结果。

主库判定客观下线了,那么如何从剩余的从库中选择一个新的主库呢?

新的主库选择出来后,如何进行故障的转移?

转移之前

故障转移

转移之后

Redis 集群

说说 Redis 哈希槽的概念?为什么是 16384 个?

  • Redis-cluster 没有使用一致性 hash,而是引入了哈希槽的概念。Redis-cluster 中有 16384(即 2 的 14 次方)个哈希槽,每个 key 通过 CRC16 校验后对 16383 取模来决定放置哪个槽。Cluster 中的每个节点负责一部分 hash 槽(hash slot)。
    为什么是 16384(2 的 14 次方个)
  • 在 redis 节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用 char 进行 bitmap 压缩后是 2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用 2k 的空间创建了 16k 的槽数。
  • 虽然使用 CRC16 算法最多可以分配 65535(2^16-1)个槽位,65535=65k,压缩后就是 8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要 8k 的心跳包,作者认为这样做不太值得;并且一般情况下一个 redis 集群不会有超过 1000 个 master 节点,所以 16k 的槽位是个比较合适的选择。

Redis 集群会有写操作丢失吗?为什么?

Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

应用场景

redis 客户端有哪些

Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。
Redisson 是一个高级的分布式协调 Redis 客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象 (Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。

Redis 如何做大量数据插入?

Redis2.6 开始 redis-cli 支持一种新的被称之为 pipe mode 的新模式用于执行大量数据插入工作。

redis 实现分布式锁实现? 什么是 RedLock?

redis 缓存有哪些问题,如何解决

缓存穿透

  • 问题来源
    缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
  • 解决方案
  1. 接口层增加校验,如用户鉴权校验,id 做基础校验,id<=0 的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null,缓存有效时间可以设置短点,如 30 秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个 id 暴力攻击
  3. 布隆过滤器。bloomfilter 就类似于一个 hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个 key 是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于 hash 算法和容器大小,

缓存击穿

  • 问题来源
    缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
  • 解决方案
  1. 设置热点数据永远不过期。
  2. 接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。
  3. 加互斥锁

缓存雪崩

  • 问题来源
    缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
  • 解决方案
  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
  3. 设置热点数据永远不过期。

缓存污染(或满了)

  • 问题来源
    缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。
    缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响 Redis 性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。
  • 解决方案
  1. 设置数据过期
  2. 把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销
  3. 缓存淘汰策略

redis 和其它数据库一致性问题如何解决

redis 性能问题有哪些,如何分析定位解决

新版本

Redis 单线程模型? 在 6.0 之前如何提高多核 CPU 的利用率?

  • 可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个 CPU,可以考虑一下分片(shard)。

6.0 版本中多线程

  • Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
  • Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改 redis.conf 配置文件:io-threads-do-reads yes。开启多线程后,还需要设置线程数,否则是不生效的。修改 redis.conf 配置文件:io-threads ,关于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6 个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。

总结

Redis凭借其多样的数据结构、高性能、高可用性和灵活的应用场景,在现代应用开发中扮演着重要角色。通过本文的介绍,读者能够掌握Redis的核心概念和最佳实践,充分发挥其在缓存优化、实时数据处理和分布式系统中的优势。

参考链接

  1. Redis知识体系详解
  2. Redis官方文档
  3. Redis中文文档