-
Redis cpu过高,单个分片还是整个分片
-
1、 请求高,负载不均衡
-
2、 大量创建链接。
-
3、 执行复杂命令 keys
-
4、 大keys 操作过于频繁
-
5、 执行持久化操作。
-
缓存类型:
-
Redis是一个支持持久化的内存数据库;通过持久化机制把内存中的数据同步到硬盘文件中。当Redis重启后通过把硬盘文件重新加载到内存,以达到恢复数据的目的。可以把他作为数据库,缓存,消息中间件来使用。他是一个非关系数据库,采用key-value存储Redis 默认端口号:6379
redis单线程模型:
-
Redis单线程模型中最为核心的就是文件事件处理器,文件事件处理器结构包含5个部分,多个socket、IO多路复用程序、socket就绪队列,不存数据只存就绪的socket、文件事件分派器、以及事件处理器。而事件处理器又分为3个部分为:连接应答处理器、命令请求处理器、命令回复处理器。
-
当客服端向redis发生请求时,首先会在对应的socket上会产生事件,该事件后会被IO多路复用程序监听到,然后IO多路复用程序会把监听到的socket信息放入到队列中,事件分配器每次从队列中取出一个socket(步骤D),然后事件分派器把socket给对应的事件处理器。
-
IO多路复用就是用epoll/poll/select。 Windos下select,LINUX下用epool。因为epoll在linux下才能使用。
当io多路程序发现有数据可读的时候,唤醒主线程” → 读取该客户端数据到其输入缓冲区 → 解析该缓冲区中的命令(按 RESP 协议) → 将解析后的命令加入执行队列 → 按顺序执行队列中的命令。
- 单线程处理所有事件 :主线程同一时间只能处理一个客户端的事件(读取、解析、执行)。
- 客户端输入缓冲区独立 ****:每个客户端有独立的输入缓冲区(client->querybuf),存储该客户端尚未被解析的数据。
- Redis 持久化机制。
RDB
-
RDB(Redis Database)是Redis默认的持久化方式。按照一定的策略把内存的数据以快照的形式保存到硬盘的二进制文件。
-
Redis配置文件redis.conf中的部分配置项
-
save <seconds> <changes>
-
seconds表示多少秒内如果超过<changes>次修改则自动保存RDB文件
-
changes表示多少次修改才自动保存RDB文件
-
示例:每60秒内如果有至少1000次修改则自动保存RDB文件(save 60 1000)
* 还提供两个命令SAVE 和 BGSAVE。
-
Save:在执行时会直接阻塞当前的线程,保证了备份文件的一致性
-
BGSAVE是通过子进程实现的。实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中。然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件。
-
优点:
-
RDB是一个快照文件,数据很紧凑,它保存了 Redis 在某个时间点上的数据集,体积比较小
-
RDB 适合用于灾难恢复,因为它只有一个文件,而且体积小,方便拷贝。相对于AOF,他的启动效率更高。
-
缺点:
-
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失,所以这种方式更适合数据要求不严谨的时候。
-
另外fork() 可能会非常耗时,造成服务器在某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。
- AOF(Append Only File)
- Redis会将每一个收到的写命令都通过Write函数追加到文件最后。因为是内存操作,所以AOF为避免将命令直接写入到磁盘,搞了一个AOF 缓冲区, *** AOF 缓冲区三种写回策略**
- 三种写回策略,appendfsync
- always 同步写回,每个写命令执行完立刻同步地将日志写回磁盘。
- everysec 每秒写回,每个写命令执行完,只是先把日志写到AOF缓冲区,每隔1s把缓存区地数据写入磁盘
- NO 操作系统控制协会,只是将日志先写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
- 当Redis重启时,会通过重新执行文件中保存的写命令,在内存中重建整个数据库的内容。
- 优点:
- 1、数据安全性高,每进行一个命令就记录一个到aof文件一次
- 缺点:
- 1、AOF 文件比 RDB 文件大,且恢复速度慢。
- 2、数据集大的时候,比 rdb 启动效率低。
- 开启AOF
混合方式持久化
- 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复,但是不建议只开启AOF
- 另外可以开启混合方式设置。(redis4),满足策略save <seconds> <changes>或手动触发进行RDB操作。
- 前置条件必须开启AOF,因为混合模式下RDB的本质是用通过重写AOF命令实现的。
- AOF重写机制:写操作命令越来越多,文件的大小会越来越大,恢复的时候就比较慢。当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。 AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到新文件,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的。
注意
- 千万不要觉得有完善的持久化机制就OK了,持久化的时候有可能会持久化不成功,比如持久化的时候服务器的磁盘有问题,网络有问题,ssh无法连接,agent、流量大等等,都会导致rdb失败。所以某家公司只有在凌晨的时候才做一次RDB备份,平时都不备份。(🤣笑死)
Redis 事务
- 它先以 MULTI 开始一个事务,然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务,一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞。如果想取消事务可以执行discard命令。收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。所以redis 事务并不保证原子性。
- 使Redis的WATCH命令用于在事务中监视一个或多个键,如果被监视的键值发生变化,事务将被终止 。
- 在Redis中,使用WATCH命令可以在事务执行前监控某些键值对,然后使用MULTI命令开启事务,执行各类对数据结构进行操作的命令,这些命令会进入队列。当使用EXEC命令执行事务时,Redis会比对被WATCH命令监控的键值对,如果没有发生变化,则执行事务中的命令;如果发生变化,则取消事务的执行,直接取消。
- 无论事务是否执行成功,最后都需要执行unWATCH。取消对watch值的监测。
- 因为jimdb只能在单个分片中保证事务,所以如果操作的key落在多个分片,jimdb sdk会立即返回失败。为了保证要操作的key落在一个分片,可以在申请jimdb集群的时候就开启hashtag,然后用hashtag的方式组织key。
- 原子操作:redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,就不需要在本地使用繁琐的事务或者使用锁机制,任何能在事务机制下能完成的任务lua脚本也能做到。
- 相关文章:
- redisbook.readthedocs.io/en/latest/f…
批量处理pipLine
-
如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了RTT(Round Time Trip),而且还频繁调用系统IO,发送网络请求,同时需要redis调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好。
-
Pipline是指,先把指令缓存到客服端,最后flush一下子传输给服务端,服务端pipeline实现的原理是队列,先进先出特性就保证数据的顺序性,在执行完所有命令,在把所有的结果一次性返回客服端。;
-
如果一次flush过多将会影响客服端方法性能,既上面所说。
-
这里所得原子性并不是mysql的原子性,而是,不能加塞。因为事务返回的是一个queue,Pipline 虽然打包一起但是可以加塞,另外虽然可以加塞,但是redis是单线程,过长会导致时间响应过长。
当io多路程序发现有数据可读的时候,唤醒主线程” → 读取该客户端数据到其输入缓冲区 → 解析该缓冲区中的命令(按 RESP 协议) → 将解析后的命令加入执行队列 → 按顺序执行队列中的命令。
-
单线程处理所有事件 :主线程同一时间只能处理一个客户端的事件(读取、解析、执行)。
-
客户端输入缓冲区独立 :每个客户端有独立的输入缓冲区(client->querybuf),存储该客户端尚未被解析的数据。
“插入”现象的本质:事件轮询的顺序性
所谓“Pipeline 命令被其他命令插入”,本质是 主线程在处理不同客户端的事件时,按轮询顺序交替处理 ,而非“在解析 Pipeline 命令的过程中直接插入其他命令”。具体场景如下:
场景1:客户端 A 发送 Pipeline,客户端 B 发送普通命令
-
时间线 :
-
- t1:客户端 A 发送 Pipeline(命令1、命令2、命令3),服务端通过 IO 多路复用检测到 A 的 Socket 可读,读取数据到 A 的输入缓冲区(此时缓冲区有命令1、命令2、命令3的部分数据)。
- t2:主线程开始解析 A 的输入缓冲区,提取出命令1(假设数据足够完整),将其加入执行队列。
- t3:主线程检测到客户端 B 的 Socket 可读(B 发送了命令4),切换处理 B 的事件:读取数据到 B 的输入缓冲区,解析命令4并加入执行队列(此时执行队列:[命令1, 命令4])。
- t4:主线程回到 A 的输入缓冲区,继续解析剩余数据(命令2、命令3),加入执行队列(最终队列:[命令1, 命令4, 命令2, 命令3])。
-
结果 :命令4(客户端 B)被插入到 Pipeline 命令1和命令2之间执行,看似“打断”了 Pipeline,但这是主线程 轮询处理不同客户端事件 的结果。
为何单线程解析无法“阻止”插入?
单线程解析的限制是: 主线程同一时间只能处理一个客户端的解析任务 ,但无法阻止其他客户端的事件被触发。具体原因如下:
1. IO 多路复用的事件驱动特性
IO 多路复用(如 epoll)会实时通知主线程“哪个客户端的 Socket 可读”。当客户端 B 的 Socket 可读时,主线程必须优先处理该事件(否则数据会滞留缓冲区),否则可能导致客户端 B 的请求超时。
2. 输入缓冲区的独立性
每个客户端的输入缓冲区是独立的,主线程处理客户端 A 的解析时,客户端 B 的数据已被读取到自己的缓冲区中。即使主线程暂时不处理 B 的解析,B 的数据也会在缓冲区中等待,直到主线程轮询到 B 的事件。
3. 命令执行队列的 FIFO 性质
所有解析后的命令(无论来自哪个客户端)都会被加入同一个执行队列,按入队顺序执行。因此,若客户端 B 的命令在 A 的 Pipeline 命令解析过程中被解析并加入队列,就会出现在 A 的后续命令之前。
相关概念:
* 缓存雪崩
- 我们可以简单的理解为:缓存中数据大批量到过期时间,所有的查询都转向了数据库。例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期。所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
- 解决方法:
- 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
- 还有一个简单方案就是将缓存失效时间分散开。
缓存击穿(同一条数据):
- 指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
- 解决方法:加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,设置热点数据永不过期。
缓存穿透:
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
解决方法:最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。////hbase就是这样做的
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟
布隆过滤器:
-
(1.开放定址法,线性探测再散列,二次探测再散列,伪随机探测再散列 2.再哈希法 3.链地址法(Java hashmap就是这么做的) 4.建立一个公共溢出区)
-
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个key的hash值有可能相同。为了减少冲突,引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
-
他的优点:空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率。
-
基本的布隆过滤器不支持删除操作,原来的位数组中的每一位由 bit 扩展为 n-bit 计数器
常见的布隆过滤器
| 语言/平台 | 库/工具 | 特点 |
|---|---|---|
| Java | Guava BloomFilter | 线程安全,支持自定义误判率,动态扩容和序列化 |
| Redis | RedisBloom | 分布式布隆过滤器,支持持久化和集群 RedisBloom 扩展模版 |
RedisBloom 的动态扩容机制通过 分层(Layer)设计 实现,当初始布隆过滤器容量不足时,自动创建新层来扩展容量,同时通过逐层降低误判率,保证整体误判率可控;底层用的用的bitmap,SDS;
缓存预热
- 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统(热点数据,双十一秒杀)。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题
* 查询所有的keys
- Keys *
- keys 前缀* 后面的参数跟通配符来列出所有符合的key
- 由于KEYS命令一次性返回所有匹配的key,所以,当redis中的key非常多时,对于内存的消耗和redis服务器都是一个隐患。
- SCAN 每次执行都只会返回少量元素,所以可以用于生产环境。
- SCAN命令是一个基于游标的迭代器。这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程,当SCAN命令的游标参数(即cursor)被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。
- 当用模式匹配时,可能满足匹配的元素很少。可能会返回空。可以使用count让redis
- 扫描更多的数据。
* redis的删除策略以及内存淘汰机制。
-
redis采用的是定期删除+惰性删除策略。
-
Redis采用定期删除+惰性删除
-
定期删除,redis默认每隔100ms,随机抽取检查key是否过期,过期就删除。因为随机抽取,会造成很多过期的key未被删除。
-
懒惰删除,也就是说在你获取某个key的时候,redis会检查一下,这个key是否过期。如果过期了此时就会删除。不过期或者key没有设置过期时间就不会被删除。
-
如果key既没有被定期删除,又没有惰性删除。Redis积累的过期key越来越多。当使用内存超过redis设置的内存,将采用内存淘汰机制淘汰进行淘汰。优先会已过期的数据
-
Noeviction(redis3) 不进行淘汰,如果满了就报错。
-
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。(先进先出)
-
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
-
volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
-
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
-
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
-
allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
-
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
-
Redis的lru和fru都是进行的算法。不可能把redis里的所有key全遍历一遍,而是随机选取一部分key,排序淘汰。
redis可用性模型
-
主从模式
-
主从模式包含两部分,主从复制和哨兵模式。
Redis 主从模式下的同步机制
- 同步复制(Sync Replication)
- 原理
- 在同步复制模式下,当主节点(Master)接收到一个写操作时,它会将这个写操作记录到内存中的复制缓冲区(Replication Buffer),然后将写操作发送给从节点(Slave)。从节点接收到写操作后,会在本地执行这个写操作,并将执行结果反馈给主节点。只有当主节点收到从节点的确认信息后,才会认为这个写操作完成。
- 优点
- 数据一致性高。由于主节点必须等待从节点的确认,所以在任何时刻,主从节点的数据都是一致的。
- 缺点
- 性能较低。因为主节点需要等待从节点的响应,这会增加写操作的延迟,特别是在网络延迟较高或者从节点数量较多的情况下,主节点的性能会受到较大影响。
- 异步复制(Async Replication)
- 原理
- 主节点接收到写操作后,会立即将写操作记录到复制缓冲区,然后将写操作发送给从节点。但是,主节点不会等待从节点的确认,而是继续处理其他请求。从节点在接收到写操作后,会在本地执行这些操作,但主节点不会关心从节点是否执行成功。
- 优点
- 性能高。主节点不需要等待从节点的响应,所以写操作可以快速处理,适用于对性能要求较高的场景。
- 缺点
- 数据一致性较差。在某些情况下,例如主节点突然宕机,从节点可能还没有来得及执行主节点发送过来的写操作,这就会导致数据不一致。
- 半同步复制(Semi - Sync Replication)
- 原理
- 半同步复制是同步复制和异步复制的结合。当主节点接收到一个写操作时,它会将写操作发送给从节点,并等待至少一个从节点的确认。一旦有一个从节点确认收到并执行了写操作,主节点就认为这个写操作完成,可以继续处理其他请求。如果在一定时间内没有从节点确认,主节点可能会切换回异步复制模式(这取决于具体的配置)。
- 优点
- 兼顾了数据一致性和性能。在保证一定程度的数据一致性的同时,也不会像同步复制那样严重影响性能。
- 缺点
- 配置相对复杂。需要根据实际情况合理设置等待从节点确认的时间等参数,否则可能无法达到预期的效果。
- 在实际应用中,需要根据业务对数据一致性和性能的要求来选择合适的复制模式。例如,对于对数据一致性要求极高的金融业务,可能更倾向于使用同步复制或半同步复制;而对于对性能要求极高、对数据一致性要求相对较低的缓存业务,异步复制可能是更好的选择。
做一个从库
-
redis主从复制实现,主节点可读可写,当发生写操作的时候自动将数据同步到从数据库,从节点slave只读,并接收主数据库同步过来的数据。一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。主从模型,读写分离可以提高读的能力,在一定程度上缓解了写的能力,保证了数据一致性。
-
每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样)。主从节点初次复制时,主节点将自己的runId和offset发送给从节点,从节点将这个runId,offset保存起来。
-
初次全量复制时主节点(master)会执行bgsave命令生成RDB,并使用一个「复制积压缓冲区」记录从现在开始执行的所有写命令复制积压缓冲区。所以主节点响应写指令时,不但会把命令发送给从节点,还会写入积压缓冲区。
-
从节点接收完主节点传送来的全部数据后会清空自身旧数据,加载RDB, 从节点成功加载完RDB后。从库会向主库发送一个 PSYNC 命令(在 Redis 2.8 及以上版本),这个命令用于请求主库将从主库执行 bgsave 命令生成 RDB 文件之后在复制积压缓冲区(replication backlog)中累积的写命令发送过来。如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作。
-
从节点每间隔一秒上报自己的偏移量,来检查数据是否有丢失,如果有丢失就从主节点上的复制缓存区拉取丢失的数据。当断线重连 ,如果从节点保存的runid与主节点现在的runid不同,进行全量复制。如果 master 发现从节点的偏移量是在缓冲区的范围内[对比从节点的偏移量是否在主节点的偏移量范围内],就会返回 continue 命令。不在这个范围内就执行全量复制。
哨兵(Sentinel)模式:
-
但是主从模式有个缺点,就是当主节点宕机了,整个集群就没有可写的节点了。
-
所以使用sentinel。
-
哨兵模式:Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance)。
-
同一个哨兵下的不同主从模式彼此相互独立,一个主从模式可能对应多个哨兵(N对N)。这些哨兵不但监视主从数据库,还相互监视。
| 场景 | 触发机制 | 示例 |
|---|---|---|
| 常规状态同步 | 周期性任务(每秒1次) | 哨兵间交换 PING/PONG |
| 主节点故障检测 | 事件驱动(PING 超时→触发 +sdown) | 标记主节点主观下线 |
| 故障转移决策 | Raft 变种协议 + 事件驱动 | 哨兵领导者选举 |
-
执行以下三个任务进行监控基于获取信息:
-
Redis Sentinel通过三个定时监控任务完成对各个节点的发现和控制:
-
1)每隔10秒,每个sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构。通过解析info replication可以找到相应的从节点。此定时任务作用于三个方面:
-
- 通过向主节点执行info命令,获取从节点的信息。
-
- 当有新的节点加入时,可以立刻感知出来。
-
- 节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息。
-
2)每隔2秒,【订阅/发布机制】每个sentinel节点会向Redis数据节点的__sentinel__:hello频道上发送该sentinel节点对于主节点的判断以及当前sentinel节点的信息,同时每隔个sentinel节点也会订阅该频道,来了解其他sentinel节点以及他们对主节点的判断,因此此任务主要作用如下:
-
-初始化组件哨兵集群,发现新的sentinel节点
-
- sentinel节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据。
-
3)每隔1秒,每隔sentinel节点会向主节点、从节点、其他sentinel节点发送一条ping命令做一次心跳检测。
-
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。在sentinel网络中,只要还有一个sentinel或者就可以实现故障切换
-
故障切换的过程:
-
当任何一个Sentinel发现被监控的Master下线时,会通知其它的Sentinel开会,投票确定该Master是否下线(半数以上,所以sentinel通常配奇数个)Raft选举算法:。
-
当Sentinel确定Master下线后,会在所有的Slaves中,选举一个新的节点,升级成Master节点。其它Slaves节点,转为该节点的从节点当原Master节点重新上线后,自动转为当前Master节点的从节点。
-
选取slave时,先根据优先级选,在根据偏移量选。
* Cluster集群模式(分布式缓存):【槽内再找就是遍历】
-
集群是Redis提供的分布式方案,一个Redis集群通常由多个(主从模式eg,一组三从)分片组成,每个分片相互独立。最多支持1000个分片
-
Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念。Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16383取模来决定放置哪个槽。
-
Cluster中的每个分片负责一部分hash槽(hash slot),但是采用了虚拟分片的原理,减少数据倾斜。 如分片A负责0-10000,分片B负责10001-20002;和一致性hash不同。
扩容的时候怎么实现高可用
1、迁移数量少:
提前分配槽位,扩容时只需计算哪些槽需迁移,避免全局数据重哈希(一致性哈希的 1/N 迁移问题)。
2、非阻塞迁移:
Redis 使用 多线程异步迁移(Redis 4.0+),源节点仍可正常处理请求。进行双重读写控制, 对未迁移的槽进行写操作的时候,先在源节点执行写操作,同步转发写操作到目标节点, 成功后返回客户端响应。
客户端透明:客户端通过 MOVED 重定向自动切换到新节点,无需停机。 MOVED 响应: 当客户端访问已迁移完成的槽时,Redis 返回 MOVED target-node-ip:port,客户端更新本地槽缓存。 ASK 临时重定向: 当客户端访问正在迁移的槽时,返回 ASK target-node-ip:port,客户端仅对本次请求重定向到目标节点,不更新缓存。
- 一致性hash算法[www.xiaolincoding.com/os/8_networ…]
| 维度 | Redis Cluster 分槽 (16384 Slots) | 一致性哈希 (Consistent Hashing) |
|---|---|---|
| 数据分布 | 预分配固定槽位,完全均匀 | 依赖哈希环,可能倾斜(需虚拟节点优化) |
| 扩容/缩容 | 仅需迁移受影响槽的数据,可控性强 | 需迁移约 1/N 的数据(N为节点数),成本高 |
| 计算复杂度 | 直接映射槽位,O(1) 时间复杂度 | 需遍历哈希环或跳表,O(log N) |
| 热点问题 | 可通过手动迁移槽解决 | 天然无法规避热点 |
| 集群管理 | 槽位信息集中存储,易于维护 | 无全局视图,需客户端维护哈希环 |
| 故障转移 | 槽为单位迁移,粒度更细 | 节点为单位迁移,影响范围大 |
-
为什么是16384个
-
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。
-
Redis的底层数据类型,以及每种数据类型的使用场景
-
底层redisObject
- 一共有五种
String
- 一个键能存储512MB数据,是二进制安全的,可以存储任何数据,比如jpg图片或序列化对象(get,set,incrby ,decrby,strlen)
- String 字符串类型的内部编码有三种
- 1、int:当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储。
-
Redis启动时会预先建立10000个分别存储0 - 9999的redisObject变量作为共享对象,这就意味着如果set字符串的键值在0~10000之间的话,则可以直接指向共享对象而不需要再建立新对象,此时键值不占空间 。
-
当大于19位用embstr,大于44个字节用embsr
-
2、embstr:代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串), 存储小于 44 个字节的字符串,只分配一次内存空间(而且 Redis Object 和 SDS 是连续的避免空间碎片)。
-
3、raw:存储大于 44 个字节的字符串(3.2 版本之前是 39 个字节),需要分配两次内存空间(分别为 Redis Object 和 SDS 分配空间)
LIST
- list按照插入顺序排序。支持添加一个元素到列表头部(左边)或者尾部(右边)的操作,底层用的quicklist.
HASH
-
hash 键值对的集合,value特别适合于存储结构化对象。(
-
当满足下列条件的时候用压缩表
-
哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
-
哈希对象保存的键值对数量小于512个、)其他时候用hashtable
-
渐进式rehash
-
过程:{
-
为ht[1]分配空间,这个过程和普通Rehash没有区别;
-
将rehashidx设置为0,表示rehash工作正式开始,同时这个rehashidx是递增的,从0开始表示从数组第一个元素开始rehash。
-
在rehash进行期间,每次对字典执行增删改查操作时,顺带将ht[0]哈希表在rehashidx索引上的键值对rehash到 ht[1],完成后将rehashidx加1,指向下一个需要rehash的键值对。
-
随着字典操作的不断执行,最终ht[0]的所有键值对都会被rehash至ht[1],再将rehashidx属性的值设为-1来表示 rehash操作已完成。
-
}
-
渐进式 rehash的思想在于将rehash键值对所需的计算工作分散到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的阻塞问题。比如更新删除查询,一个值先更新ht[0],然后再更新ht[1]。而新增操作直接就新增到ht[1]表中,ht[0]不会新增任何的数据,这样保证ht[0]只减不增,直到最后的某一个时刻变成空表,这样rehash操作完成。
-
为什么java不用渐进式hash,因为渐进式hash实现复杂,特别当遇到多线程的时候,要考虑并发;java不像redis那边每次操作都需要高性能,所以没有采用渐进式hash;
set
-
set 不能有重复元素,Redis集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
-
满足下列条件时候用intset
-
结合对象保存的所有元素都是整数值
-
集合对象保存的元素数量不超过512个
-
或者用hashtable
sorted set
-
每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数(score)却可以重复。
-
满足下列条件的时候是压缩表,有序集合保存的所有元素的长度小于64字节,有序集合保存的元素数量小于128个或者调表
-
压缩表:(省内存,效率不高,非常适合低数据量)
-
域 长度/类型 域的值
-
zlbytes uint32_t 整个 ziplist 占用的内存字节数,对 ziplist 进行内存重分配,或者计算末端时使用。
-
zltail uint32_t 到达 ziplist 表尾节点的偏移量。 通过这个偏移量,可以在不遍历整个 ziplist 的前提下,弹出表尾节点。
-
zllen uint16_t ziplist 中节点的数量。 当这个值小于 UINT16_MAX (65535)时,这个值就是 ziplist 中节点的数量; 当这个值等于 UINT16_MAX 时,节点的数量需要遍历整个 ziplist 才能计算得出。
-
entryX ziplist 所保存的节点,各个节点的长度根据内容而定。
-
zlend uint8_t 255 的二进制值 1111 1111 (UINT8_MAX) ,用于标记 ziplist 的末端。
-
pre_entry_length 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。
-
encoding 保存 了 content 部分所保存的数据的类型(以及长度)
-
content 部分保存着节点的内容,类型和长度由 encoding 和 length 决定。
-
压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失
-
Quicklist实际上是ziplist和linkedlist的混合体,它将linkedlist按段进行切分,每一段使用ziplist进行紧凑存储,多个ziplist之间使用双向指针进行串接。
-
skiplist 跳跃表(skiplist)是一种有序的数据结构,通过在每个节点中维持多个指向其他节点 的指针,从而达到快速访问节点的目的。本质是利用空间换时间的思想,建立多级索引来提高查找,插入,删除操作的效率。
-
在跳表中查询一个数据的时间复杂度是O( log n ),空间复杂度是O( n )
-
Redis 跳表由zskiplist和zsklistNode组成。而zskiplist结构则用于保存跳跃表节点的相关信息,其中zsliplistNode结构用于表示跳跃表节点
-
zskiplist结构
-
header:指向跳跃表的表头节点。
-
tail:指向跳跃表的表尾节点。
-
level:记录目前跳跃表内层数最大的那个节点的层数(表头不包含在内)
-
length:记录跳跃表的长度,即当前跳跃表包含的节点的数量(不包含表头)
-
zsklistNode结构
-
level:层,L1,L2,L3表示每个层标记。每层都有两个参数,前进指针和跨度。前进指针指向下一个节点,跨度表示下一个节点和本节点的距离。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
-
backward:后退指针,用来在从尾向表头遍历时使用。
-
score:各个节点中的1.0、2.0、3.0是节点所保存的分值。在跳跃表中,各个节点按照各自所保存的分值从小到大排列。
-
注意表头节点和其他节点的构造是一样的: 表头节点也有后退指针、分值和成员对象, 不过表头节点的这些属性都不会被用到, 所以图中省略了这些部分, 只显示了表头节点的各个层。
-
上述的k层level是随机的产生的。每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为 level数组的大小,这个大小就是层的“高度”。
java Hashmap 为啥不用跳表而用红黑树
-
内存占用比红黑树大(每个节点4指针);
-
为什么redis 选择跳表而不用红黑树。
-
1、跳表操作时间复杂度和红黑树相同nlog
-
2、在做范围查找的时候,skiplist要比平衡树容易
-
3、插入skiplist只需要的修改相邻节点的指针,而平衡树插入需要维护其性质
数据结构常用场景
-
字符串(String):缓存(如用户会话、网页内容)、计数器(文章阅读量、视频播放量)、分布式锁(通过
SETNX实现)。 -
列表(List):消息队列(生产者 - 消费者模型)、最新动态(如社交平台时间线)、任务队列(异步处理任务)。
-
哈希(Hash):存储对象(如用户信息、商品详情)、批量操作(同时更新多个字段)。
-
集合(Set):标签系统(文章分类)、共同好友(社交关系)、数据去重(如注册邮箱校验)。
-
有序集合(Sorted Set):排行榜(游戏得分、热门文章)、时间序列数据(按时间排序的事件记录)。地理位置 非常有用
-
特殊结构:
- 位图(Bitmap):统计活跃用户、权限控制。
- HyperLogLog:估算独立访客数(UV)。
- 地理空间(Geo):附近位置查询、地理位置索引
Redis 分布式锁:
bolean result=redisClient.setNx(key,value,timeout)
if(!result){
return "并发锁冲突";
}
try{
执行业务逻辑
}finaly(Exception e){
redisClient.del(key);
}
上面直接删除key来解锁方式会存在一个问题,考虑下面这种情况: (1) 线程1执行业务时间过长导致自己加的锁过期 (2) 这时线程2进来加锁成功 (3) 然后线程1业务逻辑执行完毕开始执行del key命令 (4) 这时就会出现错误删除线程2加的锁 (5) 错误删除线程2的锁后,线程3又可以加锁成功,导致有两个线程执行业务代码
锁误删除问题
加入锁标识
uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
return;
}
try{
//执行业务逻辑
}finally{
//解锁
redisClient.eval(delLuaScript,keys,values)
}
//解锁的lua脚本,原子性
delLuaScript = " if redis.call('get',key) == value then
return redis.call('del',key)
else
return 0
end;"
锁的时长问题:
在执行业务代码时,由于业务执行时间长,最终可能导致在业务执行过程中,自己的锁超时,然后锁自动释放了,在这种情况下第二个线程就会加锁成功,从而导致数据不一致的情况发生;
对于上述的这种情况,原因是由于设置的过期时间太短或者业务执行时间太长导致锁过期,但是为了避免死锁问题又必须设置过期时间,那这就需要引入自动续期的功能,即在加锁成功时,*开启一个定时任务,自动刷新Redis加锁key的超时时间
uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
//执行业务逻辑
}finally{
//删除锁
redisClient.eval(delLuaScript,keys,values)
//取消定时任务
cancelScheduler(uuid);
}
//定时任务,判断Redis中的锁是否是自己的,如果存在的话就使用expire命令重新设置过期时间
luaScript = "if redis.call('get',key) == value) then
return redis.call('expire',key,timeOut);
else
return 0;
end;"
锁的可重入性:
uuid = getUUID();
//加锁
lockResut = redisClient.eval(addLockLuaScript,keys,values);
if(!lockResult){
return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
//执行业务逻辑
}finally{
//删除锁
redisClient.eval(delLuaScript,keys,values)
//取消定时任务
cancelScheduler(uuid);
}
//加锁的时候
//锁不存在
if (redis.call('exists', key) == 0) then
redis.call('hset', key, uuid, 1);
redis.call('expire', key, time);
return 1;
end;
//锁存在,判断是否是自己的锁
if (redis.call('hexists', key, uuid) == 1) then
redis.call('hincrby', key, uuid, 1);
redis.call('expire', key, uuid);
return 1;
end;
//锁不是自己的,返回加锁失败
return 0;
//解锁的时候
//判断锁是否是自己的,不是自己的直接返回错误
if (redis.call('hexists', key,uuid) == 0) then
return 0;
end;
//锁是自己的,则对加锁次数-1
local counter = redis.call('hincrby', key, uuid, -1);
if (counter > 0) then
//剩余加锁次数大于0,则不能释放锁,重新设置过期时间
redis.call('expire', key, uuid);
return 1;
else
//等于0,代表可以释放锁了
redis.call('del', key);
return 1;
end;
-
RedLock
-
缓存与数据库数据一致性问题:
-
只要异构数据源就会面临着数据一致性问题,如果业务需要强一致性就不要异构数据,以下方案都是为了保证弱一致性或最终一致性基础上的。
-
方案1 、先改数据库,在改redis。
-
并发问题,a先线程改完数据库,b线程在去改数据库。改缓存是b先改缓存,a后改。Redis和数据库不一致。保证不了最终一致性。
-
方案2、先改redis 在改数据库
-
并发问题,a线程先改缓存,b线程在改缓存。改数据库是b线改数据库,a在改数据库。Redis和数据库不一致。保证不了最终一致性。
-
方案1 和方案2 都是由于并发写导致的。所以有两个优化解决方案。
-
直接较次优化方案
-
1.写请求更新之前先获取分布式锁,获得之后才能去数据库更新这个数据,获取不到就进行等待,超时后就返回更新失败。
-
2.更新完之后去刷新缓存,更新失败重试,失败重试两次报警。
使用数据库事务,如果redis更新失败,直接回滚数据库,但是不是不能保证数据库和redis数据一致,因为redis客服端失败,并不代表服务端失败,导致数据库无数据,redis有数据)。
-
3、更新完成就释放锁
-
这种技术方案通过对写请求的实现串行化来保证数据一致性,但是会导致吞吐量变低。比较适合银行相关的业务。
-
比较好的方案
-
异步更新缓存【把redis当成一个从库】
-
1.先更新数据库
-
2.基于binlog异步更新redsi,顺序。
-
3、更新失败重试
-
方案3、先删缓存,在改数据库。
-
并发问题, a线程删除缓存但是还没更新数据库 ,b线程去缓存查数据查不到就去数据库查然后写缓存,写的还是a线程没改数据库之前老值。a线程然后再去更新数据库。Redis和数据库不一致。保证不了最终一致性。
-
方案4、先更新数据库,然后再去删除缓存。
-
第一种情况
-
a线程先更新数据库,立马去删缓存。 B线程发现缓存失效,去查数据库(如果查的主库还好,但是如果查的从库可能出现延迟还是之前的老数据),更新缓存的时候更新的还是之前的数据,Redis和数据库不一致。保证不了最终一致性。
-
第二种情况
-
B线程读取数据库还是老数据,在刷新到缓存之前。A线程更改数据并删除缓存,然后B线程在把老数据给删除,最终不一致。
-
延迟双删【先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。 】
-
既然方案4不能立刻删除缓存,那我们就延迟删除,但是延迟时间很难确定。及时延迟后,从库数据已更新,那在延迟这一段时间内还是数据不一致。这样的话就会导致每次更新都会延迟。在业务上是不可以被接受的,那就在更新之前在删除一次缓存。这样有可能延迟时间内数据缓存一直,也有不可能不一致。但是比只要延迟就不一致要强。
什么是大KEY:
-
•单个String类型的Key大小达到20KB并且OPS高
-
•单个String达到100KB
-
•集合类型的Key总大小达到1MB,
-
•集合类型的Key中元素超过5000个
大KEY带来的影响:
-
严重影响 QPS 、TP99 等指标,对大Key进行的慢操作会导致后续的命令被阻塞,从而导致一系列慢查询。
-
hgetall 、 smembers 等时间复杂度O(N)的命令使用不当,容易造成使用率过高。
-
大Key发生热点,大 String,value 大于 20K。当OPS为 10000,流量即为 200M, 达到单实例的流量配额. 导致 JIMDB 无法正常提供服务。
-
集群各分片内存使用不均。某个分片占用内存较高或OOM,发送缓存区增大等,导致该分片其他Key被逐出,同时也会造成其他分片的资源浪费。
-
集群各分片的带宽使用不均。某个分片被流控,其他分片则没有这种情况,且影响宿主机上的其它应用。
-
数据迁移失败 过大的Key(如超过1G),在迁移、缩容、扩容,主从全量同步在序列化过程中,内存上涨,数据同步失败,且存在 OOM 风险。
发现大KEY后的处理建议
按业务从不用纬度拆分
- String类型的大Key:可以尝试将对象分拆成几个Key-Value, 使用MGET或者多个GET组成的pipeline获取值,分拆单次操作的压力,对于集群来说可以将操作压力平摊到多个分片上,降低对单个分片的影响。
- 集合类型的大Key,并且需要整存整取要在设计上严格禁止这种场景的出现,如无法拆分,有效的方法是将该大Key从JIMDB去除,单独放到其他存储介质上。
- 合类型的大Key,每次只需操作部分元素:将集合类型中的元素分拆。以Hash类型为例,可以在客户端定义一个分拆Key的数量N,每次对HGET和HSET操作的field计算哈希值并取模N,确定该field落在哪个Key上。
直接删除
- 禁止使用DEL直接删除大Key,可能会造成JIMDB阻塞,建议使用SCAN的方式进行循序渐进式删除。
定期清理
- 有些大Key是累积产生的,建议合理设置过期时间并对过期数据定期清理。采用scan方式清除
Redis 为什么是单线程的
-
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了
-
1、采用单线程,避免了不必要的上下文切换和竞争条件。
-
2、避免了大量的加锁
-
3、redis开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)。而这个文件事件处理器是单线程的,所以才叫redis的单线程模型,这也决定了redis是单线程的
-
单线程的redis为什么这么快:
-
(一) 纯内存操作
-
(二) 单线程操作,避免了频繁的上下文切换
-
(三) 采用了非阻塞I/O多路复用机制
-
(四) Redis 内置了多种优化过后的数据结构实现,性能非常高。
其他数据结构:
位图(Bitmaps):基于sds字符串类型,可以对每个位进行操作。底层用的SDS,权限标记,标记用户属性(VIP/新用户/完成实名认证)。布隆过滤器
基数统计(HyperLogLogs):用于基数统计(底层也用的sds),可以估算集合中的唯一元素数量。独立访客统计
地理空间(Geospatial):基于 Sorted Set,用于存储地理位置信息。将坐标转为一维数组,用Sorted set 处理
发布/订阅(Pub/Sub):一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。
流(Streams):用于消息队列和日志存储,支持消息的持久化和时间排序。
模块(Modules):Redis 支持动态加载模块,可以扩展 Redis 的功能。
redis 性能调优实战 pdai.tech/md/db/nosql…