1.Redis是什么

Redis是基于内存的一种,key-value形式的非关系型数据库

2.Redis是单线程还是多线程

Redis的核心流程包括:接受命令、解析命令、执行命令、返回结果等
Redis4.0之前Redis完全是单线程的
Redis4.0时引入了多线程,但核心流程还是单线程的,多线程只用于后台处理
Redis6.0时核心流程也引入了多线程的概念。在接受命令和返回结果阶段使用了多线程。但是解析命令执行命令阶段还是单线程的。

3.为什么Redis是单线程的

Redis的数据IO完全基于内存操作,因此CPU不会限制Redis的性能。因而单线程方法就成为了最优解。因为多线程的引入会带来额外的线程切换开销

4.为什么Redis是单线程的也很快

  1. 基于内存的操作
  2. 使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器
  3. 单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。(线程从失去时间片时开始,到再次获取时间片,CPU需要维护住线程失去时间片时的状态并恢复线程状态,这就是所谓的线程上下文开销)

    5.Redis在项目中的使用

  4. 緩存

  5. 计数器(incrby)
  6. 排行榜(zset)
  7. 分布式锁
  8. 消息队列(stream)
  9. 访客统计(hyperloglog)
  10. 地理位置(geo)

    6.Redis常用的数据结构

    基本数据类型

  11. String:字符串,最基础的数据类型。

  12. List:列表。
  13. Hash:哈希对象。
  14. Set:集合。
  15. Sorted Set:有序集合,Set 的基础上加了个分值。

高级数据类型

  1. HyperLogLog:通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
  2. Geo:redis 3.2 版本的新特性。可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作:获取2个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
  3. Bitmap:位图。
  4. Stream:主要用于消息队列,类似于 kafka,可以认为是 pub/sub 的改进版。提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

    7.Redis 删除过期键的策略(缓存失效策略、数据过期策略)

    定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
    惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
    定期删除:每隔一段时间,默认100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
    Redis 使用惰性删除和定期删除。

    8.Redis的内存淘汰策略

  5. noeviction:默认策略,不淘汰任何 key,直接返回错误

  6. allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key(最近最少使用)
  7. allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key(最近最不经常使用)
  8. allkeys-random:在所有的 key 中,随机淘汰部分 key
  9. volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key(最近最少使用)
  10. volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key(最近最不经常使用)
  11. volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
  12. volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰

    9.LRU与LFU

    LRU:最近最少使用(最长时间)淘汰算法(Least Recently Used)。LRU是淘汰最长时间没有被使用的页面。
    LFU:最不经常使用(最少次)淘汰算法(Least Frequently Used)。LFU是淘汰一段时间内,使用次数最少的页面
    例子:

    1. 2 1 2 1 2 3 4

    当需要使用页面4时,内存块中存储着1、2、3,内存块中没有页面4,就会发生缺页中断,而且此时内存块已满,需要进行页面置换。
    若按LRU算法,应替换掉页面1。因为页面1是最长时间没有被使用的了,页面2和3都在它后面被使用过。
    若按LFU算法,应换页面3。因为在这段时间内,页面1被访问了2次,页面2被访问了3次,而页面3只被访问了1次,一段时间内被访问的次数最少。
    LRU 关键是看页面最后一次被使用到发生替换的时间长短,时间越长,页面就会被置换
    LFU关键是看一定时间段内页面被使用的频率(次数),使用频率越低,页面就会被置换

  13. LRU算法适合:较大的文件比如游戏客户端(最近加载的地图文件)

  14. LFU算法适合:较小的文件和零碎的文件比如系统文件、应用程序文件
  15. LRU消耗CPU资源较少,LFU消耗CPU资源较多

LRU算法具体实现为,底层维护一个双向链表,链表分为热数据区以及冷数据区

  1. 存入数据时默认存入冷数据区头节点
  2. 查询数据时,如果为热数据,则移动向头节点
  3. 查询数据时,如果为冷数据,判定该冷数据存在时长,达到一定时长移动到热数据区头结点,否则保持不动
  4. 当存入数据时,如果内存已满,则淘汰掉尾节点
  5. 热数据与冷数据分区避免了大量冷数据在一次命中后就移动到链表头部造成缓存误差的情况出现

    10.Redis的持久化策略(存盘机制)

    RDB:

    说明:类似于快照。在某个时间点,将 Redis 在内存中的数据库状态(数据库的键值对等信息)保存到磁盘里面。RDB 持久化功能生成的 RDB 文件是经过压缩的二进制文件。

  6. RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合用于做备份。 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

  7. RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。
  8. RDB 可以最大化 redis 的性能。父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
  9. RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

    AOF:

    说明:保存 Redis 服务器所执行的所有写操作命令来记录数据库状态,并在服务器启动时,通过重新执行这些命令来还原数据集。

  10. AOF 比 RDB可靠。你可以设置不同的 fsync 策略:no、everysec 和 always。默认是 everysec,在这种配置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。

  11. AOF文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
  12. 当 AOF文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
  13. AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

    混合持久化

    说明:混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。

  14. 混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件,然后再将 AOF 重写缓冲区(aof_rewrite_buf_blocks)的增量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

    11.Redis集群模式

    主从机制

    主服务器,从服务器。通过socket进行通信

    哨兵机制

    由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

    集群模式

    哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。
    为了解决哨兵模式存在的问题,集群模式应运而生。在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。
    集群模式具备的特点如下:

  15. 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384 个槽,每个节点负责处理部分槽。

  16. 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383。
  17. 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
  18. 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,下面的架构图就是这样设计的。
  19. 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

    12.巨量Key问题

  20. keys java 命令,该命令性能很好,但是在数据量特别大的时候会有性能问题

  21. scan 0 MATCH java 命令,基于游标的迭代器,更好的选择
  22. SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
  23. 当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

    13.缓存穿透

    访问一个不存在于缓存和DB的Key,请求会直接打到数据库上。因为数据库中也不存在,因此也不会写入缓存。导致请求每次都会穿透缓存。
    解决方案:

  24. 接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

  25. 缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。
  26. 布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。

    14.缓存击穿

    在热点数据失效的一瞬间,大量请求穿过缓存层请求到数据库,导致数据库压力过大。
    解决方案:

  27. 互斥锁:访问请求资源时,每次只有一个请求能访问到对应资源。其他请求阻塞直到获取到锁。再请求资源返回

  28. 热点数据不过期:直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。可能会有脏数据出现

    15.缓存雪崩

    大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。
    解决方案:

  29. 过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。

  30. 热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
  31. 加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。