1. Redis介绍
Redis是一种基于内存的键值存储系统,它支持多种数据结构,Redis具有以下特点:
- 快速:Redis是一种基于内存的存储系统,因此具有非常快的读写速度
- 持久化:Redis支持数据持久化,可以将数据保存到磁盘上,以便在服务器重启后恢复数据
- 支持多种数据结构:Redis支持多种数据结构,例如字符串、哈希表、列表、集合、有序集合等,可以满足不同的数据存储需求
- 分布式:Redis支持分布式存储,可以将数据分布在多个节点上,以提高数据处理能力和可用性
- 事务:Redis支持事务,可以将多个操作封装在一个事务中进行,以保证数据的一致性
1.1. Redis的使用场景
Redis适用于需要快速读写、高并发、多种数据结构的场景,例如缓存、计数器、消息队列等。在后端开发中,Redis通常用于以下场景:
- 缓存:将热点数据存储在Redis中,以提高系统的读写性能。例如,将数据库查询结果、API响应结果、页面片段等存储在Redis中,以减少对数据库和API的访问。
- 分布式锁:基于 Redisson实现分布式锁,以保证多个进程或线程之间的数据一致性。例如,在分布式系统中,多个进程或线程需要对同一个资源进行操作时,可以使用Redis实现分布式锁,以保证只有一个进程或线程可以访问该资源。
- 计数器:使用Redis实现计数器,以统计某个事件发生的次数。例如,统计网站的PV(Page View)、UV(Unique Visitor)、注册用户数等。
- 消息队列:使用Redis实现消息队列,以实现异步处理任务。例如,将用户提交的任务放入Redis队列中,由后台进程或线程进行处理。
- 地理位置:使用Redis实现地理位置查询,以实现附近的人、附近的店铺等功能。例如,将用户的地理位置存储在Redis中,使用Redis提供的地理位置查询功能,查询附近的人、附近的店铺等。
- 实时排行榜:使用Redis实现实时排行榜,以统计某个事件的排名。例如,统计网站的热门文章、热门商品等。通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜
- 会话管理:使用Redis实现会话管理,以实现用户登录、注销等功能。例如,将用户的会话信息存储在Redis中,使用Redis提供的会话管理功能,实现用户登录、注销等功能。
1.2. Redis为什么快
内存存储:Redis 将所有数据保存在内存中,而不是硬盘上。内存的读写速度远高于硬盘,因此 Redis 的数据访问速度极快。
单线程模型:Redis 使用单线程事件循环来处理请求,这避免了多线程间的上下文切换和锁竞争,从而提高了性能。不过,Redis 也通过使用 I/O 多路复用技术来处理多个连接,这使得单线程也能高效处理大量并发请求。
高效的数据结构:Redis 内部实现了多种高效的数据结构,包括字符串、列表、集合、有序集合和哈希表等。这些数据结构都经过精心设计和优化,以确保高效的存储和访问性能。
优化的网络通信:Redis 使用了基于 TCP 的二进制协议,该协议精简且高效,减少了通信开销。此外,Redis 的请求/响应模型非常简单,进一步减少了协议解析所需的时间。
持久化机制:虽然 Redis 主要将数据保存在内存中,但它也支持将数据持久化到硬盘上。Redis 提供了 RDB 快照和 AOF 日志两种持久化方式,用户可以根据需求选择合适的持久化策略,从而在保证性能的同时也能提高数据的可靠性。
良好的缓存机制:Redis 提供了多种缓存淘汰策略(如 LRU、LFU 等),用户可以根据实际需求选择合适的策略,从而在高并发场景下依然能够保持稳定的性能。
紧凑的编码:Redis 对常用的数据类型进行了紧凑的编码优化。例如,对于小整数,Redis 使用特殊的编码方式来减少内存占用和加快访问速度。
1.3. 为什么要用 Redis
高性能
如果用户访问的数据属于高频数据并且不会经常改变的话,那就可以将该用户访问的数据存在缓存中。保证用户下一次再访问这些数据的时候可以直接从缓存中获取。操作缓存就是直接操作内存,速度相当快。
高并发
QPS(Query Per Second):服务器每秒可以执行的查询次数
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)
2. Redis数据类型
Redis 主要支持以下几种数据类型:
基础数据类型
- string(字符串): 基本的数据存储单元,可以存储字符串、整数或者浮点数。
- hash(哈希):一个键值对集合,可以存储多个字段。
- list(列表):一个简单的列表,可以存储一系列的字符串元素。
- set(集合):一个无序集合,可以存储不重复的字符串元素。
- zset(sorted set:有序集合): 类似于集合,但是每个元素都有一个分数(score)与之关联。
特殊数据类型
- 位图(Bitmaps):基于字符串类型,可以对每个位进行操作。
- 超日志(HyperLogLogs):用于基数统计,可以估算集合中的唯一元素数量。
- 地理空间(Geospatial):用于存储地理位置信息。
- 发布/订阅(Pub/Sub):一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。
- 流(Streams):用于消息队列和日志存储,支持消息的持久化和时间排序。
- 模块(Modules):Redis 支持动态加载模块,可以扩展 Redis 的功能。
3. 基础数据类型
简单动态字符串(SDS)、LinkedList(双向链表)、Hash Table(哈希表)、SkipList(跳跃表)、Intset(整数集合)、ZipList(压缩列表)、QuickList(快速列表)
Redis 基础数据类型的底层数据结构实现如下:
String | List | Hash | Set | Zset |
---|---|---|---|---|
SDS | LinkedList/ZipList/QuickList | Hash Table、ZipList | ZipList、Intset | ZipList、SkipList |
Redis 3.2 之前,List 底层实现是 LinkedList 或者 ZipList。 Redis 3.2 之后,引入了 LinkedList 和 ZipList 的结合 QuickList,List 的底层实现变为 QuickList
- 字符串:字符串是Redis中最基本的数据结构,可以存储任何类型的数据,例如数字、文本、二进制数据等。字符串的最大长度为512MB。
- 哈希表:哈希表是一种键值对集合,其中每个键都对应一个值。哈希表可以存储任何类型的数据,例如数字、文本、二进制数据等。哈希表的最大长度为232-1。
- 列表:列表是一种有序的字符串集合,其中每个元素都有一个索引。列表可以存储任何类型的数据,例如数字、文本、二进制数据等。列表的最大长度为232-1。
- 集合:集合是一种无序的字符串集合,其中每个元素都是唯一的。集合可以存储任何类型的数据,例如数字、文本、二进制数据等。集合的最大长度为232-1。
- 有序集合:有序集合是一种有序的字符串集合,其中每个元素都有一个分数。有序集合可以存储任何类型的数据,例如数字、文本、二进制数据等。有序集合的最大长度为232-1。
3.1. String(字符串)
String 是一种二进制安全的数据结构,可以用来存储任何类型的数据
3.1.1. 应用场景
需要存储常规数据的场景
- 举例 :缓存 session、token、图片地址、序列化后的对象(相比较于 Hash 存储更节省内存)
- 相关命令 :
SET
、GET
需要计数的场景
- 举例 :用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数
- 相关命令 :
SET
、GET
、INCR
、DECR
分布式锁
利用 SETNX key value
命令可以实现一个最简易的分布式锁(存在一些缺陷,通常不建议这样实现分布式锁)
3.2. List(列表)
Redis 中的 List 就是双链表数据结构的实现
3.2.1. 应用场景
信息流展示
- 举例 :最新文章、最新动态。
- 相关命令 :
LPUSH
、LRANGE
。
消息队列
Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
相对来说,Redis 5.0 新增加的一个数据结构 Stream
更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决
3.3. Hash(哈希)
Redis 中的 Hash 是一个 String 类型的 field-value(键值对) 的映射表,特别适合用于存储对象,后续操作的时候,可以直接修改这个对象中的某些字段的值
3.3.1. 应用场景
对象数据存储场景
- 举例 :用户信息、商品信息、文章信息、购物车信息。
- 相关命令 :
HSET
(设置单个字段的值)、HMSET
(设置多个字段的值)、HGET
(获取单个字段的值)、HMGET
(获取多个字段的值)
key(用户ID)
field(商品ID)value(商品数量)
field(商品ID)value(商品数量)
field(商品ID)value(商品数量)
3.4. Set(集合)
Redis 中的 Set 类型是一种无序集合,集合中的元素没有先后顺序但都唯一,有点类似于 Java 中的 HashSet
。当你需要存储一个列表数据,又不希望出现重复数据时,Set 是一个很好的选择,并且 Set 提供了判断某个元素是否在一个 Set 集合内的重要接口,这个也是 List 所不能提供的。
可以基于 Set 轻易实现交集、并集、差集的操作,比如你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。这样的话,Set 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程
3.4.1. 应用场景
需要存放的数据不能重复的场景
- 举例:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 - 相关命令:
SCARD
(获取集合数量)
需要获取多个数据源交集、并集和差集的场景
- 举例 :共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集) 、订阅号推荐(差集+交集) 等场景。
- 相关命令:
SINTER
(交集)、SINTERSTORE
(交集)、SUNION
(并集)、SUNIONSTORE
(并集)、SDIFF
(差集)、SDIFFSTORE
(差集)
需要随机获取数据源中的元素的场景
- 举例 :抽奖系统、随机。
- 相关命令:
SPOP
(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER
(随机获取集合中的元素,适合允许重复中奖的场景)
3.5. Sorted Set(有序集合)
和 Set 相比,Sorted Set 增加了一个权重参数 score
,使得集合中的元素能够按 score
进行有序排列,还可以通过 score
的范围来获取元素的列表
3.5.1. 应用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
- 举例 :各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
- 相关命令 :
ZRANGE
(从小到大排序) 、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
需要存储的数据有优先级或者重要程度的场景 比如优先级任务队列。
- 举例 :优先级任务队列。
- 相关命令 :
ZRANGE
(从小到大排序) 、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
4. 特殊数据结构
4.1. Pub/Sub(发布/订阅)
一种消息传递机制,允许发送者(发布者)将消息发送到一个或多个通道,并且允许接收者(订阅者)接收来自一个或多个通道的消息。发布者和订阅者之间是松散耦合的,这意味着它们不需要知道彼此的存在
基本概念
- 频道(Channel):消息的传递媒介。发布者将消息发布到频道,订阅者订阅频道以接收消息。
- 发布者(Publisher):发送消息的一方,将消息发布到一个或多个频道。
- 订阅者(Subscriber):接收消息的一方,订阅一个或多个频道来接收消息。
基本命令
- 发布消息:
PUBLISH channel message
- 订阅频道:
SUBSCRIBE channel [channel ...]
- 取消订阅:
UNSUBSCRIBE [channel ...]
使用场景
- 实时消息处理:如聊天系统、实时通知等。
- 事件驱动架构:在微服务架构中,各个微服务通过消息传递进行通信。
- 日志收集:集中收集和处理分散的日志信息。
- 数据更新通知:当某些数据更新时,通知相关服务或客户端进行处理。
注意事项
- 消息丢失:Redis 的 Pub/Sub 没有持久化机制,消息只能被在线的订阅者实时消费,不能重复消费,如果订阅者在消息发布时不在线,则会丢失该消息。
- 消息顺序:消息的顺序保证是基于每个订阅者的,即订阅者接收到的消息顺序与发布者发送的顺序一致。
- 扩展性:Redis 的 Pub/Sub 适用于中小规模的实时消息传递。在大规模分布式系统中,可能需要使用更专业的消息队列系统(如 Kafka、RabbitMQ 等)来替代。
4.2. Streams(流)
Redis Streams 是 Redis 5.0 引入的一种新的数据结构,旨在支持高效的消息队列和日志处理等场景。它提供了强大的功能,如持久化、消费组和消息确认,适用于需要可靠消息传递和重复消费的应用场景。
基本概念
- Stream:一个按时间顺序保存的消息日志,每条消息都有一个唯一的 ID。
- Entry:Stream 中的每条消息,包含一个 ID 和一个字段-值对的集合。
- Consumer Group:消费者组,允许多个消费者协同处理一个 Stream,并支持消息确认和未处理消息的重分配。
- Consumer:消费者组中的一个成员,负责处理 Stream 中的消息。
基本命令
- 添加消息:
XADD mystream * field1 value1 field2 value2
(*
表示自动生成消息 ID) - 读取消息:
XRANGE mystream - +
,读取 Stream 中的所有消息,-
和+
分别表示流的起始和结束。
使用场景
- 日志收集与处理:将日志数据写入 Stream,并由多个消费者组进行处理和分析。
- 消息队列:在分布式系统中使用 Stream 作为消息队列,支持消息确认和重试机制。
- 事件源:保存事件日志,支持事件溯源和重放。
Redis Streams 提供了强大的消息队列功能,适用于需要高吞吐量、消息持久化和消费者组协同处理的场景。通过合理使用这些功能,可以构建高效可靠的分布式消息处理系统。
4.3. Bitmap(位存储)
通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态。可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做 offset(偏移量)
通过以下例子理解Bitmap
我们有1千万个整数,整数的范围在1到1亿之间。如何快速查找某个整数是否在这1千万个整数中呢?
可以使用一种特殊的散列表,那就是位图来解决
申请一个大小为1亿、布尔类型(true或者false)的数组。将这1千万个整数作为数组下标,将对应的数组值设置成true。比如,整数5对应下标为5的数组值设置为true,也就是array[5]=true。
查询某个整数K是否在这1千万个整数中的时候,只需将array[K]取出来,看是否等于true。如果等于true,那说明1千万整数中包含这个整数K;相反,就表示不包含这个整数K。
4.3.1. 应用场景
需要保存状态信息(0/1 即可表示)的场景
- 举例 :用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
- 相关命令 :
SETBIT
、GETBIT
、BITCOUNT
、BITOP
4.4. HyperLogLog(基数统计)
HyperLogLog 是一种有名的基数计数概率算法
Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64
个不同元素
4.4.1. 应用场景
数量量巨大(百万、千万级别以上)的计数场景
- 举例 :热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、
- 相关命令 :
PFADD
、PFCOUNT
4.5. Geospatial index(地理位置)
Geospatial index(地理空间索引,简称 GEO) 主要用于存储地理位置信息,基于 Sorted Set 实现。GEO 中存储的地理位置信息的经纬度数据通过 GeoHash 算法转换成了一个整数,这个整数作为 Sorted Set 的 score(权重参数)使用
通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能
GEO 底层是 Sorted Set ,可以对 GEO 使用 Sorted Set 相关的命令
4.5.1. 应用场景
需要管理使用地理空间数据的场景
- 举例:附近的人。
- 相关命令:
GEOADD
、GEORADIUS
、GEORADIUSBYMEMBER
5. 数据一致性策略
为了保证缓存和数据库的一致性,常用的策略包括:
- 缓存失效(Cache Invalidation):在数据库更新时,主动删除缓存中的对应数据,确保缓存中的数据是最新的。
- 双写(Dual Write):在数据库和缓存中同时进行写操作,确保两者数据一致。
6. 常见的缓存读写策略
6.1. Cache Aside Pattern(旁路缓存模式)
使用比较多的一个缓存读写模式,比较适合读请求比较多的场景
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准
写
- 先更新 db
- 然后直接删除 cache
读
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 再把数据放到 cache 中
6.2. Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能
写
- 先查 cache,cache 中不存在,直接更新 db
- cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)
读
- 从 cache 中读取数据,读取到就直接返回
- 读取不到的话,先从 db 加载,写入到 cache 后返回响应
6.3. Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写
但是Write Behind Pattern 只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量
7. Redis 线程模型
7.1. Redis 单线程模型
Redis 6.0 之前主要还是单线程处理
Redis 基于 Reactor 模式开发了自己的网络事件处理器:文件事件处理器
文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字
7.2. Redis6.0 多线程
6.0之前为何不使用多线程
- Redis 的性能瓶颈不在 CPU ,主要在内存和网络
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能
6.0之后为何使用多线程
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行
8. Redis 内存管理(重点)
8.1. 设置缓存过期时间
有助于缓解内存的消耗
Redis 中除了字符串类型有自己独有设置过期时间的命令 setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间
业务场景:需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效
8.2. 判断数据是否过期
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间
过期字典是存储在 redisDb 这个结构里:
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
8.3. 过期的数据的删除策略(重点)
惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
Redis 采用的是 定期删除+惰性/懒汉式删除
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这就需要内存淘汰机制
8.4. Redis 内存淘汰机制
MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据
volatile-lru(least recently used):从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
allkeys-random:从数据集中任意选择数据淘汰
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错
9. Redis 持久化机制(重点)
Redis 支持两种不同的持久化操作。Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)
9.1. RDB 持久化
快照持久化是 Redis 默认采用的持久化方式
Redis 在某个时间点上对存储在内存里面的数据进行的一次拍摄,用来记录数据的状态和变化,RDB 文件存储的内容是经过压缩的二进制数据
Redis 提供了两个命令来生成 RDB 快照文件:
save
: 同步保存操作,会阻塞 Redis 主线程bgsave
: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项
9.2. AOF 持久化
开启 AOF 持久化后,每执行一次写命令,Redis 就会将该命令写入到内存缓存 server.aof_buf
中。与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式:推荐 appendfsync everysec方式
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
9.3. AOF 日志
关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志
为什么在执行完命令之后记录日志
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
在执行完命令之后记录日志带来的风险
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)
9.4. AOF 重写
当 AOF 变得太大时,Redis 能够在后台自动重写一个新的 AOF 文件替换旧的AOF文件。该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作
重写AOF文件是指将内存中的数据写入到磁盘中,以便在Redis重启时可以重新加载数据。可以通过执行 BGREWRITEAOF 命令来重写AOF文件。该命令会在后台执行,不会阻塞Redis服务器
在重写期间,Redis会继续将新的写命令追加到旧的AOF文件中,以确保不会丢失任何数据
9.5. AOF和RDB的选用
在Redis重启时,可以通过加载RDB文件或者重放AOF文件来恢复数据。
RDB的优点是文件比较小,恢复速度比较快。缺点是可能会丢失最后一次快照之后的所有数据。
AOF的优点是数据比较安全,可以最大程度地避免数据丢失。缺点是文件比较大,恢复速度比较慢,要一条一条地执行命令。
一般来说,如果数据比较重要,建议使用AOF持久化方式。如果数据不是很重要,可以使用RDB持久化方式。如果两种方式都使用,Redis会优先使用AOF文件来恢复数据。
10. Redis 生产问题(重点)
10.1. 缓存穿透
根本原因:Redis中不存在请求所访问的key,导致大量请求直接访问数据库
指查询一个一定不存在的数据,由于缓存是不命中时被动写,即从 DB 查询到数据,则更新到缓存中,并且出于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要去 DB 查询,失去了缓存的意义。在流量大时,DB 可能就挂掉了
情况一:redis中没有key,数据库中没有key。如黑客攻击不断尝试错误的信息
方案一: 使用布隆过滤器,在缓存的基础上,构建布隆过滤器数据结构,在布隆过滤器中存储对应的 key,如果存在,则说明 key 对应的值为空。这样整个业务逻辑如下:
- 根据 key 查询缓存,如果存在对应的值,直接返回;如果不存在则继续执行。
- 根据 key 查询缓存在布隆过滤器的值,如果存在值,则说明该 key 不存在对应的值,直接返回空,如果不存在值,继续向下执行。
- 查询 DB 对应的值,如果存在,则更新到缓存,并返回该值,如果不存在值,则更新缓存到布隆过滤器中,并返回空
方案二:参数校验与限制,对传入的参数进行严格校验,防止恶意构造的请求直接访问数据库。例如,限制查询参数的长度和格式
方案三:缓存空对象,当从 DB 查询数据为空,我们仍然将这个空结果进行缓存,具体的值需要使用特殊的标识, 能和真正缓存的数据区分开,另外将其过期时间设为较短时间。
情况二:redis中没有key,数据库中有key。如新业务刚上线,redis是空的
方案一:预热redis,运行批处理脚本,将可能会大量访问的数据,提前加载到redis中,业务再开张
布隆过滤器
布隆过滤器是一种概率型数据结构,用于测试一个元素是否属于一个集合。它是一种高效的空间概率型数据结构
布隆过滤器使用一个位数组和一组哈希函数实现。当一个元素被添加到集合中时,它被哈希使用一组哈希函数,并将位数组中对应的位设置为1。当我们想要检查一个元素是否在集合中时,我们使用相同的一组哈希函数对元素进行哈希,并检查位数组中对应的位是否设置为1。如果所有位都设置为1,则该元素可能在集合中。如果任何位没有设置为1,则该元素肯定不在集合中。
可能会出现误判的情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
10.2. 缓存击穿
某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库
缓存穿透和缓存击穿有什么区别
缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。
解决办法
- 设置热点数据永不过期或者过期时间比较长。
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
- 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力
10.3. 缓存雪崩
大量的key是存在的,但是同时失效了,所有请求全部达到 DB 中,导致 DB 负荷大增,最终挂掉的情况
比如,对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了
解决方案:
- 缓存高可用:使用 Redis Sentinel 等搭建缓存的高可用,避免缓存挂掉无法提供服务的情况,从而降低出现缓存雪崩的情况。
- 使用本地缓存:如果使用本地缓存,即使分布式缓存挂了,也可以将 DB 查询的结果缓存到本地,避免后续请 求全部达到 DB 中。当然引入本地缓存也会有相应的问题,比如本地缓存实时性如何保证。对于这个问题,可以使用消息队列,在数据更新时,发布数据更新的消息,而进程中有相应的消费者消费该消息,从而更新本地缓存;简单点可以通过设置较短的过期时间,请求时从 DB 重新拉取。
- 请求限流和服务降级:通过限制 DB 的每秒请求数,避免数据库挂掉。对于被限流的请求,采用服务降级处理,比如提供默认的值,或者空白值。
- 采用 Redis 集群
11. Redis 集群
Redis支持三种集群方案
- 主从复制模式
- Sentinel(哨兵)模式
- Cluster模式
11.1. Redis主从复制
主从复制模式中包含一个主数据库实例(master)与一个或多个从数据库实例(slave)
具体工作机制为:
- slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(即上文所介绍的RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
- master将保存的快照文件发送给slave,并继续记录执行的写命令
- slave接收到快照文件后,加载快照文件,载入数据
- master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
- 此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
优点:
master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
缺点:
不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
难以支持在线扩容,Redis的容量受限于单机配置
11.2. Redis Sentinel
哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障
哨兵就是来为Redis集群站哨的,一旦发现问题能做出相应的应对处理。其功能包括:监视、选主、通知、自我监控
11.3. Redis Cluster
哨兵模式解决了主从复制不能自动故障转移,达不到高可用的问题,但还是存在难以在线扩容,Redis容量受限于单机配置的问题。Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题
Cluster模式的具体工作机制:
在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
客户端分片、代理分片、服务器端分片
12. Redis 事务(不推荐用)
Redis事务可以通过MULTI、EXEC、DISCARD和WATCH命令来实现。其中,MULTI命令用于开启一个事务,EXEC命令用于执行事务,DISCARD命令用于取消事务,WATCH命令用于监视一个或多个键,如果一个被 WATCH 命令监视的 Key 被 其他客户端/Session 修改的话,整个事务都不会被执行。如果 WATCH 与 事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的
MULTI
SET key1 value1
SET key2 value2
EXEC
注意:Redis 事务是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)
13. Redis 性能优化
13.1. 使用批量操作减少网络传输
一个 Redis 命令的执行可以简化为以下 4 步:
- 发送命令
- 命令排队
- 命令执行
- 返回结果
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT
原生批量操作命令
Redis服务器本身支持的:mget、hmget、sadd等
pipeline(流水线)
将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输,pipeline 是非原子操作
Lua 脚本
一段 Lua 脚本可以视作一条命令执行,可以看作是原子操作。一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行
13.2. 大量 key 集中过期问题
定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢
如何解决:
- 给 key 设置随机过期时间。
- 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间
13.3. Redis bigkey
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey,应该尽量避免写入 bigkey
可以使用Redis自带命令查找bigkey或使用专用工具
13.4. Redis 内存碎片(重点)
Redis产生内存碎片的原因
Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间:
Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认jemalloc。当程序申请内存时,jemalloc 会给它分配最接近其申请值的那个较大的空间,比如程序需要申请 17 字节的内存,jemalloc 会直接给它分配 32 字节的内存
频繁修改 Redis 中的数据也会产生内存碎片:
当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统
查看 Redis 内存碎片
使用 info memory
命令即可查看 Redis 内存相关的信息
mem_fragmentation_ratio
(内存碎片率)的值越大代表内存碎片率越严重
清理 Redis 内存碎片
Redis4.0-RC3 版本以后自带了内存整理
# 开启内存清理
config set activedefrag yes
# 内存碎片占用空间达到 500mb 的时候开始清理
config set active-defrag-ignore-bytes 500mb
# 内存碎片率大于 1.5 的时候开始清理
config set active-defrag-threshold-lower 50
# 内存碎片清理所占用 CPU 时间的比例不低于 20%
config set active-defrag-cycle-min 20
# 内存碎片清理所占用 CPU 时间的比例不高于 50%
config set active-defrag-cycle-max 50
重启节点可以做到内存碎片重新整理