指引:
-
redis的数据结构5种 或者 详解
-
Redis的各种架构说明 重点
1、面试题:redis中zset的score值一样,那它是按什么排序的:按字典值排序
在score相同的情况下,redis使用字典排序,而所谓的字典排序其实就是“ABCDEFG”这样的排序,在首字母相同的情况下,redis会再比较后面的字母,还是按照字典排序。
2、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。但是redis的单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。
这个时候可使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
首先,redis的数据结构
Redis有5个基本数据结构,string、list、hash、set和zset。
-
String: 动态字符串SDS,string表示的是一个可变的_字节数组_;
-
list:列表的存储结构用的是_双向链表_。因为它是链表,所以随机定位性能较弱,首尾插入删除性能较优。
-
hash:哈希等价于Java语言的HashMap
-
set:Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
-
zset:zset底层实现使用了两个数据结构,第一个是hash,第二个是_跳跃列表_,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。
跳表插入一条数据的过程:通过将有序链表分层,由最上层依次向后查询,如果本层的next节点大于要找的值或指向null,则从本节点开始,下降一层向后查找,依次类推,如果找到则返回节点,否则返回空。因此在节点较多的时候可以跳过一些节点,查询效率大大提升,这就是跳跃表的基本思想。
zset应用:排行榜
zset:字典与跳跃表
其中,字典(也称为hashtable或散列表)
Redis--各个数据类型最大存储量
Strings类型:一个String类型的value最大可以存储512M
Lists类型:list的元素个数最多为2^32-1个,也就是4294967295个。
Sets类型:元素个数最多为2^32-1个,也就是4294967295个。
Hashes类型:键值对个数最多为2^32-1个,也就是4294967295个。
Sorted sets类型:跟Sets类型相似。
redis 高级数据类型(特殊用途)
1、Bitmaps(位图) 场景:用户签到、布隆过滤器
通过String类型实现的位操作
适合存储布尔值数据
常用命令:SETBIT, GETBIT, BITCOUNT
2、HyperLogLog(基数统计) 场景:UV统计
用于估计集合的基数(不重复元素数量)
固定使用12KB内存
标准错误率0.81%
常用命令:PFADD, PFCOUNT, PFMERGE
3、Geospatial(地理空间) 场景:地理位置应用
存储地理位置信息
基于Sorted Set实现
支持半径查询
常用命令:GEOADD, GEODIST, GEORADIUS
4、Stream(流) 场景:消息队列
Redis 5.0引入的新数据类型
用于消息队列功能,支持消费者组
常用命令:XADD, XREAD, XGROUP
一、缓存穿透 & 缓存雪崩& 缓存击穿
缓存解决方案很全:juejin.cn/post/701167…
1、缓存穿透,
是指查询一个一定不存在的数据,由于缓存是不命中时被动写( 被动写,指的是从 DB 查询到数据,则更新到缓存中 )的,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义。 在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方法:
方案一,缓存空对象:当从 DB 查询数据为空,我们仍然将这个空结果进行缓存,具体的值需要使用特殊的标识,能和真正缓存的数据区分开。另外,需要设置较短的过期时间,一般建议不要超过 5 分钟。
方案二,BloomFilter 布隆过滤器:在缓存服务的基础上,构建 BloomFilter 数据结构,在 BloomFilter 中存储对应的 KEY 是否存在,如果存在,说明该 KEY 对应的值为空。
简介:布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
思想步骤:
1. 位数组:初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。
2. 添加元素(k个独立的hash函数)
添加元素时,对x使用k个哈希函数得到k个哈希值,对m取余,对应的bit位设置为1。
3. 判断元素是否存在
判断y是否属于这个集合,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置都是1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。
2、缓存雪崩,
是指缓存由于某些原因无法提供服务( 例如,缓存挂掉了 ),大面积缓存失效,所有请求全部达到 DB 中,导致 DB 负荷大增,最终挂掉的情况。
解决方案: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
解决方法:
1)缓存高可用
2)本地缓存
3)请求 DB 限流
4)服务降级
5)提前演练
缓存雪崩的原因有两种
- 大量缓存数据在同一时间过期或失效
- Redis 故障宕机
解决方案:
1、原有的失效时间基础上增加一个随机值,比如1-5分钟随机
2、互斥锁
使用互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里)。当缓存构建完成后,再释放锁。
未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
3、双key策略
对缓存数据使用两个
key,一个是主key,会设置过期时间;一个是备key,不会设置过期。它们只是key不一样,但value值是一样的,相当于给缓存数据做了个副本。
当业务线程访问不到主key的缓存数据时,就直接返回备key的缓存数据,然后在更新缓存的时候,同时更新主key和备key的数据。
4、后台更新缓存,定时更新或者使用消息队列通知更新
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新或使用消息队列通知更新。
定时更新:后台线程定时的把数据更新到缓存中。
使用消息队列通知更新:在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存。
在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的 「缓存预热」。后台更新缓存的机制刚好也适合干这个事情。
5、请求限流
启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
3、缓存击穿
原因:热点数据缓存过期
缓存击穿:是指某个**极度“热点”**数据在某个时间点过期时,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
解决方案:
1、分布式锁:在访问数据库资源之前,我们使用redis给访问数据库的程序加上锁,以及在结束的时候释放锁
2、手动过期。
缓存上从不设置过期时间,功能上将过期时间存在 KEY 对应的 VALUE 里。流程如下:
- 1、获取缓存。通过 VALUE 的过期时间,判断是否过期。如果未过期,则直接返回;如果已过期,继续往下执行。
- 2、通过一个后台的异步线程进行缓存的构建,也就是“手动”过期。通过后台的异步线程,保证有且只有一个线程去查询 DB。
- 3、同时,虽然 VALUE 已经过期,还是直接返回。通过这样的方式,保证服务的可用性,虽然损失了一定的时效性。
3、不给热点数据设置过期时间,由后台异步更新缓存
不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
4、区别:
-
缓存“击穿”和缓存“雪崩“”的区别在于,前者针对某一 KEY 缓存,后者则是很多 KEY 。
-
缓存“击穿”和缓存“穿透“”的区别在于,这个 KEY 是真实存在对应的值的。
二、Redis为什么是单线程、及高并发快的3大原因详解
1、纯内存操作。 Redis 为了达到最快的读写速度,将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 Redis 具有快速和数据持久化的特征。 如果不将数据放在内存中,磁盘 I/O 速度为严重影响 Redis 的性能。
2、核心是基于非阻塞的 IO 多路复用机制。
3、单线程反而避免了多线程的频繁上下文切换问题。 Redis 利用队列技术,将并发访问变为串行访问,消除了传统数据库串行控制的开销
4、Redis 全程使用 hash 结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
三、IO多路复用技术
学习:IO多路复用的三种机制Select,Poll,Epoll 重新学习
redis 采用网络IO多路复用技术来保证在多连接的时候, 系统的高吞吐量。
多路-指的是多个socket连接,复用-指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。
redis的网络io模型底层使用IO多路复用,通过reactor模式实现的。
Redis 是一个典型的单线程、事件驱动的内存数据库
事件驱动模型的核心思想是:应用程序(Redis)在一个主循环中等待事件的发生(如连接建立、数据到达、定时器超时),当事件发生后,调用预先绑定好的回调函数来处理事件。 这避免了为每个连接创建一个线程所带来的上下文切换和资源竞争开销,使得单线程也能实现极高的并发性能。
Redis 的事件驱动模型是一个经典的 Reactor 模式 实现。它通过单线程 + I/O 多路复用,配合处理文件事件和时间事件,巧妙地平衡了性能、资源消耗和开发复杂度,使其成为高性能键值存储的典范。理解这一模型,对于用好 Redis、诊断性能问题至关重要。
总结部分:
Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,
所以 Redis 才叫做单线程的模型。
它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
- 多个 Socket 。
- IO 多路复用程序。
- 文件事件分派器。
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)。
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
来看客户端与 redis 的一次通信过程:
-
客户端 Socket01 向 Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个
AE_READABLE事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 Socket01,并将该 Socket01 的AE_READABLE事件与命令请求处理器关联。 -
假设此时客户端发送了一个
set key value请求,此时 Redis 中的 Socket01 会产生AE_READABLE事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 Socket01 的AE_READABLE事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 Scket01 的set key value并在自己内存中完成set key value的设置。操作完成后,它会将 Scket01 的AE_WRITABLE事件与令回复处理器关联。 -
如果此时客户端准备好接收返回结果了,那么 Redis 中的 Socket01 会产生一个
AE_WRITABLE事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如ok,之后解除 Socket01 的AE_WRITABLE事件与命令回复处理器的关联。
这样便完成了一次通信。理解一下,灰常重要。如果还是不能理解,可以在网络上搜一些资料,在理解理解。
四、缓存和数据库一致性解决方案
第一种方案:采用延时双删策略 + 缓存超时 !!!
1.在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
2.具体的步骤就是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒:需要评估自己的项目的读数据业务逻辑的耗时
4)再次删除缓存
3.设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
4.该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
第二种方案:异步更新缓存(基于订阅binlog的同步机制)
1.技术整体思路:
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis
1)读Redis:热数据基本都在Redis
2)写MySQL:增删改都是操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
2.Redis更新
1)数据操作主要分为两大块:
- 一个是全量(将全部数据一次写入到redis)
- 一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
以上就是Redis和MySQL数据一致性详解
四、本地缓存有redis的一致性
第三种方案:版本号/时间戳比对 本地缓存有redis的一致性
实现方式:
1-数据增加版本号或时间戳字段
2-读取时比较本地与Redis中的版本
public Data getData(String id) {
// 1. 从本地缓存获取
Data localData = localCache.get(id);
// 2. 从Redis获取版本
long redisVersion = redis.getVersion(id);
// 3. 版本比对
if (localData == null || localData.version < redisVersion) {
Data newData = redis.get(id);
localCache.put(id, newData);
return newData;
}
return localData;
}
第二种方案、双写模式 + 失效机制
实现方式:
1-写操作同时更新Redis和本地缓存
2-通过消息队列通知其他节点失效本地缓存
public void updateData(Data data) {
// 1. 更新数据库
db.update(data);
// 2. 更新Redis
redis.set(data.getId(), data);
// 3. 发送缓存失效消息
mq.sendCacheEvictMessage(data.getId());
}
// 各节点监听消息
@MQListener
public void handleCacheEvict(String key) {
localCache.invalidate(key);
}
第一种方案、定时过期 + 延迟双删
实现方式:
1-设置本地缓存短过期时间(如30秒)
2-更新数据时采用延迟双删策略
public void updateData(Data data) {
// 1. 删除本地缓存
localCache.invalidate(data.getId());
// 2. 更新数据库
db.update(data);
// 3. 删除Redis
redis.delete(data.getId());
// 4. 延迟再次删除(通过异步线程)
asyncTask.delay(1000, () -> {
redis.delete(data.getId());
localCache.invalidate(data.getId());
});
}
总结:
五、redis集群机制
摘自:零壹技术栈
在 Redis 中,实现 高可用 的技术主要包括 持久化、复制、哨兵 和 集群,下面简单说明它们的作用,以及解决了什么样的问题:
- 持久化:持久化是 最简单的 高可用方法。它的主要作用是 数据备份,即将数据存储在 硬盘,保证数据不会因进程退出而丢失。
- 复制:复制是高可用 Redis 的基础,哨兵 和 集群 都是在 复制基础 上实现高可用的。复制主要实现了数据的多机备份以及对于读操作的负载均衡和简单的故障恢复。缺陷是故障恢复无法自动化、写操作无法负载均衡、存储能力受到单机的限制。
- 哨兵:在复制的基础上,哨兵实现了 自动化 的 故障恢复。缺陷是 写操作 无法 负载均衡,存储能力 受到 单机 的限制。
- 集群:通过集群,Redis 解决了 写操作 无法 负载均衡 以及 存储能力 受到 单机限制 的问题,实现了较为 完善 的 高可用方案。
1、redis的主从复制
面试题: redis主从同步原理
主:写,从:读
Redis 主从复制可将主节点数据同步给从节点,从节点此时有两个作用:
-
一旦主节点宕机,从节点作为主节点的备份可以随时顶上来。
-
扩展主节点的读能力,分担主节点读压力。
主从复制同时存在以下几个问题:
一旦主节点宕机,从节点晋升成主节点,同时需要修改应用方的主节点地址,还需要命令所有 从节点去复制新的主节点,整个过程需要人工干预。
主节点的写能力受到单机的限制。
主节点的存储能力受到单机的限制。
原生复制 的弊端在早期的版本中也会比较突出,比如:Redis 复制中断 后,从节点 会发起 psync。此时如果 同步不成功,则会进行 全量同步,主库 执行 全量备份 的同时,可能会造成毫秒或秒级的卡顿。
2、redis的哨兵模式
哨兵模式就是不时地监控redis是否按照预期良好地运行(至少是保证主节点是存在的),若一台主机出现问题时,哨兵会自动将该主机下的某一个从机设置为新的主机,并让其他从机和新主机建立主从关系。
哨兵模式也存在单点故障问题,如果哨兵机器挂了,那么就无法进行监控了,解决办法是哨兵也建立集群,Redis哨兵模式是支持集群的。
Sentinel 的主要功能包括 主节点存活检测、主从运行情况检测、自动故障转移 、主从切换。Redis 的 Sentinel 最小配置是 一主一从。
Redis 的 Sentinel 系统可以用来管理多个 Redis 服务器,该系统可以执行以下四个任务:
- 监控
Sentinel 会不断的检查 主服务器 和 从服务器 是否正常运行。
- 通知
当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本 向 管理员 或者其他的 应用程序 发送通知。
- 自动故障转移
当主节点不能正常工作时,Sentinel 会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他从节点指向新的主节点
- 配置提供者
在 Redis Sentinel 模式下,客户端应用 在初始化时连接的是 Sentinel 节点集合,从中获取主节点的信息。
3、redis集群
- Redis Cluster
- Twemproxy
- Codis
- 客户端分片
redis集群核心原理:gossip通信、jedis Smart定位、主备切换
六、Redis 集群方案如下:
1、什么是 Redis 分区
可能会懵逼,又是 Redis 主从复制,又是 Redis 分区,又是 Redis 集群。傻傻分不清啊!
- Redis 分区是一种模式,将数据分区到不同的 Redis 节点上,而 Redis 集群的 Redis Cluster、Twemproxy、Codis、客户端分片( 不包括 Redis Sentinel ) 这四种方案,是 Redis 分区的具体实现。
- Redis 每个分区,如果想要实现高可用,需要使用到 Redis 主从复制。
2、有哪些 Redis 分区实现方案?
(1)客户端分区方案
客户端就已经决定数据会被存储到哪个 redis 节点或者从哪个 redis 节点 读取数据。其主要思想是采用 哈希算法 将 Redis 数据的 key 进行散列,通过 hash 函数,特定的 key会 映射 到特定的 Redis 节点上。
客户端分区方案 的代表为 Redis Sharding,Redis Sharding 是 Redis Cluster 出来之前,业界普遍使用的 Redis 多实例集群 方法。Java 的 Redis客户端驱动库 Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及结合缓存池的 ShardedJedisPool。
优点:不使用第三方中间件,分区逻辑可控,配置简单,节点之间无关联,容易线性扩展,灵活性强。
缺点:客户端无法动态增删服务节点,客户端需要自行维护分发逻辑,客户端之间无连接共享,会造成连接浪费。
(2)代理分区方案
代理分区,意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些 Redis 实例,然后根据 Redis 的响应结果返回给客户端。
优点:简化客户端的分布式逻辑,客户端透明接入,切换成本低,代理的转发和存储分离
缺点:多了一层代理层,加重了 架构部署复杂度和性能损耗。
代理分区主流实现的有方案有 Twemproxy 和 Codis。
01. Twemproxy
Twemproxy 也叫 nutcraker,是 twitter 开源的一个 redis 和 memcache 的 中间代理服务器 程序。Twemproxy 作为 代理,可接受来自多个程序的访问,按照 路由规则,转发给后台的各个 Redis 服务器,再原路返回。Twemproxy 存在单点故障问题,需要结合 Lvs 和 Keepalived 做 高可用方案。
02. Codis
Codis 是一个 分布式 Redis 解决方案,对于上层应用来说,连接 Codis-Proxy 和直接连接 原生的 Redis-Server 没有的区别。Codis 底层会 处理请求的转发,不停机的进行 数据迁移 等工作。Codis 采用了无状态的 代理层,对于 客户端 来说,一切都是透明的。
(3)查询路由方案
客户端随机地 请求任意一个 Redis 实例,然后由 Redis 将请求 转发 给 正确 的 Redis 节点。Redis Cluster 实现了一种 混合形式 的 查询路由,但并不是 直接 将请求从一个 Redis 节点 转发 到另一个 Redis 节点,而是在 客户端 的帮助下直接 重定向( redirected)到正确的 Redis 节点。
开源方案 Redis-cluster
01. Redis-cluster原理
Hash slot。集群内的每个redis实例监听两个tcp端口,6379(默认)用于服务客户端查询,16379(默认服务端口 + 10000)用于集群内部通信。
-
请求重定向
客户端会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot; -
计算hash slot
hash slot的算法,就是根据key计算CRC16值,然后对16384取模,拿到对应的hash slot;
那为什么是16384呢:详解
key的空间被分到16384个hash slot里;
计算key属于哪个slot,CRC16(key) & 16384。
用hash tag可以手动指定key对应的slot,同一个hash tag下的key,都会在一个hash slot中,比如set mykey1:{100}和set mykey2:{100};
- hash slot查找
节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上;
节点间状态同步:gossip协议,最终一致性。节点间通信使用轻量的二进制协议,减少带宽占用。
16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,
gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
02. Redis-cluster请求路由方式: 客户端的路由
Redis-Cluster借助客户端实现了混合形式的路由查询
查询路由并非直接从一个redis节点到另外一个redis,而是借助客户端转发到正确的节点。根据客户端的实现方式,可以分为以下两种:包括Jedis在内的许多redis client,已经实现了对Redis Cluster的支持。
- dummy client
- smart client
Redis cluster采用这种架构的考虑:
减少redis实现的复杂度
降低客户端等待的时间。Smart client可以在客户端缓存 slot 与 redis节点的映射关系,当接收到 MOVED 响应时,会修改缓存中的映射关系。请求时会直接发送到正确的节点上,减少一次交互。
Redis Cluster 的客户端相比单机Redis 需要具备路由语义的识别能力,且具备一定的路由缓存能力。当Client 访问的key 不在当前Redis 节点的 slots 中,Redis 会返回给Client 一个moved命令。并告知其正确的路由信息,如下所示:
当Client 接收到moved 后,再次请求新的Redis时,此时Cluster 的结构又可能发生了变化(slot 迁移)。此时有可能再次返回moved 。Client 会根据 moved响应,更新其内部的路由缓存信息,以便后续的操作直接找到正确的节点,减少交互次数。
基于重定向的客户端,很消耗网络io,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点; 所以大部分的客户端比如java redis客户端,都是jedis,都是smart的,
本地维护一份hashslot -> node的映射表在缓存里,大部分情况下直接走本地缓存就可以找到hashslot -> node,不需要通过节点进行moved重定向;
(4)高可用性与主备切换原理
redis-cluster群集高可用架构:参考
01 判断节点宕机
如果一个节点认为另外一个节点宕机,name就是pfail,主观宕机;
如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown;
在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail;
如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail;
02 从节点过滤
对宕机的master node,从其所有的slave node中,选择一个切换成master node;
检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master;
03 从节点选举
哨兵:对所有从节点进行排序,slave priority,offset,run id;
每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举;
所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master;
从节点执行主备切换,从节点切换为主节点;
04 与哨兵比较
整个流程跟哨兵相比,非常类似,所以说,redis cluster功能强大,直接集成了replication和sentinal的功能;
redis集群核心原理: 转载自 blog.csdn.net/qq\_3381462…
七、分布式寻址算法
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) 算法
- redis cluster 的 hash slot 算法
1、hash 算法
来了一个 key,首先计算 hash 值,然后对节点数取模。使用公式:hash(key)% N计算出 哈希值,用来决定数据 映射 到哪一个节点上。 然后打在不同的 master 节点上。
一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
2、一致性 hash 算法
一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
3、redis cluster 的 hash slot 算法
redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。
redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。
任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
八、持久化
Redis 提供 RDB 和 AOF 两种数据的持久化存储方案,解决内存数据库最担心的万一 Redis 挂掉,数据会消失。
Redis 提供了两种方式来将数据持久化到硬盘。
-
1、【全量】RDB 持久化,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。实际操作过程是,fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。这样的好处就是可以 copy-on-write。
快照形式是直接把内存中的数据以快照的方式保存到一个 二进制文件=dump.rdb 文件中,定时保存,保存策略。 当然我们也可以手动执行 save 或者 bgsave(异步)做快照。 Redis默认是快照RDB的持久化方式 默认 Redis 是会以快照 “RDB”的形式,将数据持久化到磁盘的,一个二进制文件,dump.rdb
-
2、【增量】AOF持久化,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 使用 AOF 做持久化,每一个写命令都通过write函数追加到 appendonly.aof 中
1、RDB持久化
定期触发:
redis通过配置文件中save参数定义了RDB中自动保存条件,以下是默认配置:
手动触发:
下面命令可以手动生成rdb文件,redis提供了两个命令来生成rdb文件,分别是save和bgsave,他们的区别就在于是否在【主线程】 里执行:
- 执行了save命令,就会在主线程生成rdb文件,由于和执行操作命令在同一个线程,所以如果写入 rdb 文件的时间太长,会阻塞主线程;
- 执行了bgsave命令,会创建一个子进程来生成rdb文件,这样可以避免主线程的阻塞;
2、AOF 持久化
AOF 是一种基于日志的持久化方式,它通过记录 Redis 服务器接收到的每个写操作来实现数据持久化。AOF 的恢复流程如下:
1-AOF 文件:
Redis 将每个写操作追加到 AOF 文件的末尾。AOF 文件包含一个完整的操作日志,描述了服务器执行的所有写操作。
2-重放 AOF 文件:
当 Redis 服务器重启时,它会尝试通过重放 AOF 文件中的写操作来重建数据集的状态。
Redis 服务器会按照 AOF 文件中写操作的顺序,逐条执行这些操作,将数据集恢复到重启前的状态。
3-AOF 文件的持久化:
配置方式:启动 AOF 持久化的方式/保存模式/同步/回写策略
appendfsync always:每次写操作都会同步到 AOF 文件,是最安全方式但也最慢。
appendfsync everysec:每秒同步一次到AOF文件,提供了很好的性能和持久性的折中。
appendfsync no:默认是no,完全依赖于操作系统的 fsync。
这种方式速度最快,但数据可能会有一定的丢失风险。
appendonly yes开启 AOF
Redis 提供了不同的 AOF 持久化策略,可以通过配置文件设置:Redis.conf配置文
4-AOF文件写入和保存
每当服务器常规任务函数被执行或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
WRITE:根据条件,将
aof_buf中的缓存写入到 AOF 文件。
SAVE:根据条件,调用fsync或fdatasync函数,将 AOF 文件保存到磁盘中。
两个步骤都需要根据一定的条件来执行, 而条件由 AOF 所使用的保存模式(上面第3点:aof文件持久化)来决定,
以下就介绍 AOF 所使用的三种保存模式, 且在这些模式下步骤WRITE和SAVE的调用条件
1、不保存
这种模式下每次调用
flushAppendOnlyFile函数, WRITE 都会被执行, 但 SAVE 会被略过。在这种模式下, SAVE 只会在以下任意一种情况中被执行:
Redis 被关闭
AOF 功能被关闭
系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
2、每一秒钟保存一次
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程调用的, 所以它不会引起服务器主进程阻塞。
3、每执行一个命令保存一次
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
引用:redisbook.readthedocs.io/en/latest/i…
5-什么是 AOF 重写?
AOF 是 Redis 持久化的一种方式,其通过记录 Redis 执行的每一条命令,重启之后通过重新执行 Redis 中的命令来恢复数据。
AOF 重写就是指通过当前状态,重新生成最新的 AOF 操作命令记录的过程。
不过,随着 Redis 执行命令的不断增多,AOF 文件越来越大,但是很多数据其是不一定都是有意义的,比如原来 set age 10,后面又来个 set age 18,然后又来个 set age 30,我们这个时候就会发现,只有最后一个 age 是有意义的,前面都是没有用的。
6-AOF 的重写流程/重写原理?
AOF 文件的重写流程主要就一句话,“一次拷贝,两处日志”。
-
一次拷贝:重写发生的时候,主进程会 fork 出一个子进程,然后子进程与主进程共享 Redis 物理内存,让子进程将这些 Redis 数据写入重写日志。===子进程重写
-
两处日志:重写发生的时候,我们需要注意 AOF 缓冲和 AOF 重写缓冲=aof现有文件;当数据进行重写的时候,如果此时有新的写入命令执行,会由主进程分别写入 AOF 缓冲和 AOF 重写缓冲;AOF 缓冲用于保证此时即使发生宕机了,原来的 AOF 日志也是完整的,可以用于恢复。AOF 重写缓冲用于保证新的 AOF 文件也不会丢失最新的写入操作。
===主进程进行命令请求,写到现有aof文件和aof重写缓存中
- 当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作: ======主线程中执行,造成阻塞
步骤1:将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。
步骤2:对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
当步骤 1 执行完毕后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替。
- 这个信号处理函数执行完毕之后, 主进程就可以继续像往常一样接受命令请求了。
在整个 AOF 后台重写过程中, 只有最后的写入缓存和改名操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。
以上就是 AOF 后台重写, 也即是 BGREWRITEAOF 命令的工作原理。
1、Redis 7.0 重写存在的问题: 上面的流程是,Redis 7.0 之前的方案,在这个过程中,我们会发现两个核心点:
1)AOF 重写缓冲区和 AOF 缓冲区内的数据是一样的;
2)父子进程之间传输数据这个操作耗时太久了;
-------------------------------------------------------------------------------
2、那 Redis 7.0 之后发生了什么呢,接下来让我们来探究一下 MP-AOF。
Multi Part AOF, 即将原来一个 AOF 文件拆分成多个 AOF 文件,在 Multi Part AOF 中,文件被分成 3 种类型,分别如下:
Base:表示基于 AOF 文件,记录了基本的命令,一般由子进程通过重写产生,只有一个。
Incr:表示增量 AOF 文件,记录了重写过程中新增的操作指令,一般会在 AOF 重写开始执行时被创建,该文件可能存在多个。
History:表示历史 AOF 文件,他主要由 Base 和 Infr 变化而来,每次 AOF 重写完成后,本次 AOF 重写之前对应的 Base 和 Incr 的 AOF 文件都会变成 Histroy,History 类型的 AOF 会被 Redis 自动删除。
然后为了管理 AOF 文件,我们引入了一个 mainfest(清单)文件来跟踪和管理这些 AOF。
于此同时,为了方便 AOF 备份和拷贝,我们将所有的 AOF 文件和 mainfest 文件放到一个单独的目录文件目中,目录文件名由 appenddirname配置(Redis 7.0新增配置项)决定。
下图是 Multi Part AOF 的流程:
从图中我们可以看到,现在重写阶段,只需要在主进程中将新请求数据写到 AOF 缓存中就可以了,然后 AOF 缓冲区的文件最后形成 Incr AOF 日志的内容,然后在子进程中,根据数据库 fork 的数据,生成一个 Base AOF 文件,两者合一构成 Redis 的全部数据。
然后在 AOF 重写结束后,主进程会更新 mainifest 文件的内容,将新生成的 Base AOF 文件和 Incr AOF 文件的内容加入,并且将之前的 Base AOF 文件和 Incr AOF 文件标记为 Histroy,然后等待 Redis 异步删除。
到此为止,我们会发现一件事,现在的 AOF 重写有了很大的改进:
-
在 AOF 重写期间不需要 AOF 重写缓冲区了
-
然后也不需要父子进程通过管道进行数据传输
从一定程度上减缓了 CPU、内存、磁盘的性能损耗,也降低了 Redis 的代码复杂度。
-----------------------------------------------------------------------------------
7-AOF 后台重写的触发条件
AOF 重写可以由用户通过调用 BGREWRITEAOF 手动触发。
另外, 服务器在 AOF 功能开启的情况下, 会维持以下三个变量:
记录当前 AOF 文件大小的变量 aof_current_size 。
记录最后一次 AOF 重写之后, AOF 文件大小的变量 aof_rewrite_base_size 。
增长百分比变量 aof_rewrite_perc 。
每次当 serverCron 函数执行时, 它都会检查以下条件是否全部满足, 如果是的话, 就会触发自动的 AOF 重写:
没有 BGSAVE 命令在进行。
没有 BGREWRITEAOF 在进行。
当前 AOF 文件大小大于server.aof_rewrite_min_size(默认值为 1 MB)。
当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定增长百分比。
默认情况下, 增长百分比为 100% , 也即是说, 如果前面三个条件都已经满足, 并且当前 AOF 文件大小比最后一次 AOF 重写时的大小要大一倍的话, 那么触发自动 AOF 重写。
3、恢复机制对比,如何选择
-
RDB 恢复:由于 RDB 是一个快照文件,恢复速度相对较快,但是可能会丢失最后一次快照后的修改。
-
AOF 恢复:AOF 通过重放操作日志来恢复数据,因此通常情况下能够保证数据更为完整,但可能会因为重放时间较长而导致恢复速度较慢。
选 RDB 还是 AOF 作为持久化方式,取决于对数据完整性/恢复速度以及系统性能的不同要求。
实际生产中的做法:RDB + AOF 双持久化
Redis 4.0 开始支持 混合持久化,结合 RDB 快照 + AOF 增量
Redis 通过 RDB 快照和 AOF 日志两种机制来保证断电后数据可恢复。RDB 提供定期快照,AOF 追加写操作日志,生产中一般结合使用,在重启时优先加载 RDB,然后用 AOF 补齐增量,最大程度避免数据丢失。
如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久。 AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低 Redis 的性能,不知道你是否可以接受。
数据库备份和灾难恢复:定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快。
--------------------------------------------------------------
九、redis的过期策略
参考:在面试pdd的时候有问到相关的问题:
知识点
1. 所有的过期指令最终都变成了PEXPIREAT指令
2. redis保存一份过期字典,键是一个指针,指向某个键对象,值是过期时间,一个毫秒精度的时间戳。过期字典与键空间中的键指向的是一个地址,不存在空间浪费。
过期键删除策略:
-
1. 定时删除。每次创建一个过期时间的同时,创建一个定时器。这种方式内存最友好,但是CPU最不友好。
-
2. 惰性删除。每次使用时检查是否删除。内存最不友好,CPU最友好。可能会存在大量无用键。
-
3. 定期删除。
redis采用的是惰性删除跟定期删除。
十、redis的内存淘汰策略
在Redis中,允许用户设置最大使用内存大小server.maxmemory,当Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
- 1.volatile-lru:从已设置过期的数据集中挑选最近最少使用的淘汰
- **2volatile-lfu ** (Redis 4.0 引入) 从 设置了过期时间的键 中,使用 LFU 算法,淘汰访问频率最低的键
- 3.volatile-ttr:从已设置过期的数据集中挑选将要过期的数据淘汰
- 4.volatile-random:从已设置过期的数据集中任意挑选数据淘汰
- 5.allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
- 6 allkeys-lfu (Redis 4.0 引入),从 所有键 中,使用 LFU(最不经常使用) 算法,淘汰访问频率最低的键
- 7.allkeys-random:从数据集中任意挑选数据淘汰
- 8.noenviction:禁止淘汰数据
redis淘汰数据时还会同步到aof
十一、redis分布式锁及会出现的问题
redis分布式锁:
1)、SET命令扩展参数 (推荐方式)
SET lock_key unique_value NX PX 10000
NX:等同于SETNX,仅当key不存在时设置
PX:设置过期时间(毫秒)
优势:原子性操作,避免锁无法释放
2)、RedLock算法 (多实例分布式锁)
用于解决单点Redis的可靠性问题,需要在多个独立Redis实例上获取锁
RedLock 是 Redis 官方提出的分布式锁算法(由 Redis 作者 Antirez 设计),用于在多个独立 Redis 节点上实现高可用的分布式锁。
Redisson 是 Java 的 Redis 客户端库,实现了 RedLock 算法,并对其进行了封装和增强。
redlock可以自行实现,而对于java来说直接使用redisson框架更方便!!!
------------------------------
分布式锁的常见问题及解决方案
1、续约问题
假想这样一个场景,如果过期时间为30S,A线程超过30S还没执行完,但是自动过期了。这时候B线程就会再拿到锁,造成了同时有两个线程持有锁。这个问题可以归结为”续约“问题,即A没执行完时应该过期时间续约,执行完成才能释放锁。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续约”。
其实,后面解锁出现的删除非自己锁,也属于“续约”问题。
解决方案:
1、合理设置锁超时时间(大于业务执行时间)
2、实现锁续期机制(watchdog模式)
2、集群同步延迟问题
用于redis的服务肯定不能是单机,因为单机就不是高可用了,一量挂掉整个分布式锁就没用了。在集群场景下,如果A在master拿到了锁,在没有把数据同步到slave时,master挂掉了。B再拿锁就会从slave拿锁,而且会拿到。又出现了两个线程同时拿到锁。
基于以上的考虑,Redis 的作者也考虑到这个问题,他提出了一个 RedLock 的算法。这个算法的意思大概是这样的:假设 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。
通过以下步骤获取一把锁:
- 获取当前时间戳,单位是毫秒。
- 轮流尝试在每个 Master 节点上创建锁,过期时间设置较短,一般就几十毫秒。
- 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点(n / 2 +1)。
- 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
- 要是锁建立失败了,那么就依次删除这个锁。
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。这个问题的根本原因就是redis的集群属于AP,分布式锁属于CP,用AP去实现CP是不可能的。
3、Redisson
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。
Redisson通过lua脚本解决了上面的原子性问题,通过“看门狗”解决了续约问题,但是它应该解决不了集群中的同步延迟问题。
redisson中的看门狗机制总结,结构如下:
参考:
youzhixueyuan.com/reasons-for…