- basic
- 为什么用 redis
- redis 的数据结构
- 如果有大量的 key 需要设置同一时间过期,需要注意什么
- 使用过 redis 分布式锁吗
- 查找固定前缀的 key
- Redis 做过异步队列吗
- redis 如何存储数据
- pipline 干啥用的
- Redis 主从机制
- Redis集群和高可用?
- 布隆过滤器
- 缓存雪崩
- 缓存穿透
- 缓存击穿
- 如何解决缓存穿透
- 如何解决缓存击穿
- 高并发架构?
- 内存模型/为啥那么快
- 什么是上下文切换
- 单线程执行,多核不是很亏么
- 单机可能出现的问题是什么,如何解决
- 主从直接如何进行数据交汇?如何持久化?
- 除了 cluster,还有什么集群模式
- 内存淘汰机制
- 没有查询,定期也没删除掉这些 key
- redis 数据类型使用场景
- redis 并发读写问题
- db 双写问题
- Redis 和 memcached 的区别
basic
为什么用 redis
- 传统的关系型数据库,例如 mysql, 并不适合所有的场景
- 例如针对热点数据的场景,比如首页数据的流量高峰访问,如果这些访问全部打到 mysql 这种传统的关系型数据库上,很容易打崩
- 例如针对限时的业务场景,比如用户注册时的需要对验证码进行过期处理,用 redis 的 expire 命令很容易实现
- 例如一些需要计数器的业务场景,比如限制手机号发送短信次数,接口限制请求次数等,可以用 redis 的 incr 这种原子操作很容易实现
- 等等
- 所以引入了缓存中间件
redis 的数据结构
- 基本的有 String List Hash Set Shorted-Set
- HyperLogLog、Geo、bitMap
- 一些 Redis Module,像 BloomFilter,RedisSearch,Redis-ML
如果有大量的 key 需要设置同一时间过期,需要注意什么
- 如果大批量的 key 过期时间过于集中,过期时间点 redis 可能会出现短暂的卡顿,甚至缓存雪崩的情况出现
- 比如使用定时任务刷新缓存,如果进行了批量失效,那么有可能大量请求进来访问缓存,造成缓存雪崩
- 所以不同业务场景下的 key 过期时间最好不太一样,而同种业务场景下 的 key 过期时间最好设置随机一点
- 让过期时间尽量的分散
使用过 redis 分布式锁吗
- 没有,但是有研究过
- 一般是使用
setnx
竞争锁,竞争到再用expire
设置过期时间避免释放
会出现什么问题? 或者 setnx
后 redis 或者机器宕机了
- 因为后续无法
expire
,所以会出现其他竞争锁的服务永远无法拿到这个锁 - 可以用两个方式
- 用 lua 脚本将
setnx
和setex
原子化 - 使用
set
指令,指定EX
和NX
参数
- 用 lua 脚本将
查找固定前缀的 key
- 如果量不大,用
keys*
量大呢
- 因为 redis 执行指令是单线程的,如果匹配到的 key 太多,就造成 redis 阻塞
-
拓展
SCAN cursor [MATCH pattern] [COUNT count]
pattern
支持正则?
- SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):
- SCAN 命令用于迭代当前数据库中的数据库键。
- SSCAN 命令用于迭代集合键中的元素。
- HSCAN 命令用于迭代哈希键中的键值对。
- ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。
- 以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。
- 不过, 增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 (offer limited guarantees about the returned elements)。
- 本文为CSDN博主「gtfaww」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
Redis 做过异步队列吗
- 不使用 mq 的时候可以用这个,配合 spring 的
[@Async](#)
rpush
生产,lpop
消费lpop
为null
时,适当sleep
可以不用 sleep 么
- 可以用
blpop key [key...] timeout
,会堵塞直到有数据- 返回当前 key 值 + lpop 出来的值
可以一次生产多次消费么
- 使用
pub-sub
模式
pub-sub
有什么缺点
- 消费者下线,会造成消息丢失
如何实现延时队列
- 使用 sorted set, score 为延时后的时间戳
- 消费端死循环
zrangebyscore <key> <min> <max> WITHSCORES
拿第一个值,判断过期没,过期就执行任务
redis 如何存储数据
- RDB 方式,在指定保存事件发生时全量保存
- AOF 方式,增量追加方式保存命令,当日志文件过大时, redis 会自动优化日志
- 优缺点
-
系统掉电怎么办
看配置文件 aof 方式的
sync
设置- 要一致可以设置每有一条命令执行就保存一次,不现实
- 可以设置为 1s 一次,最多丢失 1s 的数据
RDB 原理
- redis 主线程不参与保存日志的 io 操作
- 通过
fork
+cow
- fork 子进程来进行写日志
- cow,copy on write,父子进程共用同一资源,当父进程要修改资源时,在副本上写
pipline 干啥用的
- 将多次 io 往返时间缩短为一次时间
- 前提是pipeline执行的指令之间没有因果相关性。
- 使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
Redis 主从机制
- 有主从,从从
- 第一次同步的时候,主节点做一次
bgsave
,将后续操作记录都保存在内存 buffer在种- 将 RDB 文件全量同步给复制节点
- 从节点接收完毕后,加载该 RDB 文件到内存中用于恢复数据
- 恢复完成后通知主节点将这段时间内保存在主节点 buffer 中的操作记录同步过来
- 主节点再将恢复期间的数据发送给从节点
- 完成第一次同步
- 后续同步都使用 AOF 方式增量同步
Redis集群和高可用?
- Redis sentinel 着眼于高可用,master 挂掉后, sential 将 slave 提升为 master
- Redis Cluster 着眼于高扩展,用于提升单节点 redis 的内存容量。key 分片保存
布隆过滤器
- 布隆过滤器是一种位数组
- 通过 k 个 hash 函数对值进行 hash,分别获得的 hash 值,将这些值作为位数组的索引,设置为 1
- 如果一个键经过 hash 后的值命中了全部或者部分位数组中值为 1 的索引,那么它有可能存在
- 如果一个键经过 hash 后的值命中了位数组中值为 0 的索引,那么它肯定不存在
- 保证判断的数据可能存在和一定不存在
-
应用
网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
- Google Chrome 使用布隆过滤器识别恶意 URL;
- Medium 使用布隆过滤器避免推荐给用户已经读过的文章
缓存雪崩
- 如果接口是访问 db 中热点数据,为了避免请求经常打到 db 中查询同一批热点数据,那么会将这批热点数据保存到缓存中,设置了过期时间,并且设置定时任务更新这些热点数据。
- 如果刚好在缓存失效的时候,大量请求打进来,缓存都没有命中,那么这些请求就会打到 db 中,如果超过 db 的并发量,那么 db 就可能会挂掉。
- 即使 db 重启后,由于缓存的热点数据是需要读 db 的,而 db 又因为连续不断的请求打掉,造成缓存无法读取到新的热点数据,造成恶性循环
如何解决
- 直接设置这些缓存过期时间为永久,定时任务直接更新值即可
- 同批业务的缓存,如果需要设置过期时间应当设置为随机值,避免缓存集中失效
缓存穿透
- 请求一直访问缓存和 db 都没有的数据,那么缓存相当于不存在,如果大量请求进来,就直接打到 db 上了
- 就是绕过缓存,直接打 db
缓存击穿
- 缓存雪崩是因为 key 过期,大量请求没有命中缓存,直接打到 db 上
- 缓存穿透是大量请求绕过缓存,直接打到 db 上
- 缓存击穿是大量请求一直访问某个 key,如果这个 key 失效,对应的请求就直接打到 db 上
如何解决缓存穿透
- 根据前端不可信原则,或者说数据不可信原则,对接口的入参进行校验,比如手机号,不让它小于0 等
- 遇到分页查询时候要注意,如果请求中的分页参数非常大,需要对其进行处理
- 小于某个数量直接 limit,大于某个数量换 where
- 可以在 db 返回 null 时,对缓存中的值也设置为类似 null 的内容,设置过期时间,看实际需求
- 如果用到 nginx ,可以配置对大请求的 ip 拉入黑名单
- 还可以用布隆过滤器,这种数据结构可以保证 key 不存在的情况
如何解决缓存击穿
- 方式一,缓存数据不过期
方式二,设置互斥锁,访问过期缓存的请求抢锁
public String getResult(String key) throws InterruptedException {
if (! checkParam(key)) {
return "数据校验错误";
}
String value = getDataFromRedis(key);
// Blank, 需要读取 db 且设值
if (isBlank(value)) {
try {
// 抢到锁才能设置
if (lock.tryLock()) {
// 读 db 的数据
// !!!! 如果一直返回 null,还是可能造成服务一直打 db
value = readFromDb(key);
if (isNotBlank(value)) {
setDataToRedis(key, value);
} else {
setDataToRedisWithExpire(key, null, 10);
}
} else {
// 拿不到值,就等待一下
TimeUnit.MILLISECONDS.sleep(100);
// 递归拿
value = getResult(key);
}
} finally {
lock.unlock();
}
}
if (value == null ) {
// 说明 db 没有该数据
// 兜底
return "没这玩意";
}
return value;
}
高并发架构?
一般避免以上情况发生我们从三个时间段去分析下:
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
- 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
作者:敖丙 链接:https://juejin.im/post/5dbef8306fb9a0203f6fa3e2 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
有限流组件,可以保证 db 不会死,有兜底数据,返回空白页面,顶多访问多几次
内存模型/为啥那么快
- redis 是基于内存的采用单进程单线程模型的 kv 数据库,由 c 语言编写
- 它的大部分操作基于内存,速度很快
- 采用单线程,避免不必要的上下文切换,不用考虑各种锁的竞争和可能出现的死锁导致的性能消耗
- 使用多路I/O复用模型,非阻塞IO
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
什么是上下文切换
- 每个线程都有自己的对变量和方法的操作记录,如果当前线程由于各种原因需要暂停执行时,就需要将这些操作记录保存起来,这样即使轮到别的线程操作相同的变量和执行相同的方法,原来线程的记录不会被修改,称为现场保护,而轮到原来线程执行的时候,就会将现场还原,整个流程就叫做上下文切换。
单线程执行,多核不是很亏么
- 可以起多个 redis 进程
单机可能出现的问题是什么,如何解决
- 高可用问题,如果单节点挂掉,直接就没了
- 并发量问题,单节点抗不住更高的并发
- 容量问题,单节点存储容量可能不够用
- 可以使用 redis cluster,设置主从读写分离。
- 一个 cluster 可以设置 多个 主节点,每个主节点可以挂载多个从节点
- 如果主节点挂了,会自动将对应的一个从节点设置为主节点
cluster 和哨兵 的区别
- 比如一样的三主三从,哨兵模式需要额外的 redis 节点作为哨兵
- 存储内容上看,哨兵模式每个主存储的都是全量数据,而集群的各个主节点整体存储全量数据,集群的各个主节点平均存储 16384 个 slot 的内容,对 key 进行 crc16 算法 并且进行 mod 16384 找到对应的 slot,最后找到对应的主节点
主从直接如何进行数据交汇?如何持久化?
- 首先 redis 对数据的持久化有两种方式
- RDB,对 redis 的数据进行周期性的全量持久化
- AOF,将每条写入指令作为日志,以
append-only
方式记录到日志文件中。没有磁盘寻址开销 类似 mysql 的 binlog
- 两种方式都可以将 redis 内存中的数据进行备份。这些备份可以用来转移,便于恢复数据。
- RDB 适合冷备, AOF 适合热备
RDB AOF 的优缺点
RDB
- 优点
- 当指定事件出现时,触发 RDB 保存
- 会生成多个数据文件,每个数据文件对应一个时间段内 redis 的数据。
- 适合冷备,定期将这些文件转移到某个地方,恢复时根据需要选择对应的数据文件进行恢复。
- 主线程不参与日志生成,没有额外的 io 开销,会
fork
子进程,cow
方式保存日志 - 恢复数据也很快
- 缺点
- 由于 RDB 存储的是指定事件事件出现时触发保存的快照文件,那么在上一次和下一次生成期间的数据,如果 redis 挂了,会让这段时间内的数据全部丢失
- 即数据完整性不可能不足
- 如果数据量太大,在
fork
保存的时候可能会有额外的性能开销,从而影响到主进程
- 由于 RDB 存储的是指定事件事件出现时触发保存的快照文件,那么在上一次和下一次生成期间的数据,如果 redis 挂了,会让这段时间内的数据全部丢失
AOF
- 优点
- AOF 默认 1s 1次通过
fsync
操作将 redis 的指令写入日志中- 写入方式是通过
append_only
,即追加方式写数据,避免磁盘寻址的开销。
- 写入方式是通过
- 适合 灾难性数据误删除的紧急恢复,只要后台重写没发生,拷贝一份 aof ,将 flushall 啥的指令删除即可
- AOF 默认 1s 1次通过
- 缺点
- 文件大
- 支持的 QPS 比 RDB 支持要低,因为默认 1s 通过
fsync
异步刷新一次日志
- 额外内容
- aof_fsync用来指定flush策略,也就是调用fsync函数的策略,它一共有三种:
a. AOF_FSYNC_NO :每次都会把aof_buf中的内容写入到磁盘,但是不会调用fsync函数;
b. AOF_FSYNC_ALWAYS :每次都会把aof_buf中的内容写入到磁盘,同时调用fsync函数; (主进程负责)
c. AOF_FSYNC_EVERYSEC :每次都会把aof_buf中的内容写入到磁盘,如果距离上次同步超过一秒则调用 fsync
函数,由子线程负责。(默认值)
如何选择
- 都要
- RDB 恢复快,优先用 RDB 恢复
- AOF 数据全,用 AOF 做数据补全
主从之间的数据如何同步
- 为什么主从
- 仅仅有主,负责读和写,性能会上不去
- 有了主从
- 主节点负责写数据,同步数据给从节点,让从节点去读,分流数据
- 当启动一个 slave 时,会发送一个
psync
命令给 master,如果是第一次连接,会触发一个全量复制。- master 会启动一个线程,生成 RDB 快照,并且将后续的新的写请求缓存到内存块中
- RDB 文件生成后,master 将该 RDB 文件发送给 slave
- slave 接收后将 RDB 文件写入本地磁盘,然后将数据加载进内存,然后通知 master 加载完毕
- master 就会将生成 RDB 文件后在内存块中保存的新的写请求都发给 slave
- slave 接收新的写请求,加载进内存,完成第一次同步
- 后续通过 AOF 方式同步
数据传输的时候断网怎么办
- 会自动重连,丽娜姐后会将缺少的数据补上
- 同步期间的增量数据会保存在主节点的内存
除了 cluster,还有什么集群模式
- sentinel 集群模式
- 哨兵必须用三个实例保证自己的健壮性,哨兵 + 主从 不能保证数据不丢失,但是能保证高可用
- 如果主节点挂了,需要有一半以上的哨兵实例确定才能进行 slave 选举
- 如果只有两个哨兵,如果有一个哨兵挂掉,那么主节点如果挂掉,剩下一个哨兵无法确定集群是否可用,无法选举从节点为主节点
内存淘汰机制
- redis 是惰性删除 + 定期删除
- 惰性删除:当访问 key 时,会检查是否过期,过期就删除
- 定期删除:默认 100 ms 随机扫描一些设置了过期时间的 key,判断是否过期,过期就删除
为什么不扫描全部的设置过期时间的 key 呢
- 100 ms 扫描一次,性能消耗太高
没有查询,定期也没删除掉这些 key
- 使用内存淘汰机制
- noeviction: 当内存限制达到阈值,当客户端执行大部分的写入命令时,返回错误
- allkeys-lru: 对全部 key 进行 lru 淘汰
- volatile-lru: 回收过期集合中的 key
- allkeys-random: 随机回收 key
- volatile-random: 随机回收过期集合中的 key
- volatile-ttl: 回收过期集合中 ttl (存活时间) 较短的 key
LRU 算法
- redis 采用的是近似 lru 算法
redis 数据类型使用场景
string
- 缓存功能: 缓存热点数据
- 计数器: 可以作为系统的计数器,redis incr/decr 是原子操作
- 共享用户 session: 可以作为共享 session 的一种方案
hash
- 没啥好用的,一半用来保存多重数据
list
- 存储列表型数据,比如粉丝列表、评论列表
- 实现分页查询: 使用
lrange
读取某个闭区间内的元素- 类似下拉不断分页的东西
- 简单消息队列:
rpush
生成数据,lpop/blpop
消费数据
set
- 用于分布式去重
- 利用 set 的特点,玩下交集、并集、差集,查看共同好友啥的
sorted set
- 有排序的 set,插入即排序
- 排行榜
- 作为带权重的队列,key 为队列名,score 为权重
redis 并发读写问题
- 分布式锁 + 写前判断时间戳,如果 db 时间戳更新才写
db 双写问题
- 需要严格双写一致,可以将请求串行化到内存队列中
-
经典的双写模式
Cache Aside Pattern
- 读的时候,先读缓存,缓存没有,就读 db, 拿到数据放入缓存,同时返回响应
- 更新的时候,先更新数据库,然后删除缓存
为什么是删除缓存,不是更新缓存
- 如果缓存中的数据需要通过计算等消耗性能的操作才能得到,那么每次更新 db 就要计算重新设置缓存,如果该缓存不是数据热点数据,那么这个重新计算更新操作没有必要
- 用到缓存才算缓存,属于一种懒加载的模式
Redis 和 memcached 的区别
- 线上已经有跑了
- 其次 redis 比 memcached 有更多的数据结构
- redis 在 3.0 后支持 cluster 模式
- redis 单线程,memcached 可以使用多核
- 所以memcached 在存储 100k 以上大数据时性能高于 redis