目录
Redis 基础
Redis 简介
缓存数据的处理流程是什么样的?
为什么要用 Redis?(为什么要使用缓存?)
Redis 除了做缓存之外,还可以做什么?
Redis 可以做消息队列吗?
Redis 数据类型
Redis 常用的数据类型有哪些?
String 的应用场景有哪些?
String 还是 Hash 来存储对象?
Redis 如何实现一个排行榜
使用 Set 实现抽奖系统需要用到什么命令?
使用 Bitmap 统计活跃用户?
使用 HyperLogLog 统计页面 UV 怎么做?
Redis 线程模型
Redis 单线程模式了解吗?
Redis 6.0 之前为什么不使用多线程?
Redis 6.0 之后为什么又引入了多线程?
Redis 内存管理
Redis 给缓存设置过期时间有什么用?
Redis 如何判断数据是否过期?
过期的删除策略有哪些?
Redis 内存淘汰机制了解吗?
Redis 的持久化
如何保证 Redis 挂掉后重启数据可以恢复?
什么是 RDB 持久化?
RDB 创建快照的时候会阻塞主线程吗?
什么是 AOF 持久化?
AOF 日志是如何实现的?
AOF 重写了解吗?
Redis 4.0 对于持久化机制做了什么优化?
Redis 事务
如何使用 Redis 事务?
Redis 支持原子性吗?
如何解决 Redis 事务的缺陷?
Redis 性能优化
Redis bigkey
什么是 bigkey?
bigkey 有什么危害?
如何发现 bigkey?
大量 key 集中过期问题
Redis 生产问题
缓存穿透
什么是缓存穿透?
缓存穿透情况的处理流程是怎么样的?
解决办法
布隆过滤器为什么会存在误判的情况?
缓存击穿
缓存雪崩
什么是缓存雪崩?
解决办法
如何保证缓存和数据库数据的一致性?
Cache Aside Pattern(旁路缓存模式)
Redis 基础
Redis 简介
Redis 是使用 C 语言开发的一个数据库,它的数据是存储在 内存 当中,所以它的存取速度是非常之快的,因此 Redis 被广泛用于 缓存。
Redis 除了做缓存之外,还可以用来做 分布式锁、消息队列、事务等等。
缓存数据的处理流程是什么样的?
用户请求数据,首先在缓存中寻找是否有对应的数据,如果没有,再去数据库中寻找对应的数据。如果在数据库中找到了该数据,返回该数据并将其加入到缓存中;如果没找到就返回空数据。
为什么要用 Redis?(为什么要使用缓存?)
我们从高性能和高并发两个角度来说明这个事情。
高性能:将用户访问的数据放在缓存中,保证用户下一次再访问这些数据的时候可以可以直接从缓存中获取。因为缓存是放在内存中的,所以存取速度非常快。但是我们需要保证缓存和数据库中的数据的一致性。
高并发:MySQL 的 QPS 在 1w 左右,使用 Redis 缓存之后很容易达到 10w+(就单机 Redis 而言,Redis 集群会更高)。
把数据库中的数据部分转入到缓存中,可以提高系统的并发性。
Redis 除了做缓存之外,还可以做什么?
分布式锁:使用 Redisson 来实现分布式锁是分布式锁中的“王者”级别。Redisson 使用了“看门狗”技术来不断更新锁的过期时间,如果锁对应的服务宕机,那么在 30s(默认)过后锁就会被自动释放,不会产生死锁。
限流:一般通过 Redis + Lua 脚本的方式来实现限流。
漏桶算法,将水比作请求,如果水流入的速度小于流出的速度的话,桶永远不会被装满;而流入的速度如果大于流出的速度的话,那么桶就会逐渐被装满,而此时新的请求就会“溢出去”,此时就处于限流状态,请求不能再进入。
令牌桶算法,系统会维护一个令牌桶,一个请求只有获取到令牌后才能进入桶。系统会以一个恒定的速度向桶里增加令牌,当请求没有获取到令牌就拒绝其请求。
Redis + Lua 脚本,Lua 脚本与 MySQL 的存储过程比较类似,它也是具有原子性的,它是一段具有业务逻辑的代码块。我们使用 Lua 脚本去实现 漏桶算法和令牌桶算法。
消息队列:Redis 自带的 list 数据结构可以作为队列使用。Redis 5.0 中加入的 Stream 类型更适合用来做消息队列,它类似于 Kafka,有主题和消费组的概念,支持消息持久化和 ACK 机制。
复杂业务场景:通过 Redis 以及 Redis 拓展提供的数据类型,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。
Redis 可以做消息队列吗?
可以用 Redis 5.0 提供的 Stream 来做消息队列。
Stream 支持消息持久化、发布 / 订阅模式、按照消费者组进行消费。
但和专业的消息队列相比,还有很多欠缺的地方,比如消息丢失和堆积的问题不好解决。所以我们还是建议使用 RocketMQ、Kafka 来作为我们的消息队列。
Redis 数据类型
Redis 常用的数据类型有哪些?
五种基础:String、List、Set、Hash、Zset(有序集合)
三种特殊:HyperLogLogs(基数统计)、Bitmap(位图)、Geospatial(地理位置)
String:最简单且最常用,可以用来存储任何类型的数据(int float string 图片 序列化后的对象)
使用 SETNX key value 命令可以实现一个最简易的分布式锁(存在一些缺陷)
List:列表,其实就是链表数据结构的实现,它的实现为一个双向链表,可以支持反向查找和遍历
list 可以用来做消息队列,但功能简单且存在很多缺陷,不建议这么做。
Hash:Redis 中的 Hash 是一个 String 类型的键值对映射表,特别适合存储对象,后续操作的时候你可以直接修改这个对象中的某些字段。
Hash 类似于 1.8 之前的 HashMap,内部实现也差不多(数组 + 链表)。不过 Redis 做了很多优化。
Set:集合,是一种无序集合,元素没有先后顺序但唯一。类似于 Java 中的 HashSet。
Sorted Set:有序集合,它增加了一个权重参数 score,使得集合中的元素可以按照 score 进行有序排序,还可以通过 score 的范围获取元素的列表。有点像 HashMap 和 TreeSet 的结合体。
Bitmap:位图,你可以将 Bitmap 看作一个存储二进制数字的数组。
Bitmap 只需要一个 bit 位来表示某个元素对应的值或者状态,key就是对应元素本身。Bitmap 本身会极大节省存储空间。
用于保存用户签到情况、活跃用户情况等
HyperLogLog:基数统计,它是一种有名的基数计数概率算法,基于 LogLog Counting 优化改进而来。只需要 12k 的空间就可以存储接近 2^64 个不同的元素。
应用场景是数据量巨大的计数场景:热门网站每日访问 ip 数统计、热门帖子 uv 统计。
Geospatial:地理位置,地理空间索引,通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。
它底层是 sorted set 实现的。
String 的应用场景有哪些?
1、常规数据(session token 序列化后的对象)的缓存
2、计数
3、setnx key value 实现分布式锁
String 还是 Hash 来存储对象?
1、String 存储的是序列化后的对象,存放整个对象
2、Hash 存储的是对象的每个字段,如果字段需要经常性地变动,那么就选择 hash 来存储
3、String 存储更省内存(约为 Hash 一半),并且具有多层嵌套的对象时也更加方便。
绝大部分情况,我们建议使用 String 来存储。
对于购物车信息:我们建议用 Hash 存储,用户 id 为 key,商品 id 为 field,商品数量为 value。我们需要对购物车进行频繁修改和变动,所以使用 Hash。
Redis 如何实现一个排行榜
使用 sorted set,里面有 score 字段,可以进行排行。
Redis 命令:zrange(升序)、zrevrange(降序)、zrevrank(指定元素排名)、zscore(查询)
使用 Set 实现抽奖系统需要用到什么命令?
spop key count:随机移除集合中一个或多个元素,适用于不能重复中奖
srandmember key count:随机抽出一个或多个元素,适用于可以重复中奖
使用 Bitmap 统计活跃用户?
使用日期 作为 key,然后 id 为 offset,如果当日活跃则设置为 1 。
使用 HyperLogLog 统计页面 UV 怎么做?
1、将访问页面的每个 id 都添加到 HyperLogLog 中。
2、统计 UV
Redis 线程模型
Redis 4.0 后引入了多线程来执行一些大键值对的异步删除操作,Redis 6.0 后引入了多线程来处理网络请求。
Redis 单线程模式了解吗?
Reactor 模型 —— 事件处理模型
Redis —— IO 多路复用程序,不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。监听多个套接字socket。
Redis 6.0 之前为什么不使用多线程?
1、单线程编程容易并且更好维护
2、Redis 瓶颈更多在 内存和网络,而不在 CPU
3、多线程存在死锁、上下文切换等问题,甚至会影响性能
Redis 6.0 之后为什么又引入了多线程?
提高网络 IO 读写性能,这是 Redis 的一个性能瓶颈。
虽然 Redis 6.0 引入了多线程,但是 Redis 的多线程只是在网络 IO 上的,执行命令依旧是单线程顺序执行,不需要担心线程安全问题。
Redis 内存管理
Redis 给缓存设置过期时间有什么用?
一般情况下,我们设置的缓存数据有一个过期时间。
因为内存空间有限,如果缓存中的数据一直保存的话,容易 OutOfMemory
Redis 自带了给缓存设置过期时间的功能,比如:
Redis 中除了 String 可以通过 setex 来设置过期时间,其他数据结构都需要通过 expire 来进行设置。
缓存设置过期时间还可以实现一些功能:例如,验证码 10min 有效;用户 token 1天有效等等。
Redis 如何判断数据是否过期?
Redis 通过一个 过期字典(hash 表),来保存数据过期时间。过期字典的 key 指向 Redis 数据库中某个 key,过期字典的值是一个 long long 类型的整数,保存了 key 所指向的数据库键的过期时间。
redisDb 的结构体:
过期的删除策略有哪些?
1、惰性删除:只有在取出缓存的时候才对数据进行过期检查。对 CPU 友好,但是会造成太多过期 key 没有删除。
2、定期删除:每隔一段时间抽取一批 key 进行过期检查。
定期删除对内存更友好,惰性删除对 CPU 更友好。
Redis 采用 定期删除 + 惰性删除。
但如果仅仅采用给 key 设置过期时间还是有可能导致内存中存在很多过期 key 的情况,导致大量 key 堆积在内存中。我们使用 Redis 内存淘汰机制来解决这个问题。
Redis 内存淘汰机制了解吗?
MySQL 中有 2000w 数据,Redis 中只存了 20w,怎么保证 Redis 中的数据都是热点数据??
Redis 提供 8 种数据淘汰策略:
1、volatile-lru:从设置过期时间的key中挑选最近最少使用
2、allkeys-lru:从所有key中挑选最近最少使用
3、volatile-lfu:最近最不常使用
4、allkeys-lfu:最近最不常使用
5、volatile-random:随机
6、allkeys-random:随机
7、volatile-ttl:从设置过期时间的key中挑选ttl最短的
8、no-eviction:内存不足会报错
Redis 的持久化
如何保证 Redis 挂掉后重启数据可以恢复?
两种方式
1、RDB(快照)
2、AOF(追加文件)
什么是 RDB 持久化?
Redis 通过创建快照的方式来保存某个时间点上的数据副本。Redis 创建快照之后,可以对快照进行备份,将它复制到其他服务器而创建具有相同数据的服务器副本(Redis 主从结构),还可以将快照留在原地以便重启服务器使用。
RDB 持久化是 Redis 默认采用的持久化方式。
RDB 创建快照的时候会阻塞主线程吗?
save会,bgsave不会。
什么是 AOF 持久化?
与 RDB 相比,AOF 的实时性更好,现在已经成为主流的持久化方案。可以通过如下命令开启:
开启 AOF 后,每执行一条改变 Redis 中数据的命令,Redis 就会将该数据写入到内存缓存的 server.aof_buf 中,然后根据配置来决定何时将其同步到硬盘中的 AOF 文件中。
为了兼顾数据和性能,通常考虑 appendfsync everysec,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎不受影响,而且如果系统崩溃最多丢失 1s 的数据。
AOF 日志是如何实现的?
MySQL 是执行命令前记录日志,而 AOF 是在执行命令后记录日志。
为什么要这么做?
1、避免额外的检查开销,AOF 记录日志不会对命令进行语法检查
2、不会阻塞当前的命令执行
风险:
1、刚执行完指令 Redis 就宕机会导致对应的修改丢失
2、可能会阻塞后续其他命令执行(AOF 记录日志是在 Redis 主线程中进行的)
AOF 重写了解吗?
AOF 重写是产生一个新的 AOF 文件,这个新的 AOF 文件和原有 AOF 保存的数据库形态一致,但占用内存更小。
在执行 bgrewriteaof 命令的时候,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在 AOF 重写的时候记录服务器执行的所有写命令。当子进程完成重写 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新的 AOF 文件中,使得新的 AOF 文件保存的数据库状态和现在一致。最后用新的 AOF 文件替代旧的 AOF 文件,以此来完成 AOF 文件重写操作。
Redis 4.0 对于持久化机制做了什么优化?
Redis 4.0 开始支持 RDB 和 AOF 混合持久化。
可以结合 RDB 和 AOF 的有点,快速加载同时避免丢失过多数据。
Redis 事务
如何使用 Redis 事务?
Redis 可以通过 MULTI,EXEC,DISCARD,WATCH 等命令来实现事务的功能。
使用 MULTI 命令后可以输入多个命令。Redis 不会执行这些命令,而是将他们放入队列。当调用 EXEC 指令的时候,将一次性执行所有命令。
也可以使用 DISCARD 命令来取消一个事务,它会清空队列中所有的命令。
WATCH 指令用于监听指定的 key,当调用 EXEC 命令执行事务的时候,如果一个被 WATCH 监听的 key 被修改,那么事务都不会背执行,直接返回失败。
Redis 支持原子性吗?
Redis 的事务和关系型数据库中的事务有所不同。事务具有 ACID 的特性。
Redis 事务在运行错误的情况下,除了出现错误的命令外,其他命令都可以正常执行。且 Redis 中不存在回滚的机制,Redis 开发者的解释是回滚会影响性能且没必要。所以 Redis 中其实不支持原子性。
那么 Redis 中的事务应该是这个意思:它将多个命令进行打包,按顺序执行所有命令且不会被打断。
如何解决 Redis 事务的缺陷?
Redis 事务是不支持原子性的,但它在 2.6 开始支持 Lua 脚本,它的功能与事务类似。我们可以利用 Lua 脚本批量执行多个 Redis 命令,并且这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。
如果 Lua 脚本运行过程中出错,那么出错之后的命令是不会被运行的。但是,出错之前的命令也是不会被撤回的。所以严格来说 Lua 脚本也不支持原子性。
7.0 新增 Redis functions 特性,可以理解为比 Lua 更强大的脚本。
Redis 性能优化
Redis bigkey
什么是 bigkey?
如果一个 key 对应的 value 占用的内存比较大,这个 key 被看作是一个 bigkey。具体来说:String 类型的 value 超过 10kb,复合类型的 value 包含的元素超过 5000 个。
bigkey 有什么危害?
1、占用内存很多
2、对性能有比较大的影响(当 bigkey 被请求的时候,需要通过网络将其传输到客户端。如果常常要传输大键,整体的响应时间会显著增加。)
因此我们应该尽量避免写入 bigkey!
如何发现 bigkey?
1、使用 Redis 自带的指令 --bigkeys 参数
2、通过 RDB 文件进行查找(如果使用的是 RDB 进行持久化)
大量 key 集中过期问题
我们的过期策略,使用的是 惰性删除 + 定期删除
定期删除中,如果突然遇到大量过期 key 的时候,客户端请求必须等待定期删除任务完成后才能发出,因为这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度比较慢。
解决:
1、给 key 设置随机过期时间
2、使用 lazy-free(惰性删除/延迟释放)。让 Redis 采用异步方式延迟释放 key 所占用的内存,将其交给子线程进行处理,避免阻塞主线程。
个人建议:尽量设置随机过期时间。
Redis 生产问题
缓存穿透
什么是缓存穿透?
简单来说就是大量请求的 key 不在缓存中,导致请求直接到了数据库上。
缓存穿透情况的处理流程是怎么样的?
解决办法
1、缓存无效 key:如果从数据库中找不到该数据,缓存一个空数据。不能从根本上解决问题
2、布隆过滤器:把所有可能存在的请求的值都放在布隆过滤器中,当有请求过来的时候,先判断该请求的值是否存在于布隆过滤器中。如果不存在,直接返回请求参数错误信息给客户端,存在才会进入如下流程。
布隆过滤器存在误判的情况(小概率)—— 如果它说该数据不存在,那么一定不存在;如果它说存在,那么有小概率不存在(大概率存在)
布隆过滤器为什么会存在误判的情况?
原理:
1、使用哈希函数进行运算,得到哈希值
2、根据哈希值,在位数组中将对应下标改为 1
所以可能存在冲突问题(误判)
缓存击穿
缓存雪崩
什么是缓存雪崩?
缓存在同一时间大量失效,后面的请求都直接落到数据库上,导致大量数据同时落到数据库上,数据库压力很大!
解决办法
1、设置随机失效时间
2、缓存永不失效
3、限流
如何保证缓存和数据库数据的一致性?
主要看我们的需求,如果我们可以容忍短时间的不一致的话,解决起来相对简单;而如果是银行这种不可容忍的话,我们就要牺牲一定程度的性能了。
但我们应聘互联网公司,一般性能要优先的。
Cache Aside Pattern(旁路缓存模式)
更新 DB,然后直接删除缓存。
如果更新数据库成功,删除缓存失败的话,解决方案:
1、缓存失效时间变短(不推荐)
2、增加 cache 更新重试机制:如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间重试一次,并且自己设定重试次数。如果多次重试失败,那么我们把更新失败的 key 加入队列,等缓存服务可用的时候再删除即可。