概述

  • 非关系型数据库
  • 基于内存
  • Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

数据结构

  • String(字符串)
    • 简单使用举例: set key value、get key等
    • 应用场景:共享session、分布式锁,计数器、限流。
    • C语言的字符串是char[]实现的,而Redis使用SDS(simple dynamic string) 封装

image.png

  • Hash(哈希)
    • 简介:在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构
    • 简单使用举例:hset key field value 、hget key field
  • List(列表)
    • 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。
    • 简单实用举例: lpush key value [value …] 、lrange key start end
    • 应用场景: 消息队列,文章列表,
      • lpush+lpop=Stack(栈)
      • lpush+rpop=Queue(队列)
      • lpsh+ltrim=Capped Collection(有限集合)
      • lpush+brpop=Message Queue(消息队列)
  • Set(集合)
    • 简单使用举例:sadd key element [element …]、smembers key
    • 注意点:smembers和lrange、hgetall都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以使用sscan来完成
    • 应用场景: 用户标签,生成随机数抽奖、社交需求。
  • zset(有序集合)
    • 简介:已排序的字符串集合,同时元素不能重复
    • 简单格式举例:zadd key score member [score member …],zrank key member
    • 底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
  • Geospatial
  • Hyperloglog
  • Bitmap

Redis 为什么快

  • 基于内存实现:内存读写是比在磁盘快很多的,省去磁盘I/O的消耗。
  • 高效的数据结构

image.png

  • SDS简单动态字符串
    • 字符串长度处理
    • 空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
    • 惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配
    • 二进制安全:Redis可以存储一些二进制数据,在C语言中字符串遇到’\0’会结束,而 SDS中标志字符串结束的是len属性。
  • 合理的数据编码:Redis会根据具体类型使用情况来转变编码格式
  • 合理的线程模型
    • I/O 多路复用

image.png

  1. - 单线程模型
  2. - 避免了CPU不必要的上下文切换和竞争锁的消耗
  3. - Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程
  • 虚拟内存机制
    • Redis直接自己构建了VM机制
    • 虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,避免因为内存不足而造成访问速度下降

缓存击穿、缓存穿透、缓存雪崩

  • 缓存穿透
    • 缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库
    • 解决办法
      • 拦截非法请求
      • 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值
      • 使用布隆过滤器快速判断数据是否存在
        • 布隆过滤器原理:它由初始值为0的位图数组和N个哈希函数组成。一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该key存在。
  • 缓存雪崩
    • 指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库(秒杀)
    • 解决办法
      • 均匀设置过期时间
      • Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦
  • 缓存击穿
    • 热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db
    • 解决办法
      • 使用互斥锁方案
      • 永不过期,由程序去控制过期

热Key问题

  • 产生原因
    • 用户消费的数据远大于生产的数据(读多写少)
    • 请求分片集中,超过单Redi服务器的性能
  • 解决办法
    • Redis集群扩容:增加分片副本,均衡读流量;
    • 将热key分散到不同的服务器中;
    • 使用二级缓存,即JVM本地缓存,减少Redis的读请求

Redis 过期策略和内存淘汰策略

  • Redis的过期策略
    • 定时过期:每存一个key都会创建一个定时器,造成CPU占用影响吞吐量
    • 惰性过期:访问的时候发现key过期再清理,可能造成内存浪费
    • 定期过期:定期的去清理过期key
    • Redis中同时使用了惰性过期和定期过期两种过期策略。
  • Redis 内存淘汰策略
    • volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
    • allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
    • volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法(访问缓存的历史频率来淘汰数据)进行删除key。
    • allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰;
    • volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
    • allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
    • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
    • noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

常用应用场景

  • 缓存
    • 使用Redis作为缓存,减小业务数据库的压力
    • Redis相比于memcached,还提供了丰富的数据结构,并且提供RDB和AOF等持久化机制
  • 排行榜
    • 利用zset数据结构来实现排行榜 ```java 用户每天上传视频,获得点赞的排行榜可以这样设计:

用户Jay上传一个视频,获得6个赞,可以酱紫: zadd user:ranking:2021-03-03 Jay 3

过了一段时间,再获得一个赞,可以这样: zincrby user:ranking:2021-03-03 Jay 1

如果某个用户John作弊,需要删除该用户: zrem user:ranking:2021-03-03 John

展示获取赞数最多的3个用户 zrevrangebyrank user:ranking:2021-03-03 0 2 ```

  • 计数器应用
    • 浏览次数等等
  • 共享session
    • 不同系统之间的session共享,可用来实现单点登录
  • 分布式锁
    • 用Redis的setnx来实现分布式的锁
  • 社交网络
  • 消息队列
    • 主要用于业务解耦、流量削峰及异步处理实时性低的业务
    • Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统
  • 位操作
    • 用于数据量上亿的场景下
    • 这里要用到位操作——使用setbit、getbit、bitcount命令
    • 原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id

Redis 的持久化机制

RDB

  • 就是把内存数据以快照的形式保存到磁盘上,在指定时间间隔讲内存中的数据写入磁盘,在指定目录下会生成一个dump.rdb文件
  • 优点:适合大规模的数据恢复场景
  • 缺点:没办法做到实时持久化/秒级持久化、新老版本存在RDB格式兼容问题

AOF

  • 将写操作记录到日志中,redis重启时根据AOF记录的写操作再执行一遍来恢复
  • 优点:数据的一致性和完整性更高
  • 缺点:AOF记录的内容越多,文件越大,数据恢复变慢。

真实开发中一般都是两者相结合来确保数据的完整性

实现Redis的高可用

  • 主从模式
    • 主从复制机制
      • 全量复制:一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制

image.png

     - redis2.8版本之后,已经使用**psync来替代sync**
  - 增量复制:slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发**增量复制**。

image.png

  • 哨兵模式
    • 自动将下线主服务器属下的某个从节点升级为新的主节点
    • 一个哨兵可能会出现单点问题,所以可以用多个哨兵来监控节点,并且哨兵时间也可以互相监控
    • 作用
      • 发送命令,等待Redis服务器(包括主服务器和从服务器)返回监控其运行状态;
      • 哨兵监测到主节点宕机,会自动将从节点切换成主节点,然后通过发布订阅模式通知其他的从节点,修改配置文件,让它们切换主机;
      • 哨兵之间还会相互监控,从而达到高可用。
    • 故障切换的过程
      • 假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
  • Cluster集群模式

    • 实现分布式存储,每台Redis中可以储存不同的数据,来实现扩容
    • 通讯协议:Gossip协议
    • 集群节点通过Gossip协议交换信息,包括:故障、新节点加入、主从节点变更信息、slot信息等等
    • Gossip消息分为4种

      • meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
      • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息
      • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
      • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
    • Hash Slot插槽算法

      • 把数据库分为若干的槽,每个集群节点承担一部分槽的存储,用CRC16算法计算出一个16 位的值,再对16384取模得到具体储存的槽的位置

Redis分布式锁

  • 分布式锁:是控制分布式系统不同进程共同访问共享资源的一种锁的实现
  • set ex px nx(只在键不存在时) + 校验唯一随机值,再删除 + lua脚本
  • 锁过期释放了,业务还没执行完的问题

Redisson

  • 解决过期释放时业务还没执行完

image.png

Redlock算法

  • 为了解决主节点宕机时从节点还没有同步加锁的数据,导致其他线程已经获取到锁的问题
  • 核心思想:搞多个Redis master部署,以保证它们不会同时宕掉
  • 实现步骤:
    • 按顺序向5个master节点请求加锁
    • 根据设置的超时时间来判断,是不是要跳过该master节点。
    • 如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
    • 如果获取锁失败,解锁!

Redis的跳跃表

  • 跳跃表是有序集合zset的底层实现之一
  • 跳跃表支持平均O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
  • 跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
  • 跳跃表就是在链表的基础上,增加多级索引提升查找效率。

MySQL与Redis 如何保证双写一致性

  • 缓存延时双删
    • 就是在数据库删除前后都去删除缓存
    • 缺点:第二次删除可能失败
  • 删除缓存重试机制
    • 写请求更新数据库
    • 缓存因为某些原因,删除失败
    • 把删除失败的key放到消息队列
    • 消费消息队列的消息,获取要删除的key
    • 重试删除缓存操作
  • 读取biglog异步删除缓存
    • 通过数据库的binlog来异步淘汰key。
    • 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
    • 然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

为什么Redis 6.0 之后改多线程呢?

  • redis的性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。
  • 使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

Redis 事务机制

  • 开始事务(MULTI)
  • 命令入队
  • 执行事务(EXEC)、撤销事务(DISCARD ) | EXEC | 执行所有事务块内的命令 | | —- | —- | | DISCARD | 取消事务,放弃执行事务块内的所有命令 | | MULTI | 标记一个事务块的开始 | | UNWATCH | 取消 WATCH 命令对所有 key 的监视。 | | WATCH | 监视key ,如果在事务执行之前,该key 被其他命令所改动,那么事务将被打断。 |

Redis的Hash 冲突怎么办

  • 通过不同的key,计算出一样的哈希值,导致落在同一个哈希桶中
  • 解决办法

    • 转链表
    • Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表

      在生成 RDB期间,Redis 可以同时处理写请求么?

  • 可以的,Redis提供两个指令生成RDB,分别是save和bgsave

    • 如果是save指令,会阻塞,因为是主线程执行的。
    • 如果是bgsave指令,是fork一个子进程来写入RDB文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。

Redis底层,使用的什么协议?

  • RESP
  • RESP主要有实现简单、解析速度快、可读性好等优点。

布隆过滤器

  • 解决缓存穿透
  • 原理:将集合中的元素利用不同的hash函数映射到一个数组的不同位置,然后把待检查的元素也通过不同的hash算法找到数组中的位置,如果改位置为1,着标识待检测元素在集合中
  • 缺点:由于哈希冲突,同一个数组槽可能由不同元素通过hash才得到的,所以容易出现误删
  • 减少这种误差呢?
    • 搞多几个哈希函数映射,降低哈希碰撞的概率
    • 同时增加B数组的bit长度,可以增大hash函数生成的数据的范围,也可以降低哈希碰撞的概率