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 脚本将 setnxsetex 原子化
    • 使用 set 指令,指定 EXNX 参数

查找固定前缀的 key

  • 如果量不大,用 keys*

量大呢

  • 因为 redis 执行指令是单线程的,如果匹配到的 key 太多,就造成 redis 阻塞
  • 可以用 scan 取代

    拓展

  • 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 消费
    • lpopnull 时,适当 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

    系统掉电怎么办

  • 看配置文件 aof 方式的 sync 设置

  • 要一致可以设置每有一条命令执行就保存一次,不现实
  • 可以设置为 1s 一次,最多丢失 1s 的数据

RDB 原理

  • redis 主线程不参与保存日志的 io 操作
  • 通过 fork + cow
    • fork 子进程来进行写日志
    • cow,copy on write,父子进程共用同一资源,当父进程要修改资源时,在副本上写

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 不存在的情况

如何解决缓存击穿

  • 方式一,缓存数据不过期
  • 方式二,设置互斥锁,访问过期缓存的请求抢锁

    1. public String getResult(String key) throws InterruptedException {
    2. if (! checkParam(key)) {
    3. return "数据校验错误";
    4. }
    5. String value = getDataFromRedis(key);
    6. // Blank, 需要读取 db 且设值
    7. if (isBlank(value)) {
    8. try {
    9. // 抢到锁才能设置
    10. if (lock.tryLock()) {
    11. // 读 db 的数据
    12. // !!!! 如果一直返回 null,还是可能造成服务一直打 db
    13. value = readFromDb(key);
    14. if (isNotBlank(value)) {
    15. setDataToRedis(key, value);
    16. } else {
    17. setDataToRedisWithExpire(key, null, 10);
    18. }
    19. } else {
    20. // 拿不到值,就等待一下
    21. TimeUnit.MILLISECONDS.sleep(100);
    22. // 递归拿
    23. value = getResult(key);
    24. }
    25. } finally {
    26. lock.unlock();
    27. }
    28. }
    29. if (value == null ) {
    30. // 说明 db 没有该数据
    31. // 兜底
    32. return "没这玩意";
    33. }
    34. return value;
    35. }

高并发架构?

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

作者:敖丙 链接:https://juejin.im/post/5dbef8306fb9a0203f6fa3e2 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
有限流组件,可以保证 db 不会死,有兜底数据,返回空白页面,顶多访问多几次

内存模型/为啥那么快

  • redis 是基于内存的采用单进程单线程模型的 kv 数据库,由 c 语言编写
  • 它的大部分操作基于内存,速度很快
  • 采用单线程,避免不必要的上下文切换,不用考虑各种锁的竞争和可能出现的死锁导致的性能消耗
  • 使用多路I/O复用模型,非阻塞IO
  • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;


什么是上下文切换

  • 每个线程都有自己的对变量和方法的操作记录,如果当前线程由于各种原因需要暂停执行时,就需要将这些操作记录保存起来,这样即使轮到别的线程操作相同的变量和执行相同的方法,原来线程的记录不会被修改,称为现场保护,而轮到原来线程执行的时候,就会将现场还原,整个流程就叫做上下文切换。

单线程执行,多核不是很亏么

  • 可以起多个 redis 进程

单机可能出现的问题是什么,如何解决

  1. 高可用问题,如果单节点挂掉,直接就没了
  2. 并发量问题,单节点抗不住更高的并发
  3. 容量问题,单节点存储容量可能不够用
  • 可以使用 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 保存的时候可能会有额外的性能开销,从而影响到主进程

AOF

  • 优点
    • AOF 默认 1s 1次通过 fsync 操作将 redis 的指令写入日志中
      • 写入方式是通过 append_only ,即追加方式写数据,避免磁盘寻址的开销。
    • 适合 灾难性数据误删除的紧急恢复,只要后台重写没发生,拷贝一份 aof ,将 flushall 啥的指令删除即可
  • 缺点
    • 文件大
    • 支持的 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 做数据补全

主从之间的数据如何同步

  • 为什么主从
    • 仅仅有主,负责读和写,性能会上不去
  • 有了主从
    • 主节点负责写数据,同步数据给从节点,让从节点去读,分流数据
  1. 当启动一个 slave 时,会发送一个 psync 命令给 master,如果是第一次连接,会触发一个全量复制。
    1. master 会启动一个线程,生成 RDB 快照,并且将后续的新的写请求缓存到内存块中
    2. RDB 文件生成后,master 将该 RDB 文件发送给 slave
    3. slave 接收后将 RDB 文件写入本地磁盘,然后将数据加载进内存,然后通知 master 加载完毕
    4. master 就会将生成 RDB 文件后在内存块中保存的新的写请求都发给 slave
    5. slave 接收新的写请求,加载进内存,完成第一次同步
  2. 后续通过 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

  1. 缓存功能: 缓存热点数据
  2. 计数器: 可以作为系统的计数器,redis incr/decr 是原子操作
  3. 共享用户 session: 可以作为共享 session 的一种方案

hash

  1. 没啥好用的,一半用来保存多重数据

list

  1. 存储列表型数据,比如粉丝列表、评论列表
  2. 实现分页查询: 使用 lrange 读取某个闭区间内的元素
    1. 类似下拉不断分页的东西
  3. 简单消息队列: rpush 生成数据,lpop/blpop 消费数据

set

  1. 用于分布式去重
  2. 利用 set 的特点,玩下交集、并集、差集,查看共同好友啥的

sorted set

  • 有排序的 set,插入即排序
  1. 排行榜
  2. 作为带权重的队列,key 为队列名,score 为权重

redis 并发读写问题

  • 分布式锁 + 写前判断时间戳,如果 db 时间戳更新才写

db 双写问题

  • 需要严格双写一致,可以将请求串行化到内存队列中
  • 但是性能大幅度下降

    经典的双写模式

  • Cache Aside Pattern

  • 读的时候,先读缓存,缓存没有,就读 db, 拿到数据放入缓存,同时返回响应
  • 更新的时候,先更新数据库,然后删除缓存

    为什么是删除缓存,不是更新缓存

  1. 如果缓存中的数据需要通过计算等消耗性能的操作才能得到,那么每次更新 db 就要计算重新设置缓存,如果该缓存不是数据热点数据,那么这个重新计算更新操作没有必要
  2. 用到缓存才算缓存,属于一种懒加载的模式

Redis 和 memcached 的区别

  • 线上已经有跑了
  • 其次 redis 比 memcached 有更多的数据结构
  • redis 在 3.0 后支持 cluster 模式
  • redis 单线程,memcached 可以使用多核
    • 所以memcached 在存储 100k 以上大数据时性能高于 redis