Redis

Redis

非关系型数据库

四大非关系型数据库

  • 键值数据库-Redis
  • 列族数据库-Hive
  • 文档数据库-MongoDB
  • 图数据库-Neo4J

CAP理论

CAP

C一致性

一个读操作总是能读到之前完成的写操作的结果

A可用性

能够快速获取数据,可以在确定的时间内返回对应的结果

P分区容忍性

部分区域出现故障,其他区域也可以继续运行

CAP只可以满足其中两个,Redis就是实现了CP的代表

BASE

BA基本可用

一部分不可用,但是其他部分还是可用的

S软状态

软状态指数据可以有一段时间不同步,有一定的滞后性

E最终一致性

强一致性

修改后其他数据可以立即读到最新的数据

弱一致性

最终一致性就是弱一致性的一个特例,即最终要保证数据已经改变,数据可以在一段时间内不一样

关系型数据库与非关系型数据库的区别

关系型数据库

  1. 数据之间有关联

  2. 数据存储在磁盘上

  3. 表现为一个一个的表格数据

非关系型数据库

  1. 数据之间没有关联

  2. 数据存储在内存

  3. 表现为key-value键值对

Redis是什么

Remote Dictionary Server远程字典服务

是一个键值对数据库

单线程但是性能很高

作用

  • 负责快速的响应用户的请求(因为是数据库的一层缓存)
  • 防止对数据库的并发冲击

Redis数据结构

数据类型

字符串

底层实现
  • Long类型整数
  • SDS简单动态字符串
    • embstr
    • raw
具体结构
  • 如果你存储的是一个64位的整型数
    采用int型编码,直接用RedisObejct 中的类型指针存放这个值即可,不需要SDS
  • 存储的是一个小于44字节的字符串
    使用embstr编码,SDS直接与RedisObejct在内存上紧挨着
  • 存储的是一个大于44字节的字符串
    使用raw编码,SDS与RedisObejct地址没有关系,通过RedisObejct中的指针指向
实现场景
  • 计数,如点赞数等等
  • 缓存
  • 使用字符串类型的列表或发布/订阅模式可以实现简单的消息队列

列表

底层实现
  • 压缩列表
  • 双向链表
实现场景

作为一个消息队列

哈希

底层实现
  • 压缩列表

  • 哈希表

场景

存储对象的多个字段和值,如用户信息、配置信息

集合

底层实现
  • 压缩数组
  • 哈希表
场景

去重、成员关系判断

有序集合

底层实现
  • 压缩列表
  • 跳表
场景

去重成员关系判断

有序集合

底层实现
  • 压缩列表
  • 跳表
场景

排行榜、范围查询

三种特殊的数据结构

HyperLogLogs

省内存的去统计各种计数,比如注册IP数、每日访问IP数的页面实时UV、在线用户数、共同好友数等

Bitmap

两个状态的,都可以使用Bitmaps,比如登录/未登录,性别男/女

geospatial

底层是Zset,有序列表

推算地理位置的信息,两地之间的距离,方圆几里的人

stream

用于消息队列和发布/订阅模式

数据类型底层的数据结构

SDS

Redis专门实现的一个结构体

有三个字段:len、free、buf[]分别用来标记长度,剩余长度,存储的字符

优点
  • 保证二进制安全,因为C语言使用’\0’来判断字符是否结束的,但是SDS使用len来表示长度的

  • O(1)级别获取字符串长度,因为用len来记录长度

  • 减少字符串的内存重新分配的次数

    • 预先分配+惰性空间释放

      • 空间预先分配
        字符串扩展会扩展的更大

      • 惰性空间释放

        字符串缩小,增大free的值,但不一定会缩小buf数组

压缩列表

什么时候会使用压缩列表?

数据中元素个数小于512

单个元素的大小小于64字节

特点
  • 压缩列表在每个元素的开头使用了一个字节作为标识,用来标识该元素的数据类型。这个类型标识允许压缩列表在解码时正确地将字节序列转换回原始数据类型
  • 一段连续的内存存放数据,类似于C实现的数组,但是可以存储不同数据类型的数据
  • 有一个字段保存了偏移长度,很节省空间,但是遍历的时间复杂度未O(n)

双向链表

与常见的双向链表一样

存储了列表长度,保证O(1)级别获取长度

哈希表

使用高性能的murmurhash算法
解决hash冲突

拉链法

扩容缩容机制
  • 负载因子=已存储的元素个数/哈希表大小
  • >1扩容 扩容为原来大小的两倍
  • <0.1缩容 缩容为当前数据量的两倍

整数数组

Set什么时候使用有序数组?
  • 存储的数据都是整数
  • 存储的元素个数不超过512个
优点
  • 没有内存碎片
  • 节省空间

跳表

特点
  • 最底层是一个链表,然后给最底层节点隔一个建立一个索引,然后再给索引建立索引,就构成的跳表
  • 查询速度为O(logn)
为什么使用跳表而不是红黑树?
  • 红黑树不适合范围查询

  • 跳表的实现比较简单

Redis底层数据结构

RedisObejct 是记录数据是什么类型的结构体,由两部分组成,元数据与类型指针

Redis最外层有两个全局哈希表

image-20240317144215207

解决碰撞的方式

拉链法

如果拉链太长怎么办?

  • Redis进行rehash:使用两个哈希表交替,一个扩容,一个继续提供服务
  • 渐进式rehash
    • 如果单纯使用两个哈希表,哈希表1负责扩容、哈希表2负责提供服务,那么扩容的哈希表1在copy哈希表2时,哈希表2需要进入阻塞状态,导致不能进行服务
    • 因此使用渐进式rehash过程,即在每次的请求携带一点,而不是统一的传输

为什么要有两个全局哈希表

  1. 因为扩容的时候可以进行渐进式扩容,可以每次的请求携带一些桶然后慢慢扩容
  2. 扩容的时候能保证提供服务

持久化操作

AOF

Append On File

特点

不同于传统的WAL,AOF是写后日志,会先执行操作,然后再记录日志

优点:
  • 不需要检查语法是否错误
  • 可以避免记录日志影响当前操作的进度
缺点:
  • 影响后续操作的速度
  • 如果执行完操作宕机了,那么日志就没有记录,使用AOF恢复的时候会丢失数据

写回策略

  • always 每个操作都写回磁盘
  • Everysec 每秒写回一次磁盘
  • No 写回的时机由OS决定

AOF重写机制

  • AOF越来越大,就会触发AOF重写机制,即只记录可以出现最后结果的操作
  • 重写过程由另一个线程来实现
  • 主线程会fork出一个子线程去执行重写操作(注意fork的过程是阻塞的)

RDB

RedisDataBase

特点

  • 一定之间间隔,检测key的变化情况,然后持久化存储
  • 主线程可以自己save,也可以创建一个线程bgsave
  • fork过程阻塞

复制过程中可以继续提供读写服务吗?

可以使用cow的思想,对某部分写操作是,对这部分copy一部分

区别

  • AOF记录的是操作,所以每次记录的内容很简单
  • RDB存储的是数据,恢复会很快,但是存储的容量很大

Redis4.0混合使用AOF与RDB:既保证安全也保证速度

各自适用什么场景?

  • RDB适用于数据库备份与迁移
  • AOF适用于数据可靠性要求高、实时性高的场景

缓存机制

Redis缓存类型

只读缓存

只负责读请求,对于写请求直接操作数据库

读写缓存

读写请求都负责,写请求会之后同步在数据库

同步策略:
  • 同步直写 请求同时发送给缓存与数据库,等待两者都操作完成后,才将结果返回
  • 异步写回 缓存不等待数据库就直接返回,数据只有被淘汰的时候才会写回磁盘

缓存一致性问题

什么时候缓存一致?

  • 缓存中没有数据,数据库的数据是最新的
  • 缓存中有数据,且和数据库数据相同

如何解决缓存不一致?

对于读写缓存

使用同步只写

对于只读缓存
  • 新增操作
    新增操作直接操作数据库,此时缓存内没有数据,数据库有,符合缓存一致

  • 删改操作:对于删改操作,直接修改数据库,此时数据库为最新数据,而缓存中为旧数据,出现了缓存一致性问题

    删除缓存数据,修改数据库内的数据

注:主要是删除或修改操作会出现缓存不一致问题

只读删改操作怎么处理?

方法
  1. 先删除缓存数据
  2. 修改数据库数据
存在的问题
问题一:任意一步出现失败
  • 1成功,2失败:此时缓存中没有数据,数据库中的值还是旧值;存在数据不一致问题
  • 1失败,2成功:缓存为旧值数据库为新值
问题二:存在并发请求

当前有很多请求进入,你还没来得及删除缓存,另一边就直接把旧数据读走了

解决措施

解决问题一:重试机制

把要更改或删除的数据保存在一个消息队列中,如果删除缓存或修改数据任意一步失败,那么就去消息队列中取数据重新执行

如果达到一定的次数,还是失败,那么就向业务层报错

解决问题二:延迟双删
  1. A线程删除缓存
  2. A线程修改数据库
  3. A线程睡一会儿:保证这段时间B线程能修改完数据库,并将最新的数据搬到redis
  4. A线程再删除一遍缓存

缓存淘汰策略

Redis4.0有八种淘汰策略

不进行数据淘汰

noeviction(默认),缓存存满,将不会接受新的请求

进行数据淘汰策略

有过期时间的数据内淘汰
  • volatile-lru

    最近最少使用

    • 当内存限制达到时,根据最近最少使用(LRU)原则删除键
    • 所有导致键数量增加的操作(如SET、LPUSH等)都会返回一个错误
  • volatile-lfu

    首先根据访问次数进行淘汰,如果访问次数相同,再根据时间淘汰

    • 当内存限制达到时,根据使用频率计算
  • volatile-ttl

    淘汰过期时间剩余最短的

  • volatile-random

    设置了过期时间的随机淘汰

所有数据中进行淘汰
  • allkeys-lru

  • allkeys-lfu

  • allkeys-random

LRU(Least Recently Used)

RedisObject中有一个字段,存放了最近一次的访问时间戳

LRU就是根据这个字段

LRU过程

  1. 首先随机选择N个数据,组成一个候选淘汰集合,然后将最早访问的数据淘汰
  2. 然后每一次淘汰,都会挑选新的数据进入候选淘汰集合中(如何挑选:选择那些字段值比候选集合最小的数据还小的数据)

存在的问题

无法解决缓存污染问题

  • 即存放了很多可能长久不会访问的数据
  • 比如业务层遍历了很多数据一次,此时LRU算法根本判断不出来这些数据中哪些数据访问的频次会高一点

LFU(Least Frequently Used)

  1. 首先根据访问次数进行淘汰
  2. 如果访问次数相同,再依据时间戳淘汰

其他缓存问题

缓存雪崩

什么是缓存雪崩?

即缓存无法解决大量的数据访问,导致更多的数据访问了数据库,数据库压力增大

原因两个

  1. 可能是相同过期时间的数据同时过期,导致缓存中数据都没有,都去访问数据库
  2. 缓存主库宕机

解决措施

  1. 情况一
    • 给数据设置不同的过期时间:比如一个固定值+一个随机值
    • 服务降级
      • 即缓存先去满足核心业务
      • 非核心业务先返回预定值或是null
  2. 情况二
    • 服务熔断 缓存暂时停止使用,防止造成连锁反应导致其他的服务也失败
    • 请求限流机制 前端进行限流,控制每秒进入的请求数QPS
    • 主从替换 使用从库代替主库,继续进行服务

缓存击穿

什么是缓存击穿?

访问非常热的请求,无法处理传到了数据库

出现的原因

往往是给热请求数据设置了过期时间

如何解决?

对于非常热的请求不要设置缓存时间

缓存穿透

什么是缓存穿透?

访问缓存与数据库根本不存在的数据

出现的原因

  • 业务层误操作
  • 被故意访问不存在的数据

解决措施

  • 缓存空值或缺省值
  • 布隆过滤器
  • 前端控制

布隆过滤器

多个hash函数+一个bit(位)数组

存储一个数据就将其多次hash后,将对应的位置设为1

检查元素是否存在时,同样使用这些哈希函数,如果所有对应位置都是1,则认为元素可能存在,但凡有一个不是1就一定不存在

Redis主从机制

为什么要建立主从机制

RDB、AOF的fork阶段会阻塞,让Redis停止服务,如果Redis越来越大,那么fork所用的时间会越来越长

确保主库宕机后,从库还可以继续提供服务

结构

  • 主库 负责读与写服务
  • 从库 只负责读服务和持久化操作
  • 哨兵
    • 监听 监听主库从库状态
    • 选择主库 从从库中选择备用主库
    • 通过发布订阅系统发送通知

第一次主从建立联系

  1. 从库发起请求连接
  2. 主库同意其连接,并发送RDB文件给从库
  3. 以后每次都发的是增量的修改

发布订阅系统

主库有发布订阅系统,订阅统一频道的节点可以发布与获取消息

主从库之间如何判断复制的延迟?

通过缓存repl_backlog_buffer,这是一个环状缓存,里面有主库从库的当前指针,通过判断主从库之间的指针位置就能知道当前从库差主库多少数据

哨兵集群的建立过程

  1. 哨兵本质就是一个运行在特殊模式下的Redis进程
  2. 哨兵创建后,互相之间是不知道彼此的存在的
  3. 哨兵与主库建立连接后,可以利用主库的发布订阅系统,将自己的Ip传出去,这样其他哨兵就可以通过订阅频道来获取其IP,最终哨兵之间也就能联系了

哨兵怎么监听主从库是否正常?

  1. 哨兵会按时给主从库发送ping请求,并要求其在一定时间内回应
  2. 如果从库没有回应,就标记为客观下线
  3. 如果主库没有相响应,就标记为主观下线,然后哨兵会给其他哨兵发送信息询问是否认为主库下线,然后进行投票,多数赢少数认为主库客观下线

哨兵怎么选择新的主库?

  1. 筛选 选择那些网络好的从库,确保他们是在线的
  2. 评分 根据三方面来打分:从库优先级、从库复制进度、从库ID号
    • 一般会给内存大的从库设置一个较大的优先级
    • 与主库复制进度差距最小的得分高
    • 当前面两个都一样的时候,会选择ID小的作为主库

哨兵怎么通知其他设备切换了新主库?

  1. 通过Redis提供的发布订阅系统
  2. 哨兵只要和主库建立连接,就可以发布消息和订阅消息了,只需要把消息发布一下,从库就会收到信息
  3. 哨兵向主库发起INFO命令,然后主库就会返回一个从库列表,然后哨兵就知道了从库的IP

Redission锁

在分布式系统中,会涉及到多个节点操作同一个资源的问题,这时候就需要锁

Redission是一个在Java中,用于操作Redis的框架,提供了一系列API来处理分布式场景下的各种需求。

特点

  1. 简单易用

    Redission提供了简单直观的API获取和释放分布式锁。
    在Java代码中,只需要创建一个Redission客户端实例,就可以操作获取和释放锁的方法。

  2. 可重入性
    Redission锁是可重入的,意味着如果一个线程已经获取了锁,那么可以在不释放锁的情况下再次获取锁

  3. 锁自动续期

    当一个线程获取到Redission分布式锁后,如果该线程执行时间长,超过了最初设定的锁过期时间,Redission会自动为锁续期

实现原理

  1. 获取锁

    获取锁时,会向Redis发送SETNX命令或其他类似原子操作命令来尝试获取锁。如果获得成功,会在Redis中设置锁的相关信息,包括锁的持有者、锁的过期时间等等。同时:后台会启动一个任务,检查锁是否要续期

  2. 锁的可重入实现
    对于可重入性,Redis会根据一个线程重复获取锁的次数,进行记录。当一个线程重复获取锁时,会增加锁的记录次数。释放锁时,这个数字减为0才会真正释放。

  3. 释放锁
    当线程完成任务要释放锁时,Redission会向Redis发送释放锁相关键值对的命令。释放锁之前,会检查当前线程是否为锁的持有者,如果是才会执行释放操作。确保了获取到锁的线程才能释放锁,避免了锁被错误释放的操作。

其他问题

存放数字最好不要使用String,而去使用hashmap

  1. 因为String浪费空间很严重,假如去存放一个数字,String的RedisObejct占16字节,它的DictEntry占24字节,这就使用了40字节,而你有效的存储只用了8字节
  2. 如果用hashmap,在存储数字时,它会使用压缩列表的结构,可以存放很多很多数组,浪费空间还很小!

Redis是单线程的吗?

Redis严格意义上来说并不是单线程的,所谓的单线程指的是从网络IO到键值对获取这个过程是单线程的

Redis为什么这么快?

  1. 单线程,无需加锁,十分快速

  2. 底层数据结构优化的很好

    • 跳表
    • 两个全局哈希表,交替扩容,渐进式rehas
  3. IO多路复用

    • 采用linux的epoll模型

    • epoll模型相较于select、poll

      • 不受文件描述符的限制

      • 支持水平触发和边缘触发两种方式
        水平触发(select只提供这种) 持续通知 没有被处理会一直通知

        边缘触发 通知一次 只会通知一次

    • 只支持linux操作系统

    • fd每次需要遍历所有的fd,epoll只需要遍历那些就绪的fd