Redis的数据结构与应用场景

五种基本数据类型

1、字符串 string:最基本的数据结构,它能存储任何类型的数据,包括二进制数据,序列化后的数据,最大512M。Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID

2、列表 list:简单的字符串列表,按照插入顺序排序,元素可以重复。可以添加一个元素到列表的头部(左边)或者尾部(右边),底层是个链表结构。通过命令的组合,既可以当做栈,也可以当做队列来使用。可以用来缓存类似微信公众号、微博等消息流数据

3、集合 set:无序不可重复集合,可以进行交集、并集、差集操作, 从而可以实现类似——我和某人共同关注的人、朋友圈点赞等功能

4、有序集合 zsetsorted set):和集合 set 一样也是string类型元素的无序不可重复集合。不同的是 zset 的每个元素都会关联一个分数(分数可以重复),redis 通过分数来为集合中的成员进行从小到大的排序。可以用来实现排行榜功能

5、哈希表 hash:可以用来存储一些key-value对,更适合用来存储对象

三种特殊数据类型

JavaGuide

1、位图 Bitmap

应用场景:需要保存状态信息(0/1 即可表示)的场景。举例:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。

2、基数统计 HyperLogLog

应用场景:数量量巨大(百万、千万级别以上)的计数场景。举例:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、

3、地理位置 Geospatial

应用场景:需要管理使用地理空间数据的场景。举例:附近的人。

数据类型 说明
Bitmap 你可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素
的下标叫做 offset(偏移量)。通过 Bitmap, 只需要一个 bit 位来表示某个元素
对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成
一个 byte,所以 Bitmap 本身会极大的节省储存空间。
HyperLogLog Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储
接近2^64个不同元素。不过,HyperLogLog 的计数结果并不是一个精确值,存在
一定的误差(标准误差为 0.81% )。
Geospatial index Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,
基于 Sorted Set 实现。

Redis线程模型:单线程快的原因

Redis 基于 Reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器 file event handler。 这个文件事件处理器,它是单线程的,所以 Redis 才叫做单线程模型。它采用IO多路复用机制来同时监听多个套接字(Socket),根据套接字上的事件类型来关联不同的事件处理器。

当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

文件事件处理器既实现了高性能的网络通信模型,又可以很好地跟内部其他单线程方式运行的模块进行对接,保证了 Redis 内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:多个Socket(客户端连接)、IO多路复用程序(支持多个客户端连接的关键)、文件事件分派器(将 socket 关联到相应的事件处理器)、事件处理器 (连接应答处理器、命令请求处理器、命令回复处理器)。

Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:

  • 文件事件(file event) :用于处理 Redis 服务器和客户端之间的网络 IO。
  • 时间事件(time eveat) :Redis 服务器中的⼀些操作(比如 serverCron 函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的

我们接触最多的还是文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。

多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。 文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。

  1. Redis 启动初始化时,将连接应答处理器跟 AE_READABLE 事件关联。
  2. 若一个客户端发起连接,会产生⼀个 AE_READABLE 事件,然后由连接应答处理器负责和客户端建立连接,创建客户端对应的 socket,同时将这个 socket 的 AE_READABLE 事件和命令请求处理器关联,使得客户端可以向主服务器发送命令请求。
  3. 当客户端向 Redis 发请求时(不管读还是写请求),客户端 socket 都会产生⼀个 AE_READABLE 事件,触发命令请求处理器。处理器读取客户端的命令内容, 然后传给相关程序执行。
  4. 当 Redis 服务器准备好给客户端的响应数据后,会将 socket 的 AE_WRITABLE 事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在 socket 产生⼀个 AE_WRITABLE 事件,由对应命令回复处理器处理,将准备好的响应数据写入 socket,供客户端读取。
  5. 命令回复处理器全部写完到 socket 后,就会删除该 socket 的 AE_WRITABLE 事件和命令回复处理器的映射。

单线程快的原因:

1)纯内存操作,数据结构简单

2)核心是基于非阻塞的IO多路复用机制

3)单线程反而避免了多线程的频繁上下文切换带来的性能问题

过期键的删除策略

过期策略就是指当 Redis 中缓存的 key 过期了,Redis 如何处理。

  • 惰性删除:只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有被再次访问,从而不会被清除,占用大量内存。
  • 定期删除:每隔一定的时间(默认100毫秒),会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是⼀个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

expires 字典(可以看作是 hash 表)会保存所有设置了过期时间的 key 的过期时间数据。

其中,key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。

Redis 中同时使用了惰性过期和定期过期两种过期策略。

仅仅通过给 key 设置过期时间还是有问题的,因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样可能导致大量过期 key 堆积在内存里,然后就 Out of memory 了。这个时候就需要引入 Redis 内存淘汰机制

内存淘汰机制

配置文件中

1
2
3
maxclients 10000  # 最大客户端数量
maxmemory 100mb # 设置最大内存限制为100M
maxmemory-policy noeviction # 内存达到限制值的处理策略

Redis提供的数据淘汰策略

1、volatile-lru:从设置了过期时间的 key 中使用 LRU 算法进行淘汰

2、volatile-ttl:在设置了过期时间的 key 中,根据 key 的过期时间进行淘汰,越早过期的越优先被淘汰

3、volatile-random:从设置了过期时间的 key 中随机淘汰

4、allkeys-lru:从所有 key 中使用 LRU 算法进行淘汰(最常用)

5、allkeys-random:从所有 key 中随机淘汰数据

6、noeviction (默认策略):对于写请求不再提供服务,直接返回错误(DEL 请求和部分特殊请求除外)

当使用 volatile-lru、volatile-ttl、volatile-random 这三种策略时,如果没有 key 可以被淘汰,则和 noeviction 一样返回错误

获取当前内存淘汰策略:config get maxmemory-policy

通过命令修改淘汰策略:config set maxmemory-policy allkeys-lru

4.0 版本后增加以下两种

7、volatile-lfu(least frequently used):从已设置过期时间的数据集中挑选最不经常使用的数据淘汰

8、allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

常见的缓存淘汰算法

FIFO(First In First Out,先进先出)

根据缓存被存储的时间,离当前最远的数据优先被淘汰

LRU(Least Recently Used,最近最少使用)

是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据。

使用 LRU 算法进行内存淘汰的核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

LFU(LeastFrequentlyUsed,最不经常使用)

在一段时间内,缓存数据被使用次数最少的会被淘汰

什么是RDB和AOF

Redis 的持久化机制:RDB 和 AOF

RDB:Redis DataBase,默认的持久化策略,将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘。在指定时间间隔内,redis 服务执行指定次数的写操作,会自动触发一次持久化操作。即在指定目录下生成一个 dump.rdb 文件。Redis 重启会通过加载 dump.rdb 文件来恢复数据。

手动触发:

  • save 命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用

  • bgsave 命令,fork 出⼀个子进程执行持久化,主进程只在 fork 过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了

自动触发:

  • save m n :在 m 秒内,如果有 n 个键发生改变,则自动触发持久化,通过 bgsave 执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置
  • flushall:用于清空 redis 所有的数据库,flushdb 清空当前 redis 所在库数据(默认是0号数据库),会清空 RDB 文件,同时也会生成 dump.rdb,内容为空
  • shutdown:也能触发 RDB 持久化机制
  • 主从同步:全量同步时会自动触发 bgsave 命令,生成 rdb 发送给从节点

优点:

  1. 整个Redis数据库将只包含⼀个文件 dump.rdb,方便持久化。
  2. 容灾性好,方便备份。
  3. 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能。
  4. 相对于大数据集时,比 AOF 的启动效率更高。

缺点:

  1. 数据安全性低。RDB 是间隔⼀段时间进行持久化,如果持久化之间 redis 发生故障,最后一次持久化后的数据可能丢失。所以这种方式更适合数据要求不严谨的时候
  2. 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务较长时间

AOF:Append Only File,以日志的形式记录服务器所处理的每⼀个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文文件看到详细的操作记录。当 Redis 重启时,可以加载 AOF 文件进行数据恢复

优点:

  1. 数据安全,Redis中提供了3种同步策略,即每秒同步(默认)、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是⼀旦系统出现宕机现象,那么这⼀秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中,最多丢一条。不同步:由操作系统控制,可能丢失较多数据
  2. 通过 append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis-check-aof 工具解决数据⼀致性问题。
  3. AOF 机制的 rewrite 模式。定期对 AOF 文件进行重写,以达到压缩的目的

缺点:

  1. AOF 文件比 RDB 文件大,且恢复速度慢。
  2. 数据集大的时候,比 RDB 启动效率低。
  3. 运行效率没有 RDB 高

AOF 重写

可以产生⼀个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态⼀样,但体积更小

在执行 BGREWRITEAOF 命令时,Redis 服务器会维护⼀个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作

AOF 文件比 RDB 更新频率高,优先使用 AOF 还原数据,RDB 性能比 AOF 好, 如果两个都配了优先加载 AOF。

简述Redis事务实现

1、事务开始

MULTI 命令的执行,标识着⼀个事务的开始。MULTI 命令会将客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成。

2、命令入队

当⼀个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为 MULTIEXECDISCARDWATCH 中的一个,立即执行这个命令,否则将命令放入⼀个事务队列里面,然后向客户端返回 QUEUED 回复

  • 如果客户端发送的命令为 MULTIEXECDISCARDWATCH 四个命令的其中⼀个,那么服务器立即执行这个命令。
  • 如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。 首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复

事务队列是按照 FIFO 的方式保存入队的命令

3、事务执行

客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。

  • 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。
  • 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端

Redis 不支持事务回滚机制,但是它会检查每⼀个事务中的命令是否错误。

Redis 事务不支持检查那些程序自己的逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型的操作!

  • MULTI 命令用于开启⼀个事务,它总是返回 OK。MULTI 执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当 EXEC 命令被调用时,所有队列中的命令才会被执行。

  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。

  • 通过调用 DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。

  • WATCH 命令是⼀个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到 EXEC 命令。

  • UNWATCH 命令可以取消 watch 对所有 key 的监控。

主从复制的流程

1、集群启动时,主从库间会先建立连接,为全量复制做准备

2、主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照 RDB

3、在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成之后收到的所有写操作。

4、最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replocation buffer 中修改操作发送给从库,从库再执行这些操作。这样⼀来,主从库就实现同步了

5、后续主库和从库都可以处理客户端读操作,但写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步

主从复制的核心原理

通过执行 slaveof 命令或设置 slaveof 选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而⼀个从数据库只能拥有一个主数据库。

全量复制:

  1. 主节点通过 bgsave 命令 fork 子进程进行 RDB 持久化,该过程是非常消耗 CPU、内存(页表复制)、硬盘IO的
  2. 主节点通过网络将 RDB 文件发送给从节点,对主从节点的带宽都会带来很大的消耗
  3. 从节点清空老数据、载入新 RDB 文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行 bgrewriteaof,也会带来额外的消耗

部分复制:

  1. 复制偏移量:执行复制的双方,主从节点,分别会维护一个复制偏移量 offset
  2. 复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出(FIFO)队列作为复制积压缓冲区, 当主从节点 offset 的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
  3. 服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。 从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度:
    • 如果从节点保存的 runid 与主节点现在的 runid 相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看 offset 和复制积压缓冲区的情况);
    • 如果从节点保存的 runid 与主节点现在的 runid 不同,说明从节点在断线前同步的 Redis 节点并不是当前的主节点,只能进行全量复制。

集群策略

Redis提供了三种集群策略:

  1. 主从模式:这种模式比较简单,主库可以读写,并且会和从库进行数据同步。这种模式下,客户端直接连主库或某个从库,但是当主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据受到某台机器的内存容量限制,所以不可能支持特大数据量
  2. 哨兵模式:这种模式在主从的基础上新增了哨兵节点。当主库节点宕机后,哨兵会首先发现,然后在从库中选择⼀个库作为新的主库,另外哨兵也可以做集群,从而可以保证当某⼀个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证Redis 集群的高可用, 但是仍然不能很好的解决 Redis 的容量上限问题。
  3. Cluster 模式:Cluster 模式是用得比较多的模式,它支持多主多从,这种模式会按照 key 进行槽位的分配,可以使得不同的 key 分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。

对于这三种模式,如果 Redis 要存的数据量不大,可以选择哨兵模式;如果 Redis 要存的数据量大,并且需要持续的扩容,那么选择Cluster 模式。

哨兵机制(sentinel)的原理

Redis 哨兵机制是通过在独立的哨兵节点上运行特定的哨兵进程来实现的。这些哨兵进程监控主从节点的状态,并在发现故障时自动完成故障发现和转移,并通知应用方,实现高可用性。

以下是哨兵机制的工作原理

a. 哨兵选举:在启动时,每个哨兵节点会执行选举过程,其中一个哨兵节点被选为领导者(leader),负责协调其他哨兵节点。

选举过程

  • 每个在线的哨兵节点都可以成为领导者,每个哨兵节点会向其它哨兵发送 is-master-down-by-addr 命令,征求判断并要求将自己设置为领导者;
  • 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
  • 如果哨兵发现自己在选举的票数大于等于 num(sentinels)/2+1 时,将成为领导者,如果没有超过,继续选举。

b. 监控主从节点:哨兵节点通过发送命令周期性地检查主从节点的健康状态,包括主节点是否在线、从节点是否同步等。如果哨兵节点发现主节点不可用,它会触发一次故障转移。

c. 故障转移:一旦主节点被判定为不可用,哨兵节点会执行故障转移操作。它会从当前的从节点中选出一个新的主节点,并将其他从节点切换到新的主节点。这样,系统可以继续提供服务而无需人工介入。

故障转移过程

  • 由 Sentinel 节点定期监控发现主节点是否出现了故障:sentinel 会向 master 发送心跳 PING 来确认 master 是否存活,如果 master 在“一定时间范围”内不回应 PONG 或者是回复了一个错误消息,那么这个 sentinel 会主观地(单方面地)认为这个 master 已经不可用了。
  • 确认主节点:
    • 过滤掉不健康的(下线或断线),没有回复过哨兵 ping 响应的从节点
    • 选择从节点优先级最高的
    • 选择复制偏移量最大,此指复制最完整的从节点

当主节点出现故障, 由领导者负责处理主节点的故障转移。

d. 客户端重定向:哨兵节点会通知客户端新的主节点的位置,使其能够与新的主节点建立连接并发送请求。这确保了客户端可以无缝切换到新的主节点,继续进行操作。
此外,哨兵节点还负责监控从节点的状态。如果从节点出现故障,哨兵节点可以将其下线,并在从节点恢复正常后重新将其加入集群。

客观下线

当主观下线的节点是主节点时,此时哨兵节点会通过指令 sentinel is-masterdown-by-addr 寻求其它哨兵节点对主节点的判断,当超过 quorum(选举)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就是客观下线。

redis 哨兵的作用:

  • 监控主数据库和从数据库是否正常运行。
  • 主数据库出现故障时,可以自动将从数据库转换为主数据库,实现自动切换。

Redis分布式锁底层是如何实现的

1、首先利用 setnx 来保证:如果 key 不存在才能获取到锁,如果 key 存在,则获取不到锁

2、然后还要利用 lua 脚本来保证多个 redis 操作的原子性

3、同时还要考虑到锁过期,所以需要额外的⼀个看门狗定时任务来监听锁是否需要续约

4、同时还要考虑到 redis 节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个 redis 节点挂掉了,锁也不能被其他客户端获取到

性能优化

Redis bigkey

简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。
有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。

如何发现 bigkey?

1、使用 Redis 自带的 --bigkeys 参数来查找

这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有⼀点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 string 数据类型,包含元素最多的复合数据类型)

2、分析 RDB 文件

借助网上现成的代码/工具

大量key集中过期

定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢

如何解决

1、给 key 设置随机过期时间。

2、开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

缓存预热、缓存击穿、缓存雪崩、缓存穿透

缓存预热:当系统上线后,提前将相关的缓存数据直接加载到缓存系统。用户直接查询事先被预热的缓存数据!

缓存中存放的大多都是热点数据,目的就是保证请求可以直接从缓存中获取到数据,而不用访问数据库。

缓存击穿:相较于缓存穿透,缓存击穿的目的性更强,一个存在的 key,在缓存过期的一刻同时有大量的请求,这些请求都会击穿到DB,造成瞬时 DB 请求量大、压力骤增。这就是缓存被击穿,只是针对其中某个 key 的缓存不可用而导致击穿,但是其他的 key 依然可以使用缓存响应。比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。解决方案有

  • 设置热点数据永不过期

    但是当 Redis 内存空间满的时候也会清理部分数据,而且此种方案会占用空间,一旦热点数据多了起来,就会占用部分空间。

  • 加互斥锁(分布式锁)

    在访问 key 之前,采用 SETNX(set if not exists)来设置另一个短期 key 来锁住当前 key 的访问,访问结束再删除该短期 key。保证同时刻只有一个线程访问。这样对锁的要求就十分高。

缓存雪崩:如果缓存中某一时刻大批热点数据同时过期,造成瞬时 DB 请求量大、压力骤增,引起雪崩。解决办法有

  • 搭建⼀个高可用的 Redis 集群
  • 合理的过期时间。为缓存的过期时间引入随机值,分散缓存过期时间,避免大规模同时失效。或者是粗暴的设置热点数据永不过期
  • 多级缓存。使用多级缓存架构,如本地缓存 + 分布式缓存,提高系统的容错能力
  • 限流降级。在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
  • 数据预热。在正式部署之前,把可能的数据预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的 key,并设置不同的过期时间,让缓存失效的时间点尽量均匀。

缓存穿透:在默认情况下用户请求数据时,会先在缓存(Redis)中查找,若没找到即缓存未命中,再在数据库中进行查找。数量少可能问题不大,可是一旦大量的请求数据(例如秒杀场景)都没有命中缓存的话,就会全部转移到数据库上,造成数据库极大的压力,就有可能导致数据库崩溃。网络安全中也有人恶意使用这种手段进行攻击,被称为洪水攻击。解决方案有 布隆过滤器

  • 使用布隆过滤器。对所有可能查询的参数以 Hash 的形式存储,以便快速确定是否存在这个值,在控制层先进行拦截校验,校验不通过直接打回,减轻了存储系统的压力。如果它认为⼀个 key 不存在,那么这个 key 就肯定不存在

  • 缓存空对象。一次请求若在缓存和数据库中都没找到,就在缓存中放一个空对象用于处理后续这个请求。

    这样做有一个缺陷:存储空对象也需要空间,大量的空对象会耗费一定的空间,存储效率并不高。解决这个缺陷的方式就是设置较短过期时间。即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

  • 参数校验。在接收到请求之前进行参数校验,判断请求参数是否合法。

Redis和数据库如何保证数据一致

语雀
  • 先更新数据库,再删除 Redis。如果删除 Redis 失败,会导致数据不一致

  • 先删除 Redis 缓存数据,再更新数据库(日常开发中都会选这种)。再次查询的时候会将数据添加到缓存中,这种方案能解决上面的问题,但是在高并发下性能较低,而且仍然会出现数据不一致的问题,比如线程1删除了 Redis 缓存数据,正在更新数据库(出现网络延迟),此时出现另外一个查询,那么就会把数据库中的老数据又缓存到 Redis 中

    因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题

    • 延时双删。步骤是:先删除 Redis 缓存数据,再更新数据库,延迟几百毫秒再删除 Redis 缓存数据。这样就算在更新数据库时,有其他线程读了数据库,把老数据缓存到了 Redis 中,那么也会被删除掉,从而把数据保持一致

    • 队列+重试机制

    • 异步更新缓存(基于订阅binlog的同步机制)。

      使用阿里的一款开源框架 canal,通过该框架可以对 MySQL 的 binlog 进行订阅,而 canal 正是模仿了 mysql 的 slave 数据库的备份请求,使得 Redis 的数据更新达到了相同的效果。