Q1:Redis 单线程为什么这么快?

  1. 纯内存操作
  2. 核心是基于非阻塞的 io 多路复用机制
  3. 单线程反而避免了多线程频繁切换上下文带来的性能问题

Q2:分布式锁的底层是怎么实现的?

  1. setnx+setex:存在设置超时时间失败的情况,导致死锁
  2. set(key,value,nx,ex):将setnx + setex 编程原子操作
  3. 问题:
    1. 任务超时,锁自动释放导致并发问题。(使用redisson 解决,看门狗-后台线程定时检查 是否需要 自动续期)
    2. 加锁和释放锁不是同一个线程(释放的不是自己线程的锁):在value中存入uuid,删除锁的时候判断该标标识(使用lua脚本保证原子操作)
    3. 不可重入,使用redisson解决(实现机制类似于AQS,计数)
    4. 异步复制可能造成锁丢失,使用redLock 解决
      1. 顺序向五个节点请求加锁
      2. 根据一定的超时时间推断是不是跳过该节点
      3. 三个节点加锁成功并且花费时间小于锁的有效期
      4. 认定加锁成功

补充:

  1. 分布式锁还可以使用mysql的主键机制、排他锁机制、乐观锁机制实现
  2. 可以使用zookeeper来实现

    1. 半数存活机制(补充)
      1. image.png
    2. Leader/Follow
      1. leader 负责读写
      2. follow 负责读
    3. Zookeeper 分布式锁的实现原理
      1. 创建临时有序节点
      2. 按照序号获取所
      3. 任务结束之后释放锁
      4. image.png

        Q3:Redis 过期键的删除策略?

  3. 惰性过期:只有当访问一个key 时,才会判断key是否已过期,过期则清除。该策略可以最大化的节省cpu资源,却对内存非常不友好。极端情况可能出现大量的过期对key没有再次被访问,从而不会清除,占用大量对内存。

  4. 定时过期:每个一定时间,会扫描expires字典中一定数量对key,并清除其中已过期的key。该策略时前两者的一种这种方案,通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果。
    1. expires字典保存了所有设置了过期时间的key 的过期时间数据。其中,key是指向键空间中的某个键的指针,value 是该键的 毫秒精读的 UNIX 时间戳表示的过期时间,键空间是redis集群中的所有键。
    2. 随机抽取20个key检测是否需要删除,如果超过25%的key过期,则立即进行下一次检查。

Q4:Redis 的持久化机制?

  1. RDB(Redis DataBase) 将某一个时刻的内存快照以二进制的形式写入磁盘。
    1. 手动触发:
      1. save命令:使redis出于阻塞状态,直到rdb完成才会响应其他客户端的操作。(生产环境慎用)
      2. bgsave命令:fork出一个子进程来进行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了。
    2. 自动触发:
      1. save m n: 在 m秒内,有n个键发生改变,则自动触发持久化,通过bgsave执行,只要满足其一就会触发,配置文件中有默认配置。
      2. flushall:用于清空Redis数据库,flushdb清空当前的redis所在的数据库(默认是0号数据库),会清空rdb文件,同时生成dump.rdb 内容为空。
      3. 主从同步:全量的主从同步时 会触发bgsave命令,生成 rdb 发送给从节点。
    3. 优点
      1. 整个redis 数据库只包含一个dump.rdb,方便持久化
      2. 容灾性好,方便备份
      3. 性能最大化,fork子进程用来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独的子进程来进行持久化,主进程不会进行任何的IO操作,保证了redis 的高性能。
    4. 缺点:
      1. 数据的安全性要求比较低。RDB是隔一段时间进行一次持久化的,如果期间redis发生故障,会造成数据丢失,所以这种方式适合数据要求不严谨的时候。
      2. 由于RDB 是通过fork 子进程进行持久化工作的,因此 当数据比较大时,可能会导致整个服务暂停几百毫秒甚至1秒钟(会占用cpu)
  2. AOF (Apeng Only File) 以日志的形式记录服务器所处理的每一个写、删操作,查询的操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调用操作系统命令进行刷盘
    1. 所有的写命令会追加到AOF 缓冲中
    2. AOF 缓冲区根据对应的策略向硬盘进行同步操作
    3. 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
    4. 当Redis 重启时,可以加载AOF文件进行数据恢复

同步策略:

  1. 每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒之内的修改记录会丢失
  2. 每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢失一条
  3. 不同步:由操作系统控制,可能丢失比较多的数据

优点

  1. 数据安全
  2. 通过append 模式写文件,及时中途服务器当即也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性的问题
  3. AOF 机制的rewrite 模式,定期对AOF文件进行重写,达到压缩的目的

缺点

  1. AOF 文件比RDB文件大,且恢复速度比较慢
  2. 数据集比较大时,比RDB 启动效率低
  3. 运行效率没有RDB高

Q5:Redis 集群方案?

主从:

  1. 哨兵模式(不能很好的解决扩容的问题):
    1. Sentinel,哨兵是redis集群中非常重要的组件,具备以下功能:
      1. 集群监控:负责监控redis master 和 slave 进程是否正常工作
      2. 消息通知:如果某个redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员
      3. 故障转移:如果master node 挂掉了,会自动转移到slave node 上
      4. 配置中心:如果故障转移发生了,通知client 客户端新的master 地址
    2. 基于哨兵实现redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,相互协同工作。
      1. 故障转移时,判断一个master node 是否宕机了,需要大部分哨兵都同意才行,涉及到了分布式选举
      2. 及时部分哨兵节点挂掉了,哨兵集群还是可以正常工作的
      3. 哨兵通常需要3个实例,来保证自己的健壮性
      4. 哨兵+redis 主从的部署架构,是不保证数据零丢失的,只能保证redis 集群的高可用性
      5. 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境都进行充足的测试和演练
  2. Redis Cluster(多主多从,用的比较多) 是一种服务端Sharding 技术,采用槽的概念,一共分成16384个槽将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
    1. 方案说明
      1. 通过哈希的方式,将数据分片,每个节点均分存储一定的哈希槽
      2. 每份数据分片会存储在多个互为主从的节点上
      3. 数据写入先写主节点,在同步到从节点
      4. 同意分片多节点间的数据不保持强一致性
      5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis 会返回转向指令,指向正确的节点
      6. 扩容时需要把旧的节点数据迁移一部分到新的节点
    2. 在redis cluster 架构下,每个redis 要开放两个端口,,比如一个是6379,另一个就是加1w的端口号,比如16379
    3. 16379 端口号是用来进行节点间通信的,也是cluster bus的通信。用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
    4. 优点
      1. 无中心架构,支持动态扩容,对业务透明
      2. 具备sentinel 的监控和自动Failover(故障转移能力)
      3. 客户端不需要链接集群所有节点,连接集群中任何一个可用节点即可
      4. 高性能,客户端直连redis服务,免去了proxy代理的损耗
    5. 缺点:
      1. 运维复杂,数据迁移需要人工干预
      2. 只能使用0号数据库
      3. 不支持批量操作(pipeline管道操作)
      4. 分布式逻辑和存储模块耦合
  3. Redis Sharding 是Redis Cluster 之前,普遍使用的Redis 实例集群方法。主要采用哈希算法将Redis 数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis 客户端驱动redis,支持redis sharding功能,即Shardedjedis 以及结缓存池的shardedjedisPool
    1. 优点:
      1. 优势非常简单,服务端的redis实例彼此独立,相互无关联,每个redis实例像单服务一样运行,非常容易线性扩展,系统的灵活性很强
    2. 缺点:
      1. sharding在客户端,对运维带来挑战
      2. 不支持动态增删节点,服务端redis实例变化时,客户端都需要更新调整。连接不能共享。

Q5:Redis 数据结构和应用场景?

  1. string
    1. 最简单的数据缓存,也可以缓存某个json格式的字符串、json等
    2. redis的分布式所功能利用了这种数据结构
    3. 计数器、全局session等
  2. hash
    1. key-value 结构,更适合存储对象
    2. 购物车等 一对多等场景
  3. list
    1. 当作栈
    2. 当作队列
    3. 可以用来缓存类似微信公众号、微博等消息流数据
    4. 双向链表结构
    5. 关注列表、消息队列等应用
    6. 常用命令 lpush \lpop \rpush…\ lrange 0 -1 (获取全部数据)
  4. set
    1. 和列表类似,但不可重复
      1. 存储大量数据,并且查询速度快
      2. 底层其实就是hash,value 为空
      3. hash结构,没有索引 sadd , smembers ,srem
    2. 进行交集、并集、差集操作
      1. sinter key1 [key2] 交集 、 sinterstore destination key1 [key2] 存储到指定集合
      2. sunion key1 [key2] 并集 、sunionstore destination key1 [key2] (如 sunionstore u3 u1 u2)
      3. sdiff key1 [key2] 差集 、 sdiffstore destination key1 [key2]
    3. 共同关注、朋友圈点赞等功能
    4. 网站访问数据的统计等
    5. 黑白名单等
  5. zset
    1. 有序集合,可以用来实现排行榜等功能
    2. 在set的基础上,增加了 score ,用于保证有序性
      1. image.png
    3. 常用命令:
      1. zadd key score1 member1 [score2 member2] 添加数据
      2. zrange key start stop [withscores] 获取全部数据
        1. (zrange kaoshi 0 -1 不包括score值)
        2. (zrange kaoshi 0 -1 with scores 包含score 值)
      3. zrevrange key start stop [withscores] 获取全部数据
        1. 反相排序
      4. zrem key member [member…..]
      5. zrangebyscore key min max [withscores] [limit] 根据score 的范围查询
        1. zrangebyscore kaoshi 50 100 limit 3
        2. zrevrangebyscore kaoshi 50 100
    4. 带有权重的任务/消息队列
      1. 利用score来记录权重即可
  6. geo
    1. 存储地理位置
    2. 借助sorted set 实现,通过zset的score进行排序可以得到坐标附近的其他元素,通过将score还原成坐标值就可以得到其他元素的原始坐标
    3. 附近的人等功能
  7. hyperloglog
    1. 基数统计,占用空间极小
    2. 例如网站等访问次数等
    3. 统计不重复数据
  8. bitmap(布隆过滤器)
  9. streams:
    1. 消息队列
    2. 订阅和发布

Q6:Redis 主从复制的核心原理?

通过执行slaveof 命令 ,让一个服务去复制另一个服务的数据。主数据库可以进行读写操作,当写操作导致数据变化时,会将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。
全量复制:

  1. 主节点通过bgsave命令fork子进程进行RDB持久化,该过程非常消耗cpu、内存、磁盘io
  2. 主节点通过网络将RDB文件发送给从节点,对主从节点对贷款都会带啦很大对损耗
  3. 从节点清空老数据,载入RDB文件的过程是阻塞的,无法响应客户端的命令
  4. 如果从节点执行bgrewriteaof,也会带来额外的消耗

部分复制:

  1. 复制 偏移量:执行复制方法,主从节点分别维护一个复制偏移量 offset (就是记录下从什么地方开始同步)
  2. 复制 积压缓冲区:主节点内部维护了一个固定长度的,先进先出的队列作为 积压缓冲区,当主从的offset 的差距超过缓冲区的长度时,将无法执行部分复制,只能执行 全量复制
  3. 服务器运行ID(ruuid):每一个redis节点都有一个ID,运行id由节点在启动时自动生成,主节点会将自己的运行id发送给从节点,从节点会讲主节点的运行id存起来。从节点redis断开重连的时候,根据id来判断同步的进度:
    1. 如果从节点保存的ruuid 和 主节点的ruuid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(能不能使用部分复制还要看offset 和 复制积压缓冲区的情况)
    2. 如果从节点保存的 ruuid 与 主节点现在的 ruuid 不同,说明从节点在短线前同步的redis节点并不是当前的主节点,只能进行全量复制

image.png

Q7:布隆过滤器原理及优缺点?

原理:

  1. 添加数据时:
    1. 将数据进行hash 得到hash值,对应到bit 位,将bit改为1,hash 函数定义多个,则一个数据添加会将多个 bit 改为1,多个hash函数的目的是为了减少hash碰撞的概率
  2. 查询数据:
    1. hash函数计算得到hash值,对应到bit中,如果有一个为0,则说明数据不在bit中,如果都为1,则该数据可能在bit中

优点

  1. 占用内存小
  2. 增加和查询元素的时间复杂度为O(k),K为哈希函数的个数,与数据量的大小无关
  3. 哈希函数相互之间没有关系
  4. 布隆过滤器不需要存储元素本身,保密性高
  5. 数据量大时,布隆过滤器可以表示全集
  6. 可以使用布隆过滤器进行交、并、差运算

缺点

  1. 误判率 因为hash碰撞,不能绝对判断元素在集合中
  2. 不能获取元素本身
  3. 一般情况下无法从布隆过滤器中删除元素

Q8:常见的缓存淘汰的算法?

  1. FIFO
    1. 先进先出
  2. LRU
    1. 最近最少使用(使用hashmap 和 链表配合,保证时间复杂度为O(1)) ```java import java.util.HashMap; import java.util.Map;

/**

  • 思路:
    1. hash 的时间复杂度为o1 ,所以最终使用肯定hash跑不掉
    1. 最久不实用的 放到 链表的最后面,时间复杂度也是o1
    1. 类似于手写一个简单的linkedhashmap */

class LRUCache {

  1. /**
  2. * 用来定位节点,hashmap 的时间复杂度为 O(1)
  3. */
  4. Map<Integer, Node> map;
  5. int capacity;
  6. DoubleLinkedList cache;
  7. public LRUCache(int capacity) {
  8. this.map = new HashMap<>(capacity);
  9. this.capacity = capacity;
  10. // 头尾 两个节点作为哨兵,不使用
  11. cache = new DoubleLinkedList();
  12. }
  13. public int get(int key) {
  14. // 如果不存在返回 -1
  15. if (!map.containsKey(key)) {
  16. return -1;
  17. }
  18. // 如果存在 放到链表的头上去
  19. Node node = map.get(key);
  20. cache.del(node);
  21. cache.addFirst(node);
  22. return node.value;
  23. }
  24. public void put(int key, int value) {
  25. Node node = new Node(key, value);
  26. // 不包含 说明第一次插入这个节点
  27. if (!map.containsKey(key)) {
  28. // 判断是不是满了
  29. if (map.size() == capacity) {
  30. // 满了 删掉最后一个元素
  31. int i = cache.delTail();
  32. map.remove(i);
  33. }
  34. } else {
  35. // 已经包含 说明是在更新
  36. cache.del(map.get(key));
  37. }
  38. // 加入map
  39. map.put(key, node);
  40. // 再加上这个元素
  41. cache.addFirst(node);
  42. }
  43. /**
  44. * 双链表
  45. */
  46. static class DoubleLinkedList {
  47. // 头节点 最经常使用的
  48. Node head;
  49. // 尾节点 最不经常使用的
  50. Node tail;
  51. public DoubleLinkedList() {
  52. head = new Node();
  53. tail = new Node();
  54. // 头尾相连
  55. head.next = tail;
  56. tail.prev = head;
  57. }
  58. /**
  59. * 添加头节点
  60. * 只需要改变指针即可
  61. */
  62. void addFirst(Node node) {
  63. Node next = head.next;
  64. next.prev = node;
  65. node.next = next;
  66. head.next = node;
  67. node.prev = head;
  68. }
  69. /**
  70. * 删除某个节点
  71. */
  72. int del(Node node) {
  73. // 包含元素 则改变指针指向
  74. Node prev = node.prev;
  75. Node next = node.next;
  76. prev.next = next;
  77. next.prev = prev;
  78. // 删除的是哪个元素,map删除时需要
  79. return node.key;
  80. }
  81. /**
  82. * 删掉最后一个元素
  83. */
  84. public int delTail() {
  85. // 如果没有元素 直接返回-1
  86. if (head.next == tail) {
  87. return -1;
  88. }
  89. return del(tail.prev);
  90. }
  91. }
  92. /**
  93. * 节点
  94. */
  95. static class Node {
  96. Node prev;
  97. Node next;
  98. int key;
  99. int value;
  100. public Node() {
  101. }
  102. public Node(int key, int value) {
  103. this.key = key;
  104. this.value = value;
  105. }
  106. }

}

/**

  • Your LRUCache object will be instantiated and called as such:
  • LRUCache obj = new LRUCache(capacity);
  • int param_1 = obj.get(key);
  • obj.put(key,value); */ ```
  1. LFU
    1. 最不经常使用

Q9:分布式系统中常用的缓存方案有哪些(用在什么地方)?

  1. 客户端缓存(localStorage/sessionStorage)
  2. CDN缓存
  3. nginx缓存:静态资源
  4. 服务端缓存:本地缓存,外部缓存
  5. 数据库缓存:持久层缓存(mybatis\hibernate 多级缓存)、mysql查询缓存
  6. 操作系统缓存:Page Cache、buffer cache

Q10:缓存穿透、缓存击穿、缓存雪崩解决?

  1. 缓存穿透
    1. 缓存中查不到,db中也查不到
      1. 被恶意利用,频繁查询不存在的数据,造成数据库压力过大
    2. 解决方案:
      1. 参数合法性校验
      2. 数据库中不存在的结果写入缓存(要注意redis中过多的无效key,所以有效期短一点)
      3. 布隆过滤器(快速判断数据是否存在)见q7
  2. 缓存击穿
    1. 缓存中没有,数据库中有。
      1. 一般是出现在key过期的时候,重新写入缓存需要时间,在高并发的情况下,过多的请求瞬间就到了db上
    2. 解决方案:
      1. 热点数据永不过期(另启线程定期更新同步这些数据)
      2. 加载db的时候,防止并发(双重检查锁)
  3. 缓存雪崩
    1. 缓存大面积过期,导致请求大量到达db (类似缓存击穿,不过是大面积)
    2. 解决方案:
      1. 把缓存有效时间分散开,防止缓存同时过期

Q11:Redis 事务机制?

  1. 事务开始:
    1. multi命令,标志一个事务的开始
    2. watch命令:
      1. 是一个乐观锁(CAS)
      2. 监控一个或者多个key,一旦其中有一个改动,之后的事务就不会执行
      3. 持续到执行exec
  2. 命令入队:
    1. 当客户端开启事务后,服务端会将命令(除了 exec\discard\watch\multi)放入一个事务队列,而不会立即执行
    2. 首先服务端会检查命令的正确性,如果不正确,则返回错误信息并回滚
    3. 如果命令没有问题,运行时出错,则不会回滚
    4. 事务按照fifo的方式保存
  3. 事务执行
    1. 客户端发送exec命令,服务器执行事务

Q12:Redis 和 Mysql 保持数据一致性(只能保证最终一致性)?

  1. 先更新db,再更新缓存
    1. 存在并发问题
  2. 先删除缓存,在更新db
    1. 存在脏数据的问题
    2. a线程删除缓存,b线程更新db,a线程更新db,本来应该a先更新
  3. 先删除缓存,再更新db,在删除缓存(延时双删)
    1. 写操作频繁,仍然有脏数据的问题(并发比较高的时候)
  4. 先写数据库,再删除缓存
    1. 问题:删除缓存可能失败
    2. 给缓存设置过期时间 (过期时间内,缓存不会更新)
    3. 引入mq(两个消费者,一个操作db,一个操作redis,保证原子性)
  5. 热点数据永不过期,后台线程扫描key,对于过期的key进行删除
  6. 加锁(效率低)


Q13:Redis 的操作命令是原子性的吗?

  1. redis对命令的操作是单线程的
  2. 多条命令不一定是原子性,如果想让多条命令同样是原子性,需要使用redis的