23Redis

什么是 Redis?

在这里插入图片描述

Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开
发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程
序的数据IO效率。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里
面,通过hash槽的方式实现了数据分片,进一步提升了性能。

Redis 的基本数据结构类型

  • String(字符串)
    String 是 Redis 最基础的数据结构类型,它是二进制安全的,可以存储图片
    或者序列化的对象,值最大存储为 512M
    1. 简单使用举例: set key value、get key等
    2. 应用场景:共享 session、分布式锁,计数器、限流。
    3. 内部编码有 3 种,int(8字节长整型)/embstr(小于等于 39字节字符串)/
      raw(大于 39个字节字符串)

C 语言的字符串是 char[]实现的,而 Redis 使用 SDS(simple dynamic string) 封装

SDS源码

struct sdshdr{
unsigned int len; // 标记buf的长度
unsigned int free; //标记buf中未使用的元素个数
char buf[]; // 存放元素的坑
}

Redis 为什么选择 SDS 结构,而 C 语言原生的 char[]不香吗?

举例其中一点,SDS 中,O(1)时间复杂度,就可以获取字符串长度;而 C 字
符串,需要遍历整个字符串,时间复杂度为 O(n)

  • Hash(哈希)
    • 在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构
    • 简单使用举例:hset key field value、hget key field
    • 内部编码:ziplist(压缩列表) 、hashtable(哈希表)
    • 应用场景:缓存用户信息等。
    • 注意点:如果开发使用 hgetall,哈希元素比较多的话,可能导致 Redis 阻塞,
      可以使用 hscan。而如果只是获取部分 field,建议使用 hmget。
  • List(列表)
    • 简介:列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储
      2^32-1 个元素。
    • 简单实用举例: lpush key value [value …] 、lrange key start end
    • 内部编码:ziplist(压缩列表)、linkedlist(链表)
    • 应用场景: 消息队列,文章列表,
    • list 应用场景参考以下:
      1.lpush+lpop=Stack(栈)
      2. lpush+rpop=Queue(队列)
      3. lpsh+ltrim=Capped Collection(有限集合)
      4. lpush+brpop=Message Queue(消息队列)
      .
  • Set(集合)
    • 简介:集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素
    • 简单使用举例:sadd key element [element …]、smembers key
    • 内部编码:intset(整数集合)、hashtable(哈希表)
    • 注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存
      在阻塞Redis的可能性,可以使用 sscan 来完成。
    • 应用场景: 用户标签,生成随机数抽奖、社交需求。
  • zset(有序集合)
    • 简介:已排序的字符串集合,同时元素不能重复
    • 简单格式举例:zadd key score member [score member …],zrank key
      member  底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
    • 应用场景:排行榜,社交需求(如用户点赞)。

三种特殊的数据结构类型

  • Geospatial
    Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信
    息进行操作。
  • Hyperloglog
    用来做基数统计算法的数据结构,如统计网站的 UV。
  • Bitmap
    用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于
    字符串类型实现的 ,可以把 bitmaps 成作一个以比特位为单位的数组

redis为什么快

我们都知道内存读写是比在磁盘快很多的,Redis 基于内存存储实现的数据库,
相对于数据存在磁盘的 MySQL 数据库,省去磁盘 I/O的消耗。

SDS 简单动态字符串

二进制安全

Redis 可以存储一些二进制数据,在 C 语言中字符串遇到’\0’会
结束,而 SDS 中标志字符串结束的是 len 属性。

字典

Redis 作为 K-V 型内存数据库,所有的键值就是用字典来存储。字典就是哈
希表,比如 HashMap,通过key 就可以直接获取到对应的 value。而哈希表
的 特性,在 O(1)时间复杂度就可以获得对应的值。

跳跃表

  • 字符串长度处理:Redis 获取字符串长度,时间复杂度为 O(1),而 C 语言中,
    需要从头开始遍历,复杂度为 O(n);
  • 空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而 SDS 修改和空间扩充,会额外分配未使用的空间,减少性能损耗
  • 惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是 free 记录下多
    余的空间,后续有变更,直接使用 free 中记录的空间,减少分配。
  • 跳跃表是 Redis 特有的数据结构,就是在链表的基础上,增加多级索引提升查
    找效率。
  • 跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序
    性操作批量处理节点。

合理的数据编码

  • String:
    • 如果存储数字的话,是用 int 类型的编码;
    • 如果存储非数字,小于等于39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。
  • List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节
    (默认),使用 ziplist 编码,否则使用 linkedlist 编码
  • Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用
    ziplist 编码,否则使用 hashtable 编码。
  • Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,
    否则使用 hashtable 编码。
  • Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使
    ziplist 编码,否则使用 skiplist(跳跃表)编码

合理的线程模型

在这里插入图片描述

I/O 多路复用

I/O多路概念

理论:多路 I/O 复用技术可以让单个线程高效的 处理多个连接请求,而 Redis 使用用
epoll 作为 I/O 多路复用技术的实现。并且,Redis 自身的事件处理模型将
epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。

当我们要编写一个echo服务器程序的时候,需要对用户从标准输入键入的交互命令做出响应。在这种情况下,服务器必须响应两个相互独立的I/O事件:
1)网络客户端发起网络连接请求
2)用户在键盘上键入命令行。我们先等待哪个事件呢?没有哪个选择是理想的。如果在acceptor中等待一个连接请求,我们就不能响应输入的命令。
3)类似地,如果在read中等待一个输入命令,我们就不能响应任何连接请求。针对这种困境的一个解决办法就是I/O多路复用技术。基本思路就是使用select函数,要求内核挂起进程,只有在一个或多个I/O事件发生后,才将控制返回给应用程序。–《UNIX网络编程》
在这里插入图片描述
核心思想
让单个线程去监视多个连接,一旦某个连接就绪,也就是触发了读/写事件就通知应用程序,去获取这个就绪的连接进行读写操作。
也就是在应用程序里面可以使用单个线程同时处理多个客户端连接,在对系统资源消耗较少的情况下
提升服务端的链接处理数量

在IO多路复用机制的实现原理中客户端请求到服务端后,此时客户端在传输数据过程中,
1.为了避免Server端在read客户端数据过程中阻塞,
2.服务端会把该请求注册到Selector复路器上,
3.服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,
4.如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行
相关的处理就可以了。

常见的IO多路复用机制(这个东西没明白,略难)

常见的IO多路复用机制的实现方式有: selectpollepoll
这些都是Linux系统提供的IO复用机制的实现,

  • select和poll是基于轮询的方式去获取就绪连接。
  • epoll是基于事件驱动的方式获取就绪连接。
  • 性能的角度来看,基于事件驱动的方式要优于轮询
    的方式。

在这里插入图片描述

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

缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库
查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到
数据库去查询,进而给数据库带来压力。

通俗点说,读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对
这个值的查询请求都会穿透到数据库,这就是缓存穿透。
缓存和数据都查不到,每次都到数据库查

出现场景:
  • 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查
    询某个 userid 查询有没有守护。
  • 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
  • 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
避免穿透

1.如果是非法请求,我们在 API 入口,对参数进行校验,过滤非法值。
过滤参数
2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写
请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当
的过期时间。(业务上比较常用,简单有效)
设置过期时间

3.使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过
滤器判断值是否存在,存在才继续往下查。
hasKey

缓存雪崩

缓存雪崩指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接
访问数据库,引起数据库压力过大甚至 down 机。
一瞬间都请求到数据库,并发不高,但是查询数据量大

缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过
期时间解决,即让过期时间相对离散一点。

  • 如采用一个较大固定值+一个较小的随机值,5 小时+0 到 1800 秒。
  • Redis 故障宕机也可能引起缓存雪崩。这就需要构造 Redis 高可用集群啦。
缓存击穿

缓存穿透指热点key 在某个时间点过期✁时候,而恰好在这个时间点对这个
Key 有大量的并发请求过来,从而大量的请求打到 db。
一瞬间高并发请求到数据库, 请求过多

缓存击穿看着有点像,其实它两区别是,缓存雪崩是指数据库压力过大甚至
down 机,缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是
缓存雪崩的一个子集
吧。有些文章认为它俩区别,是区别在于击穿针对某一热
点 key 缓存,雪崩则是很多 key

解决方案就有两种:
1.使用互斥锁方案。缓存失效时,不是立即去加载 db 数据,而是先使用某些带成
功返回的原子操作命令,如(Redis 的 setnx)去操作,成功的时候,再去加载
db数据库数据和设置缓存。否则就去重试获取缓存。
2. “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去
更新和设置过期时间。

常规用法

在这里插入图片描述
在这里插入图片描述

如何解决缓存雪崩、缓存穿透和缓存击穿?

缓存雪崩

缓存雪崩:是指缓存同一时间大面积失效,后面数据查询时都查询数据库,数据查询的压力全
部落到了数据库上,导致数据库短时间内承受大量的请求而down掉。
在这里插入图片描述

解决方法

1.缓存数据的过期时间设置为随机,防止同一时间出现大批数据过期的现象
2.将热点数据设置为永不过期
3.如果并发量不是特别高,可以加入队列或者锁
4.如果缓存数据库是分布式部署的,可以将热点的数据打散均匀分布到多个节点中。

缓存穿透

在这里插入图片描述

缓存穿透:是指缓存和数据库中都没有的数据,导致所有的请求全都落到数据库上,并且是并
发量较高,当然也有可能是恶意攻击,造成数据库短时间内承受大量请求而挂掉。

解决方法

1、给接口层增加校验,比如用户鉴权校验。
2、将空数据放到缓存中,设置key-null,同时设置短的缓存时间(如30秒),这样可以防止攻击用
户反复用一个id暴力攻击
3、采用布隆过滤器,将所有可能存在的数据哈希存到一个足够大的 中,一些一定不存在的数
据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

缓存击穿

在这里插入图片描述
缓存击穿:是指缓存中没有或者已经到期但是数据库中有的数据,这时由于并发用户特别多,
同时在缓存中又没有读到数据,所以直接去数据库中取数据,引起数据库压力瞬间增大。和缓存雪崩
不同的是,缓存击穿指并发查询同一条数据,缓存雪崩是不同数据都过期了,大面积数据都查询不到,从而查询数据库。

解决方法

1、设置热数据永不过期
2、使用 避免大量请求同时查询DB
3、做好熔断、降级,防止系统崩溃

总结

其实,缓存雪崩、穿透以及击穿,最终导致的问题都是我的请求绕过了我们的缓存中间件,直接打到
了DB的集群,导致DB的压力过大或者造成DB崩溃的现象。只不过场景稍微有些不一样。

  • 缓存雪崩
    缓存雪崩主要有大量的key同时打到DB,那么产生这种场景有2种主要原因。
    1.缓存中间件服务不可用,缓存失效,导致所有的请求全部打到DB,那么这种解决方案就是尽可能
    保证缓存中间件的高可用,比如Redis缓存,那么就采用Redis的cluster集群,以及提前做好缓存组
    件报警机制,杜绝宕机。
    2.缓存中大部分key同时过期,比如Redis中key同时过期,对于这样的情况,可以在失效时间上增加
    一个1到5分钟的随机值。或者对热数据,进行访问的时候,自动续期。
  • 缓存穿透
    缓存穿透是指缓存和数据库中都没有的数据,但是用户一直请求不存在的数据!这时的用户很可能就
    是攻击者,恶意搞你们公司的,攻击会导致数据库压力过大。
    1.我们可以在缓存之外再加一层性能更好的过滤方案,比如布隆过滤器或者布谷鸟过滤器。如果数据
    不存在,直接在过滤器进行过滤掉,不会打到Redis跟DB。
    2.如果发现是恶意攻击,可以对IP进行处理,加入IP黑名单。
  • 缓存击穿
    单个key过期的时候有大量并发,我们可以采用dcl锁等方式,但是我觉得如果要采用锁来保证,那
    么就没有必要用到缓存。
    所以,在我看来,您提出来的这个问题,有点过于放大了它带来的影响。
    首先,在一个成熟的系统里面,对于比较重要的热点数据,必然会有一个专门缓存系统来维护,同时
    它的过期时间的维护必然和其他业务的key会有一定的差别。而且非常重要的场景,我们还会设计多
    级缓存系统。
    其次,即便是触发了缓存雪崩,数据库本身的容灾能力也并没有那么脆弱,数据库的主从、双主、读
    写分离这些策略都能够很好的缓解并发流量。
    最后,数据库本身也有最大连接数的限制,超过限制的请求会被拒绝,再结合熔断机制,也能够很好
    的保护数据库系统,最多就是造成部分用户体验不好。

什么是热 Key 问题,如何解决热 key 问题

什么是热 Key 呢?
在 Redis 中,我们把访问频率高的 key,称为热点key。
如果某一热点 key 的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
热点Key 是怎么产生的呢?

  • 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
  • 请求分片集中,超过单 Redis 服务器的性能,比如固定名称 key,Hash 落入同
    一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。

那么在日常开发中,如何识别到热点 key 呢?

  • 凭经验判断哪些是热 Key;
  • 客户端统计上报;
  • 服务代理层上报

如何解决热 key 问题?

  • Redis 集群扩容:增加分片副本,均衡读流量;
  • 将热 key 分散到不同的服务器中;
  • 使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。

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

在这里插入图片描述
定时过期
每个设置过期时间的 key 都需要创一个定时器,到过期时间就会立即对 key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。
定期过期
每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中,key是指向键空间中的某个键的指针,value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 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,自然而然就想到缓存,国内外中大型的网站都离不开缓存。
    合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以
    降低数据库 DB 的压力。并且,Redis 相比于 memcached,还提供了丰富的
    数据结构,并且提供 RDB 和 AOF 等持久化机制,强的一批。
  • 排行榜
    当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交
    APP 的礼物排行榜、小程序的投票排行榜等等。Redis 提供的 zset数据类型能
    够实现这些复杂的排行榜。
    比如,用户每天上传视频,获得点赞的排行榜可以这样设计:
    1. 用户 Jay 上传一个视频,获得 6 个赞:
zadd user:ranking:2021-03-03 Jay 3
2.  过了一段时间,再获得一个赞,可以这样:
zincrby user:ranking:2021-03-03 Jay 1
3.  如果某个用户 John 作弊,需要删除该用户:
zrem user:ranking:2021-03-03 John
4. 展示获取赞数最多的 3 个用户
zrevrangebyrank user:ranking:2021-03-03 0 2
  • 计数器应用
    各大网站、APP 应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
  • 共享Session
    如果一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,用户刷
    新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用 Redis
    将用户的 Session 进行集中管理,每次用户更新或者查询登录信息都直接从
    Redis 中集中获取。
    几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一
    个资源的并发访问的技术难题,如秒杀、下单减库存等场景。
  • 分布式锁
    1. 用 synchronize 或者reentrantlock 本地锁肯定是不行的。
    2. 如果是并发量不大话,使用数据库的悲观锁、乐观锁来实现没啥问题。
    3. 但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的
      性能。
    4. 实际上,可以用 Redis 的 setnx 来实现分布式的锁。
  • 社交网络
    赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由
    于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的
    数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
  • 消息队列
    消息队列是大型网站必用中间件,如 ActiveMQ、RabbitMQ、Kafka 等流行
    的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。
    Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另
    外,这个不能和专业的消息中间件相比。
  • 位操作
    用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某
    用户是否在线状态等等。腾讯 10 亿用户,要几个毫秒内查询到某个用户是否
    在线,能怎么做?千万别说给每个用户的立一个 key,然后挨个记(你可以算
    一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作——
    使用 setbit、getbit、bitcount 命令。原理是:redis 内构的一个足够长的数
    组,每个数组元素只能是 0 和 1 两个值,然后这个数组的下标 index 用来表
    示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标
    和元素值(0 和 1)来构的一个记忆系统。

Redis 持久化有哪几种方式

RDB 和 AOF 的实现原理、优缺点

Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,
所以提供了RDB和AOF两种持久化机制。

RDB

在这里插入图片描述

RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写
入到磁盘,以二进制的压缩文件进行存储。

RDB快照的触发方式有很多

  • 比如执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。

  • 根据redis.conf文件里面的配置,自动触发bgsave主从复制的时候触发AOF持久化,它是一种近乎
    实时的方式,把Redis Server执行的事务命令进行追加存储。

    简单来说,就是客户端执行一个数据变更的操作,Redis Server就会把这个命令追加到aof缓冲区的
    末尾,然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,
    是根据刷盘的策略来决定的。

AOF

在这里插入图片描述
在这里插入图片描述

因为AOF这种指令追加的方式,会造成AOF文件过大,带来明显的IO性能问题,所以Redis针
对这种情况提供了AOF重写机制,也就是说当AOF文件的大小达到某个阈值的时候,就会把这个文
件里面相同的指令进行压缩。
优缺点
RDB是每隔一段时间触发持久化,因此数据安全性低。
AOF可以做到实时持久化,数据安全性较高
RDB文件默认采用压缩的方式持久化,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好

如何选择 RDB 和 AOF

  • 如果数据不能丢失,RDB 和 AOF 混用
  • 如果只作为缓存使用,可以承受几分钟的数据丢失的话,可以只使用 RDB。
  • 如果只使用 AOF,优先使用 everysec 的写回策略。

怎么实现 Redis 的高可用?(Redis 主从、哨兵、集群)

我们在项目中使用 Redis,肯定不会是单点部署 Redis 服务的。因为,单点部署一旦宕
机,就不可用了。为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不
同的服务器上,其中一台挂了也可以继续提供服务。 Redis 实现高可用有三种部署模式:
主从模式,哨兵模式,集群模式

Redis 主从

一个就是数据不能丢失,或者说尽量减少丢失;另外一个就是保证 Redis 服务不中断。

  • 对于尽量减少数据丢失,可以通过 AOF 和 RDB 保证。
  • 对于保证服务不中断的话,Redis 就不能单点部署。

Redis 主从概念

  • Redis 主从模式,就是部署多台 Redis 服务器,有主库和从库,它们之间通过主
    从复制,以保证数据副本的一致。
  • 主从库之间采用的是读写分离的方式,其中主库负责读操作和写操作,从库则负责
    读操作。
  • 如果 Redis 主库挂了,切换其中的从库成为主库。

Redis主从同步的过程

在这里插入图片描述
Redis 主从同步包括三个阶段。
第一阶段:主从库间建立连接、协商同步。

  • 从库向主库发送 psync 命令,告诉它要进行数据同步。
  • 主库收到 psync 命令后,响应 FULLRESYNC 命令(它表示第一次复制采用的是全
    量复制),并带上主库 runID 和主库目前的复制进度 offset。

第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。

  • 主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB
    文件后,会先清空当前数据库,然后加载 RDB 文件。
  • 主库把数据同步到从库的过程中,新来的写操作,会记录到 replication
    buffer。

第三阶段,主库把新写的命令,发送到从库。

  • 主库完成 RDB 发送后,会把 replication buffer 中✁修改操作发给从库,从
    库再重新执行这些操作。这样主从库就实现同步啦。

Redis 主从的一些注意

  • 主从数据不一致
    因为主从复制是异步进行的,如果从库滞后执行,则会导致主从数据不一致。
    主从数据不一致一般有两个原因
  • 主从库网路延迟。
  • 从库收到了主从命令,但是它正在执行阻塞性的命令(如 hgetall等)。

如何解决主从数据不一致问题呢?

  1. 可以换更好的硬件配置,保证网络畅通。
  2. 监控主从库间的复制进度
  3. 读取过期数据

Redis 删除数据有这几种策略

  • 惰性删除:只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除
  • 定期删除:每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量
    的 key,并清除其中已过期的 key
  • 主动删除:当前已用内存超过最大限定时,触发主动清理策略

如果使用 Redis 版本低于 3.2,读从库时,并不会判断数据是否过期,而是会
返回过期数据。而 3.2 版本后,Redis 做了改进,如果读到的数据已经过期了,
从库不会删除,却会返回空值,避免了客户端读到过期数据
因此,在主从 Redis 模式下,尽量使用 Redis 3.2 以上的版本。

一主多从,全量复制时主库压力问题
如果是一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复
制的话,主库的压力是很大的。因为主库 fork 进程生成 RDB,这个 fork 的过
程是会阻塞主线程处理正常请求的。同时,传输大的 RDB 文件也会占用主库
的网络宽带。

主-从-从模式
在这里插入图片描述

其实就是部署主从集群时,
选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系

主从网络断了怎么办呢?
主从库完成了全量复制后,它们之间会维护一个网络长连接,用于主库后续收到写命令传输到从库,它可以避免频繁的立连接的开销。但是,如果网络断开重连后,是否还需要进行一次全量复制呢?
如果是 Redis 2.8 之前,从库和主库重连后,确实会再进行一次全量复制,但是这样开销就很大。
而 Redis 2.8 之后做了优化,重连后采用增量复制方式,即把主从库网络断连期间主库收到的写命令,同步给从库

当主从库断开连接后,主库会把断连期间收到的写操作命令,写入replication buffer,同时也会把这些操作命令写入repl_backlog_buffer这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。

主从库重连后,就是利用 repl_backlog_buffer 实现增量复制。

如何保证主从数据一致性
全量同步、增量同步、指令同步
全量同步:一般发生在第一次建立主从关系、或者跟主断开时间比较久的场景。
它的步骤为:
1.slave向master发起同步指令,master收到以后,会通过bgsave指令生成一个快照RDB
文件,同步给slave,slave拿到后,会通过master同步的快照文件进行加载。这个时候,
主生成rdb文件时候的所有数据都以及同步给了slave。
2.但是bgsave指令是不会阻塞其他指令执行的,所以master在生成快照文件时,还是能接
收新的指令执行。这些指令master会先保存到一个叫replication_buffer的内存区间,等
slave加载完快照文件后会同步。

哨兵作用

哨兵其实是一个运行在特殊模式下的 Redis 进程。它有三个作用,分别是:
监控、自动选主切换(简称选主)、通知。

哨兵进程在运行期间,监视所有的 Redis 主节点和从节点。
它通过周期性给主从库发送 PING命令,检测主从库是否挂了。
如果从库没有在规定时间内响应哨兵的 PING命令,哨兵就会把它标记为下线状态;
如果主库没有在规定时间内响 应哨兵的 PING命令,哨兵则会判定主库下线,然后开始切换到选主任务。

所谓选主,其实就是从多个从库中,按照一定规则,选出一个当做主库。至于
通知呢,就是选出主库后,哨兵把新主库的连接信息发给其他从库,让它们和
新主库的立主从关系。
同时,哨兵也会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
在这里插入图片描述


因为Redis 哨兵也是一个 Redis 进程,如果它自己挂了呢,那是不是就起不了
监控的作用啦。我们一起来看下 Redis 哨兵模式
哨兵模式,就是由一个或多个哨兵实例组成的哨兵系统,它可以监视所有的
Redis 主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主
服务器属下的某个从节点升级为新的主节点。,一个哨兵进程对 Redis 节点
进行监控,就可能会出现问题(单点问题)。因此,一般使用多个哨兵来进行
监控 Redis 节点,并且各个哨兵之间还会进行监控。

哨兵之间是通过发布订阅机制组成集群的,同时,哨兵又通过 INFO命令,
获得了从库连接信息,也能和从库的立连接,从而进行监控。

哨兵如何判定主库下线

主观下线

哨兵进程向主库、从库发送 PING 命令,如果主库或者从库没有在规定的时间内响
应 PING 命令,哨兵就把它标记为主观下线。

客观下线

如果是主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频
率,以确认主库是否真的进入了主观下线。 当有多数的哨兵(一般少数服从多数,
由 Redis 管理员自行设定的一个值)在指定的时间范围内确认主库的确进入了主
观下线状态,则主库会被标记为客观下线
。这样做的目的就是避免对主库的误判,
以减少没有必要的主从切换,减少不必要的开销。

假设我们有 N 个哨兵实例,如果有 N/2+1 个实例判断主库主观下线,此时就可
以把节点标记为客观下线,就可以做主从切换了。

哨兵的工作模式(重要)
  1. 每个哨兵以每秒钟一次的频率向它所知的主库、从库以及其他哨兵实例发送一个
    PING命令。
  2. 如果一个实例节点距离最后一次有效回复 PING命令的时间超过 down-after- milliseconds选项所指定的值, 则这个实例会被哨兵标记为主观下线。
  3. 如果主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率
    确认主库的确进入了主观下线状态。
  4. 当有足够数量的哨兵(大于等于配置文件指定的值)在指定的时间范围内确认主库
    的确进入了主观下线状态, 则主库会被标记为客观下线。
  5. 当主库被哨兵标记为客观下线时,就会进入选主模式。
  6. 若没有足够数量的哨兵同意主库已经进入主观下线, 主库的主观下线状态就会被
    移除;若主库重新向哨兵的 PING命令返回有效回复,主库的主观下线状态就会被
    移除。

哨兵是如何选主库

如果明确主库已经客观下线了,哨兵就开始了选主模式。
哨兵选主包括两大过程,分别是:过滤和打分。其实就是在多个从库中,先按
照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给
剩下的从库逐个打分,将得分最高的从库选为新主库

在这里插入图片描述

  • 选主时,会判断从库的状态,如果已经下线,就直接过滤。
  • 如果从库网络不好,老是超时,也会被过滤掉。看这个参数 down-after- milliseconds,它表示我们认定主从库断连的最大连接超时时间。
  • 过滤掉了不适合做主库的从库后,就可以给剩下的从库打分,按这三个规则打分:
    从库优先级、从库复制进度以及从库 ID 号
  • 从库优先级最高的话,打分就越高,优先级可以通过 slave-priority配置。如果优先
    级一样,就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库
    ID 号小的打分高。

由哪个哨兵执行主从切换呢?

一个哨兵标记主库为主观下线后,它会征求其他哨兵的意见,确认主库是否的
确进入了主观下线状态。它向其他实例哨兵发送 is-master-down-by-addr命令。
其他哨兵会根据自己和主库的连接情况,回应 Y或 N(Y 表示赞成,N 表示反对
票)。如果这个哨兵获取得足够多的赞成票数(quorum配置),主库会被标记
为客观下线

标记主库客观下线的这个哨兵,紧接着向其他哨兵发送命令,再发起投票,希
望它可以来执行主从切换。这个投票过程称为 Leader 选举。因为最终执行主
从切换的哨兵称为 Leader,投票过程就是确定 Leader。一个哨兵想成为
Leader 需要满足两个条件:

  • 需要拿到 num(sentinels)/2+1的赞成票。
  • 并且拿到的票数需要大于等于哨兵配置文件中的 quorum值。

故障转移

假设哨兵模式架构如下,有三个哨兵,一个主库 M,两个从库 S1 和 S2。
在这里插入图片描述
当哨兵检测到 Redis 主库 M1 出现故障,那么哨兵需要对集群进行故障转移。
假设选出了哨兵 3 作为 Leader。故障转移流程如下:
在这里插入图片描述
在这里插入图片描述

  1. 从库 S1 解除从节点身份,升级为新主库
  2. 从库 S2 成为新主库的从库
  3. 原主节点恢复也变成新主库的从节点
  4. 通知客户端应用程序新主节点的地址

在这里插入图片描述

Redis Cluster 集群

哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。
但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此,
Reids Cluster 集群(切片集群的实现方案)应运而生,它在 Redis3.0 加入
的,实现了 Redis 的分布式存储
。对数据进行分片,也就是说每台 Redis 节
点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据
即分散数据到各个 Redis 实例,还提供复制和故障转移的功能。

比如你一个 Redis 实例保存 15G 甚至更大的数据,响应就会很慢,这是因为
Redis RDB 持久化机制导致的,Redis 会 fork 子进程完成 RDB 持久化操作,
fork 执行的耗时与 Redis 数据量成正相关。

这时候你很容易想到,把 15G 数据分散来存储就好了嘛。这就是 Redis 切片
集群的初衷。切片集群是啥呢?来看个例子,如果你要用 Redis 保存 15G 的
数据,可以用单实例 Redis,或者 3 台 Redis 实例组成切片集群

切片集群和 Redis Cluster 的区别:Redis Cluster 是从 Redis3.0 版本开始,
官方提供的一种实现切片集群的方案。

在这里插入图片描述
哈希槽(Hash Slot)
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射
关系。

一个切片集群被分为 16384个 slot(槽),每个进入 Redis 的键值对,根据
key 进行散列,分配到这 16384 插槽中的一个。使用的哈希映射也比较简单,
CRC16算法计算出一个 16bit的值,再对 16384取模。数据库中的每个键都属
于这 16384 个槽的其中一个,集群中的每个节点都可以处理这 16384 个槽。
集群中的每个节点负责一部分的哈希槽,假设当前集群有 A、B、C3 个节点,
每个节点上负责的哈希槽数 =16384/3,那么可能存在的一种分配:

  • 节点A 负责 0~5460 号哈希槽
  • 节点B 负责 5461~10922 号哈希槽
  • 节点C 负责 10923~16383 号哈希槽

MOVED 重定向和 ASK 重定向
在 Redis cluster 模式下,节点对请求的处理过程如下:

  1. 通过哈希槽映射,检查当前 Redis key 的否存在当前节点
  2. 若哈希槽不的由自身节点负责,就返回 MOVED 重定向
  3. 若哈希槽确实由自身负责,且 key 在 slot 中,则返回该 key 对应结果
  4. 若 Redis key 不存在此哈希槽中,检查该哈希槽的否正在迁出(MIGRATING)?
  5. 若 Redis key 正在迁出,返回ASK 错误重定向客户端到迁移的目的服务器上
  6. 若哈希槽未迁出,检查哈希槽的否导入中?
  7. 若哈希槽导入中且有 ASKING 标记,则直接操作,否则返回 MOVED 重定向
    Moved 重定向

客户端给一个 Redis 实例发送数据读写操作时,如果计算出来的槽不的在该节
点上,这时候它会返回 MOVED 重定向错误,MOVED 重定向错误中,会将哈
希槽所在的新实例的 IP 和 port 端口带回去。这就的 Redis Cluster 的
MOVED 重定向机制。流程图如下:
在这里插入图片描述

ASK 重定向

Ask 重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源
节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向可
以解决此种情况。
在这里插入图片描述

Cluster 集群节点的通讯协议:Gossip

一个 Redis 集群由多个节点组成,各个节点之间的怎么通信的呢?通过
Gossip 协议!Gossip 的一种谣言传播协议,每个节点周期性地从节点列表中
选择 k 个节点,将本节点存储的信息传播出去,直到所有节点信息一致,即算
法收敛了。

Gossip 协议基本思想:一个节点想要分享一些信息给网络中的其他的一些节点。
于的,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信
息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节
点。一般而言,信息会周期性的传递给 N 个目标节点,而不只的一个。这个 N
被称为 fanout

Redis Cluster 集群通过 Gossip 协议进行通信,节点之前不断交换信息,交换
的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot 信息
等。gossip 协议包含多种消息类型,包括 ping,pong,meet,fail,等等

在这里插入图片描述

  • meet 消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet 消
    息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong 消息
    交换。
  • ping 消息:节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知
    的两个节点的地址、槽、状态信息、最后一次通信时间等
  • pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息
    正常通信。消息中同样带有自己已知的两个节点信息。
  • fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息,
    其他节点接收到 fail 消息之后把对应节点更新为下线状态。

每个节点的通过集群总线(cluster bus) 与其他的节点进行通信的。通
讯时,使用特殊的端口号,即对外服务端口号加 10000。例如如果某个 node
的端口号的 6379,那么它与其它 nodes 通信的端口号的 16379nodes 之
间的通信采用特殊的二进制协议

故障转移

redis 集群通过 ping/pong 消息,实现故障发现。这个环境包括主观下线和客
观下线

主观下线: 某个节点认为另一个节点不可用,即下线状态,这个状态并不的最
终的故障判定,只能代表一个节点的意见,可能存在误判情况。
在这里插入图片描述

客观下线: 指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,
从而达成共识的结果。如果的持有槽的主节点故障,需要为该节点进行故障转

  • 假如节点A 标记节点B 为主观下线,一段时间后,节点 A 通过消息把节点 B 的
    状态发到其它节点,当节点 C 接受到消息并解析出消息体时,如果发现节点 B 的
    pfail 状态时,会触发客观下线流程;
  • 当下线为主节点时,此时 Redis Cluster 集群为统计持有槽的主节点投票,看投票
    数的否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
    在这里插入图片描述
    故障恢复:故障发现后,如果下线节点的的主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。
    在这里插入图片描述
  • 资格检查:检查从节点的否具备替换故障主节点的条件。
  • 准备选举时间:资格检查通过后,更新触发故障选举时间。
  • 发起选举:到了故障选举时间,进行选举。
  • 选举投票:只有持有槽的主节点才有票,从节点收集到足够的选票(大于一半),
    触发替换主节点操作

分布式锁

  • 命令 setnx + expire 分开写
if(jedis.setnx(key,lock_value) == 1{ //加锁
expire(key,100; //设置过期时间
try {
do something / / 业务请求
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}

如果执行完 setnx加锁,正要执行 expire 设置过期时间时,进程 crash 掉或者
要重启维护了
,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以 分
布式锁不能这么实现。

  • setnx + value 值的过期时间
long expires = System.currentTimeMillis() + expireTime; 
//系统时间+设置的过期时间String
expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key, expiresStr) == 1) {
return true;
}
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key);
// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解 redis的 getSet命令的小伙伴,可以去官网看下哈)
String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
}
缺点
- 过期时间的客户端自己生成的,分布式环境下,每个客户端的时间必须同步。
- 没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
- 锁过期的时候,并发多个客户端同时请求过来,都执行了 jedis.getSet(),
最终只能有一个客户端加锁成功,但的该客户端锁的过期时间,可能被别的客户端覆盖。
  • set 的扩展命令(set ex px nx)
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1{ //加锁
try {
do something / / 业务处理
}catch(){
}
finally {
jedis.del(key); //释放锁
}
}

可能存在这样的问题:
1 锁过期释放了,业务还没执行完。
2 锁被别的线程误删

  • set ex px nx + 校验唯一随机值,再删除
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1{ //加锁
try {
do something / / 业务处理
}catch(){
}
finally {
//判断的不的当前线程加的锁,的才释放
if (uni_request_id.equals(jedis.get(key))) {
jedis.del(key); //释放锁
}
}
}

在这里,判断当前线程加的锁和释放锁的不的一个原子操作。如果调用
jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加
的锁

Redisson

分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微
把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的
线程,开启一个定时守护线程,每隔一段时间检查锁的否还存在,存在则对锁
的过期时间延长,防止锁过期提前释放。
在这里插入图片描述
只要线程一加锁成功,就会启动一个 watch dog看门狗,它的一个后台线程,
会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断的延长锁 key
的生存时间。因此,Redisson 就的使用 Redisson 解决了锁过期释放,业
务没执行完问题。

原理
搞多个Redis master 部署,以保证它们不会同时宕掉。并且这些 master 节
点的完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个
master 实例上,的与在 Redis 单实例,使用相同方法来获取和释放锁。
在这里插入图片描述

  • 按顺序向 5 个 master 节点请求加锁
  • 根据设置的超时时间来判断,的不的要跳过该 master 节点。
  • 如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁
    成功啦。
  • 如果获取锁失败,解锁!

Redlock 算法

在这里插入图片描述
如果线程一在 Redis 的 master 节点上拿到了锁,但的加锁的 key 还没同步到
slave 节点。
恰好这时,master 节点发生故障,一个 slave 节点就会升级为
master 节点。
线程二就可以获取同个 key 的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

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

  • 缓存延时双删
  • 删除缓存重试机制
  • 读取biglog 异步删除缓存

延时双删

写请求 -> 删除缓存 -> 更新数据库 -> 删除缓存

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如 1 秒),再次删除缓存。( 休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒)

删除缓存重试机制

在这里插入图片描述
因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可
以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了

删除缓存重试流程

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的 key 放到消息队列
  4. 消费消息队列的消息,获取要删除的 key
  5. 重试删除缓存操作

读取biglog 异步删除缓存

通过数据库的 binlog 来异步淘汰 key

在这里插入图片描述

  • 可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面
  • 然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

Redis6.0多线程

  • Redis6.0 之前,Redis 在处理客户端的请求时,包括读 socket、解析、执行、
    写socket 等都由一个顺序串行的主线程处理,这就的所谓的“单线程”
  • Redis6.0 之前为什么一直不使用多线程?使用 Redis 时,几乎不存在 CPU 成为
    瓶颈的情况, Redis 主要受限于内存和网络。例如在一个普通的 Linux 系统上,
    Redis 通过使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要
    使用 O(N)或 O(log(N))的命令,它几乎不会占用太多 CPU。

redis 使用多线程并非的完全摒弃单线程,redis 还的使用单线程模型来处理
客户端的请求,只的使用多线程来处理数据的读写和协议解析,执行命令还的
使用单线程。
这样做的目的的因为 redis 的性能瓶颈在于网络 IO 而非 CPU,使用多线程能
提升 IO 读写的效率,从而整体提高 redis 的性能。

Redis的事务机制

Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。
事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行
过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会
插入到事务执行命令序列中。
简言之,Redis 事务就的顺序性、一次性、排他性的执行一个队列中的一系
列命令。
Redis 执行事务的流程如下:

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

Hash 冲突怎么办

Redis 作为一个 K-V 的内存数据库,它使用用一张全局的哈希来保存所有的
键值对。这张哈希表,有多个哈希桶组成,哈希桶中的 entry 元素保存了 key
和value 指针,其中 key 指向了实际的键,value 指向了实际的值。
哈希表查找速率很快的,有点类似于 Java 中的 HashMap,它让我们在 O(1)
的时间复杂度快速找到键值对。首先通过 key 计算哈希值,找到对应的哈希桶
位置,然后定位到entry,在 entry 找到对应的数据

哈希冲突: 通过不同的 key,计算出一样的哈希值,导致落在同一个哈希桶中。

在这里插入图片描述
Redis 为了解决哈希冲突,采用了链式哈希。链式哈希的指同一个哈希桶中,
多个元素用一个链表来保存,它们之间依次用指针连接。

有些读者可能还会有疑问:哈希冲突链上的元素只能通过指针逐一查找再操作。
当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就
会降低了。
为了保持高效,Redis 会对哈希表做 rehash 操作,也就的增加哈希桶,减少
冲突。为了 rehash 更高效,Redis 还默认使用了两个全局哈希表,一个用于
当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。

HashMap是怎么解决哈希冲突的

首先,我们得知道下HashMap的存储方式,HashMap是k-v的数据结构, 底层是一个entry数组的
数据结构, 并且根据key计算出一个hash值,取模entry数组长度-1,得到一个下标位置来存储数据。
所谓hash冲突 ,就是不同数据hash算法计算出来的结果可能是相同的,那么不同的数据就会放在同
一个下标位置。
通常解决hash冲突的方法有4种。

  • 1.开放寻址法,包含线性探测、平方探测等等,就是从发生冲突的那个位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到了线性探测法来解决hash冲突的。
  • 2.链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式来存储。
  • 3.再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
  • 4.建立公共溢出区, 就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入到溢出表中。
    HashMap就是通过链式寻址法来解决的,但是链式地址有个问题:当链表过长的时候,会影响查询
    性能,所以在HashMap里,当链表长度大于8的时候,会转为红黑树,提升查询性能。但是树在添
    加数据的时候也会有树的分裂合并,所以在树节点小于6的时候,又会转为链表
    这个 是我对HashMap哈希冲突的理解。
    面试官

HashMap在并发的时候会有什么问题?

因为HashMap添加数据的时候,不是线程安全的,所以当并发添加数据的时候,有可能导致数据丢失.
比如:当2个数据同时链地址到一个节点的时候,只会有1个是成功的。
还有,因为entry节点会有数据结构的变化,比如当链表长度达到8并且数组长度达到64后,会转为链表。那么在并发的时候,可能会产生数据结构类型转换错误。因为有可能在操作的时候,其他线程把数据结构变更。
所以,如果在有并发要求的场景,可以用currentHashMap或者hashTable。它们是线程安全的。

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

Redis 提供两个指令生成 RDB,分别的 save 和 bgsave。

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

Redis 底层,使用什么协议

RESP,英文全称的 Redis Serialization Protocol,它的专门为 redis 设计的
一套序列化协议. 这个协议其实在 redis 的 1.2 版本时就已经出现了,但的到了
redis2.0 才最终成为redis 通讯协议的标准。
RESP 主要有实现简单、解析速度快、可读性好等优点

Redis存在线程安全问题吗

  • Redis服务端
    Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。
    虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。
    Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈点,所以没必要使用多线程来执行指令。
    如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。

  • Redis客户端
    虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。
    假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。

客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指
令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等

Redis的缓存淘汰策略

第一个方面
当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据
配置的内存淘汰策略。
把访问频率不高的 key 从内存中移除。
maxmemory 默认情况是当前服务器的最大内存。
第二个方面
Redis 默认提供了 8 种缓存淘汰策略,这 8 种缓存淘汰策略总的来说,我认为可
以归类成五种
第一种, 采用 LRU 策略,就是把不经常使用的 key 淘汰掉。
第二种,采用 LFU 策略,它在 LRU 算法上做了优化,增加了数据访问次数,从
而确保淘汰的是非热点 key。
第三种,随机策略,也就是是随机删除一些 key。
第四种,ttl 策略,从设置了过期时间的 key 里面,挑选出过期时间最近的 key
进行优先淘汰
第五种,当内存不够的时候,直接报错,这是默认的策略
这些策略可以在 redis.conf 文件中手动配置和修改,我们可以根据缓存的类型和
缓存使用的场景来选择合适的淘汰策略。
最后一个方面,我们在使用缓存的时候,建议是增加这些缓存的过期时间。
因为我们知道这些缓存大概的生命周期,从而更好的利用内存

Redis主从复制的原理

Redis主从复制包括全量复制增量复制
全量复制是发生在初始化阶段,从节点会主动向主节点发起一个同步请求,主节点收到请求后会生成一份当前数据的快照发送给从节点,从节点收到数据进行加载后完成全量复制。
增量复制是发生在每次Master数据发生变化的过程中,会把变化的数据同步给所有的从节点。
增量复制是通过维护Offset这个复制偏移量来实现的。

Redis主从复制,是指在Redis集群里面,Master节点和Slave节点数据同步的一种机制。
简单来说就是把一台Redis服务器的数据,复制到其他Redis服务器中。
其中负责复制数据的来源称为master,被动接收数据并同步的节点称为slave
在这里插入图片描述
全量复制:
全量复制一般发生在Slave节点初始化阶段,这个时候需要把master上所有数据都复制一份。
具体的工作原理是:
1.Slave向Master发送SYNC命令
2.Master收到命令以后生成数据快照
3.把快照数据发送给Slave节点
4.Salve节点收到数据后丢弃旧的数据,并重新载入新的数据
需要注意,在主从复制过程中,Redis并没有采用实现强数据一致性,因此会存在一定时间的数据不一致问题。

增量复制:
1.Master收到数据变更之后
2.把变更的数据同步给所有Slave节点。
增量复制的原理是,Master和Slave都会维护一个复制偏移量(offset),用来表示Master向Slave传递的字节数。
每次传输数据,Master和Slave维护的Offset都会增加对应的字节数量。
Redis只需要根据Offset就可以实现增量数据同步了。

AOF 重写

AOF是Redis里面的一种数据持久化方式,它采用了指令追加的方式。
近乎实时的去实现数据指令的持久化,因为AOF,会把每个数据更改的操作指令,追加存储到aof文件里面。
所以很容易导致AOF文件出现过大,造成IO性能问题。
Redis为了解决这个问题,设计了AOF重写机制,也就是说把AOF文件里面相同的指令进行压缩,只保留最新的数据指令。

简单来说,如果aof文件里面存储了某个key的多次变更记录,但是实际上,最终在做数据恢复的时候,只需要执行最新的指令操作就行了,历史的数据就没必要存在这个文件里面占空间。

AOF文件重写的具体过程:
首先,根据当前Redis内存里面的数据,重新构建一个新的AOF文件
然后,读取当前Redis里面的数据,写入到新的AOF文件里面
最后,重写完成以后,用新的AOF文件覆盖现有的AOF文件
另外,因为AOF在重写的过程中需要读取当前内存里面所有的键值数据,再生成对应的一条指令进行保存。
而这个过程是比较耗时的,对业务会产生影响。
所以Redis把重写的过程放在一个后台子进程里面来完成,
这样一来,子进程在做重写的时候,主进程依然可以继续处理客户端请求。
最后,为了避免子进程在重写过程中,主进程的数据发生变化,
导致AOF文件和Redis内存中的数据不一致的问题,Redis还做了一层优化。
就是子进程在重写的过程中,主进程的数据变更需要追加到AOF重写缓冲区里面。
等到AOF文件重写完成以后,再把AOF重写缓冲区里面的内容追加到新的AOF文件里面。

Redis哨兵机制和集群区别

Redis集群有几种实现方式,一个是主从集群、一个是Redis Cluster
主从集群,就是在Redis集中包括一个Master节点和多个Slave节点。
Master负责数据的读写,Slave节点负责数据的读取。
Master上收到的数据变更,会同步到Slave节点上实现数据的同步。
通过这种架构实现可以Redis的读写分离,提升数据的查询性能。

Redis主从集群不提供容错和恢复功能,一旦Master节点挂了,不会自动选出新的Master,导致后续客户端所有写请求直接失败。
所以Redis提供了哨兵机制,专门用来监听Redis主从集群提供故障的自动处理能力。
哨兵会监控Redis主从节点的状态,当Master节点出现故障,会自动从剩余的Slave节点中选一个新的Master。

哨兵模式下虽然解决了Master选举的问题,但是在线扩容的问题还是没有解决。
于是就有了第三种集群方式,Redis Cluster,它实现了Redis的分布式存储,也就是每个节点存储不同的数据实现数据的分片

在Redis Cluster中,引入了Slot槽来实现数据分片,Slot的整体取值范围是0~16383,每个节点会分配一个Slot区间当我们存取Key的时候,Redis根据key计算得到一个Slot的值,然后找到对应的节点进行数据的读写。
在高可用方面,Redis Cluster引入了主从复制模式, 一个Master节点对应一个或多个Slave节点,当Master出现故障,会从Slave节点中选举一个新的Master继续提供服务。

Redis Cluster虽然解决了在线扩容以及故障转移的能力,但也同样有缺点,比如
客户端的实现会更加复杂,Slave节点只是一个冷备节点,不提供分担读操作的压力
对于Redis里面的批量操作指令会有限制。
因此主从模式和Cluster模式各有优缺点,在使用的时候需要根据场景需求来选择。

总结
Redis哨兵集群是基于主从复制来实现的,所以它可以实现读写分离,分担Redis读操作的压力
Redis Cluster 集群的Slave节点只是实现冷备机制,它只有在Master宕机之后才会工作
Redis哨兵集群无法在线扩容,所以它的并发压力受限于单个服务器的资源配置。
Redis Cluster提供了基于Slot槽的数据分片机制,可以实现在线扩容提升写数据的性能
从集群架构上来说
Redis 哨兵集群是一主多从
Redis Cluster是多主多从

Redis的理解

Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程序的数据IO效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值