当前位置:主页 > 查看内容

Redis学习总结--《我的Java打怪日记》

发布时间:2021-07-18 00:00| 位朋友查看

简介:} dictEntry; hash算法 Redis使用MurmurHash2算法计算键的哈希值。 hash冲突 解决hash冲突的方法有 1 开放定址法 ?所谓的开放定址法就是一旦发生了冲突 就去寻找下一个空的散列地址 只要散列表足够大 空的散列地址总能找到. 2 再哈希法 再哈希法又叫双哈希法……
} dictEntry; hash算法

Redis使用MurmurHash2算法计算键的哈希值。

hash冲突

解决hash冲突的方法有

1 开放定址法 ?所谓的开放定址法就是一旦发生了冲突 就去寻找下一个空的散列地址 只要散列表足够大 空的散列地址总能找到.

2 再哈希法 再哈希法又叫双哈希法 有多个不同的Hash函数 当发生冲突时 使用第二个 第三个 …. 等哈希函数计算地址 直到无冲突。虽然不易发生聚集 但是增加了计算时间。

3 链地址法 链地址法的基本思想是 每个哈希表节点都有一个next指针 多个哈希表节点可以用next指针构成一个单向链表 被分配到同一个索引上的多个节点可以用这个单向?链表连接起来。

4 建立公共溢出区 将哈希表分为基本表和溢出表两部分 凡是和基本表发生冲突的元素 一律填入溢出表.


Redis使用的是链地址法解决hash冲突 即放在链表的下一个位置。


rehash过程

rehash过程为扩容或收缩过程 该过程是并不是一次性、集中式地完成 而是分多次、渐近式地完成 因为数据量可能很大 避免对服务器性能造成影响

1.若为扩容为副表ht[1]分配空间 为原表ht[0]的2倍大小 若是收缩 为大于等于原表used的个数的第一个2的指数次幂。

2.数据迁移

3.将原表置为空

渐进式过程

为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而带来的阻塞问题。


捎带脚式的rehash会不会导致整个过程非常漫长 如果某个value一直没有操作那么需要扩容时由于一直不用所以影响不大 需要缩容时如果一直不处理可能造成内存浪费 具体的还没来得及研究 先埋个问题吧

Set

集合对象的编码可以是intset或者hashtable。当满足以下条件时 使用intset结构

所有元素都是整数元素数量不超过512个Sorted Set(Zset)

有序集合的编码可以是ziplist或者skiplist。

使用ziplist存储时 每个元素使用两个相邻的节点来保存 第一个节点保存元素的成员 第二个节点保存元素的分值 score 。集合元素按分值从小到大排序。


使用skiplist时包含一个字典和一个跳跃表 跳跃表按score从小到大保存所有集合元素。字典保存着从member到score的映射。两种结构通过指针共享相同元素的member和score 不浪费额外内存。

typedef struct zset {
 dict *dict;
 zskiplist *zsl;
} zset;

ZSet中的字典和跳表布局


image.pngziplistziplist总体结构


image

从图中我们基本上可以看到几个主要部分 zlbytes、zltail、zllen、zlentry、zlend。

image.png

来看下ziplist.c中对ziplist的申请和扩容操作 加深对上面几个属性的理解

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
 unsigned int bytes ZIPLIST_HEADER_SIZE ZIPLIST_END_SIZE;
 unsigned char *zl zmalloc(bytes);
 ZIPLIST_BYTES(zl) intrev32ifbe(bytes);
 ZIPLIST_TAIL_OFFSET(zl) intrev32ifbe(ZIPLIST_HEADER_SIZE);
 ZIPLIST_LENGTH(zl) 0;
 zl[bytes-1] ZIP_END;
 return zl;
/* Resize the ziplist. */
unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {
 zl zrealloc(zl,len);
 ZIPLIST_BYTES(zl) intrev32ifbe(len);
 zl[len-1] ZIP_END;
 return zl;
跳跃表

Redis只有两处使用到了跳跃表 一个是实现有序集合键 sorted set ,另一个是在集群节点中用作内部数据结构。

image.png

Redis数据库

redis服务器的状态都保存在redisServer redisDb两个结构中 具体如下图

typedef redisServer {
 redisDb *db; // db数组 保存着服务器中的所有数据库 
 int dbnum; // 服务器的数据库数量
 //...
typedef struct redisDb {
 dict *dict; // 键空间 保存着数据库中的所有键值对
 dict *expires; // 过期字典 保存着键的过期时间
 // ...
} redisDb;

redisDB的一个例子如下图

image

内存回收机制

为了让Redis服务安全稳定的运行 让使用内存保持在一定的阈值内是非常有必要的 因此我们就需要删除该删除的 清理该清理的 把内存留给需要的键值对 试想一条大河需要设置几个警戒水位来确保不决堤不枯竭 Redis也是一样的 只不过Redis只关心决堤即可 来一张图

image

图中设定机器内存为128GB 占用64GB算是比较安全的水平 如果内存接近80%也就是100GB左右 那么认为Redis目前承载能力已经比较大了 具体的比例可以根据公司和个人的业务经验来确定。

笔者只是想表达出于安全和稳定的考虑 不要觉得128GB的内存就意味着存储128GB的数据 都是要打折的。

B.1 回收的内存从哪里来

Redis占用的内存是分为两部分 存储键值对消耗和本身运行消耗。显然后者我们无法回收 因此只能从键值对下手了 键值对可以分为几种 带过期的、不带过期的、热点数据、冷数据。对于带过期的键值是需要删除的 如果删除了所有的过期键值对之后内存仍然不足怎么办 那只能把部分数据给踢掉了。

image

B.2 如何实施过期键值对的删除

要实施对键值对的删除我们需要明白如下几点

带过期超时的键值对存储在哪里
如何判断带超时的键值对是否可以被删除了
删除机制有哪些以及如何选择
1.键值对的存储

老规矩来到github看下源码 src/server.h中给的redisDb结构体给出了答案

typedef struct redisDb {
 dict *dict; /* The keyspace for this DB */
 dict *expires; /* Timeout of keys with a timeout set */
 dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
 dict *ready_keys; /* Blocked keys that received a PUSH */
 dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
 int id; /* Database ID */
 long long avg_ttl; /* Average TTL, just for stats */
 unsigned long expires_cursor; /* Cursor of the active expire cycle. */
 list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
复制代码


Redis 通过一个叫做过期字典 可以看作是 hash 表 来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键) 过期字典的值是一个 long long 类型的整数 这个整数保存了 key 所指向的数据库键的过期时间 毫秒精度的 UNIX 时间戳 。

image

过期字典是存储在 redisDb 这个结构里的

typedef struct redisDb {
 dict *dict; //数据库键空间,保存着数据库中所有键值对
 dict *expires // 过期字典,保存着键的过期时间
} redisDb;
2. 键值对的过期删除判断

判断键是否过期可删除 需要先查过期字典是否存在该值 如果存在则进一步判断过期时间戳和当前时间戳的相对大小 做出删除判断 简单的流程如图

? ? ? ? ? ? ? ? ? ? ? ? ? ? ?image

3. 键值对的删除策略

经过前面的几个环节 我们知道了Redis的两种存储位置 键空间和过期字典 以及过期字典expires的结构、判断是否过期的方法 那么该如何实施删除呢

先抛开Redis来想一下可能的几种删除策略

定时删除 在设置键的过期时间的同时 创建定时器 让定时器在键过期时间到来时 即刻执行键值对的删除
定期删除 每隔特定的时间对数据库进行一次扫描 检测并删除其中的过期键值对
惰性删除 键值对过期暂时不进行删除 至于删除的时机与键值对的使用有关 当获取键时先查看其是否过期 过期就删除 否则就保留

在上述的三种策略中定时删除和定期删除属于不同时间粒度的主动删除 惰性删除属于被动删除。

三种策略都有各自的优缺点 定时删除对内存使用率有优势 但是对CPU不友好 惰性删除对内存不友好 如果某些键值对一直不被使用 那么会造成一定量的内存浪费 定期删除是定时删除和惰性删除的折中。

Reids采用的是惰性删除和定期删除的结合 一般来说可以借助最小堆来实现定时器 不过Redis的设计考虑到时间事件的有限种类和数量 使用了无序链表存储时间事件 这样如果在此基础上实现定时删除 就意味着O(N)遍历获取最近需要删除的数据。

但是我觉得antirez如果非要使用定时删除 那么他肯定不会使用原来的无序链表机制 所以个人认为已存在的无序链表不能作为Redis不使用定时删除的根本理由 冒昧猜测唯一可能的是antirez觉得没有必要使用定时删除。

? ? ? ? ? ? ? ? image


B.3 内存淘汰机制

为了保证Redis的安全稳定运行 设置了一个max-memory的阈值 那么当内存用量到达阈值 新写入的键值对无法写入 此时就需要内存淘汰机制 在Redis的配置中有几种淘汰策略可以选择 详细如下

noeviction: 当内存不足以容纳新写入数据时 新写入操作会报错
allkeys-lru 当内存不足以容纳新写入数据时 在键空间中移除最近最少使用的 key
allkeys-random 当内存不足以容纳新写入数据时 在键空间中随机移除某个 key
volatile-lru 当内存不足以容纳新写入数据时 在设置了过期时间的键空间中 移除最近最少使用的 key
volatile-random 当内存不足以容纳新写入数据时 在设置了过期时间的键空间中 随机移除某个 key
volatile-ttl 当内存不足以容纳新写入数据时 在设置了过期时间的键空间中 有更早过期时间的 key 优先移除

后三种策略都是针对过期字典的处理 但是在过期字典为空时会noeviction一样返回写入失败 毫无策略地随机删除也不太可取 所以一般选择第二种allkeys-lru基于LRU策略进行淘汰。

4.0 版本后增加以下两种

volatile-lfu least frequently used 从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰allkeys-lfu least frequently used 当内存不足以容纳新写入数据时 在键空间中 移除最不经常使用的 key


过期健删除策略强调的是对过期健的操作 如果有健过期而内存足够 Redis不会使用内存淘汰机制来腾退空间 这时会优先使用过期健删除策略删除过期健。

内存淘汰机制强调的是对内存数据的淘汰操作 当内存不足时 即使有的健没有到达过期时间或者根本没有设置过期也要根据一定的策略来删除一部分 腾退空间保证新数据的写入。


持久化快照RDB

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后 可以对快照进行备份 可以将快照复制到其他服务器从而创建具有相同数据的服务器副本 Redis 主从结构 主要用来提高 Redis 性能 还可以将快照留在原地以便重启服务器的时候使用。

AOF

与快照持久化相比 AOF 持久化 的实时性更好 因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF append only file 方式的持久化 可以通过 appendonly 参数开启

appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令 Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同 都是通过 dir 参数设置的 默认的文件名是 appendonly.aof。

过期删除策略

过期数据的删除策略:

惰性删除 只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好 但是可能会造成太多过期 key 没有被删除。定期删除 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且 Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

定期删除对内存更加友好 惰性删除对 CPU 更加友好。两者各有千秋 所以 Redis 采用的是 定期删除 惰性/懒汉式删除 。

但是 仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里 然后就 Out of memory 了。

怎么解决这个问题呢 答案就是 Redis 内存淘汰机制。


Redis集群单实例Redis架构

最开始的一主N从加上读写分离 Redis作为缓存单实例貌似也还不错 并且有Sentinel哨兵机制 可以实现主从故障迁移。


单实例一主两从 读写分离结构:

image

单实例的由于本质上只有一台Master作为存储 就算机器为128GB的内存 一般建议使用率也不要超过70%-80% 所以最多使用100GB数据就已经很多了 实际中50%就不错了 以为数据量太大也会降低服务的稳定性 因为数据量太大意味着持久化成本高 可能严重阻塞服务 甚至最终切主。


如果单实例只作为缓存使用 那么除了在服务故障或者阻塞时会出现缓存击穿问题 可能会有很多请求一起搞死MySQL。


如果单实例作为主存 那么问题就比较大了 因为涉及到持久化问题 无论是bgsave还是aof都会造成刷盘阻塞 此时造成服务请求成功率下降 这个并不是单实例可以解决的 因为由于作为主存储 持久化是必须的。


所以我们期待一个多主多从的Redis系统 这样无论作为主存还是作为缓存 压力和稳定性都会提升 尽管如此 笔者还是建议 Redis尽量不要做主存储

集群架构

要支持集群首先要克服的就是分片问题 也就是一致性哈希问题 常见的方案有三种

客户端分片 N主N从模式 这种情况主要是类似于哈希取模的做法 当客户端对服务端的数量完全掌握和控制时 可以简单使用。

中间层分片 这种情况是在客户端和服务器端之间增加中间层 充当管理者和调度者 客户端的请求打向中间层 由中间层实现请求的转发和回收 当然中间层最重要的作用是对多台服务器的动态管理

服务端分片 N主N从模式 不使用中间层实现去中心化的管理模式 客户端直接向服务器中任意结点请求 如果被请求的Node没有所需数据 则向客户端回复MOVED 并告诉客户端所需数据的存储位置 这个过程实际上是客户端和服务端共同配合 进行请求重定向来完成的。

客户端分片版集群

需要解决一致性哈希的问题 也就是动态扩缩容时的数据问题。 一致性hash算法

image.png

中间层分片的集群版Redis

在Redis官方发布集群版本之前 业内有一些方案迫不及待要用起自研版本的Redis集群 其中包括国内豌豆荚的Codis、国外Twiter的twemproxy。


核心思想都是在多个Redis服务器和客户端Client中间增加分片层 由分片层来完成数据的一致性哈希和分片问题 每一家的做法有一定的区别 但是要解决的核心问题都是多台Redis场景下的扩缩容、故障转移、数据完整性、数据一致性、请求处理延时等问题。

image

业内Codis配合LVS等多种做法实现Redis集群的方案有很多都应用到生成环境中 表现都还不错 主要是官方集群版本在Redis3.0才出现 对其稳定性如何 很多公司都不愿做小白鼠 不过事实上经过迭代目前已经到了Redis5.x版本 官方集群版本还是很不错的 至少笔者这么认为。

服务端分片的官方集群版本

官方版本区别于上面的Codis和Twemproxy 实现了服务器层的Sharding分片技术 换句话说官方没有中间层 而是多个服务结点本身实现了分片 当然也可以认为实现sharding的这部分功能被融合到了Redis服务本身中 并没有单独的Sharding模块。


官方集群引入slot的概念进行数据分片 之后将数据slot分配到多个Master结点 Master结点再配置N个从结点 从而组成了多实例sharding版本的官方集群架构。


Redis Cluster 是一个可以在多个 Redis 节点之间进行数据共享的分布式集群 在服务端 通过节点之间的特殊协议进行通讯 这个特殊协议就充当了中间层的管理部分的通信协议 这个协议称作Gossip流言协议。


分布式系统一致性协议的目的就是为了解决集群中多结点状态通知的问题 是管理集群的基础 如图展示了基于Gossip协议的官方集群架构图

image


同步机制

理解持久化和数据同步的关系 需要从单点故障和高可用两个角度来分析

1 单点宕机故障

假如我们现在只有一台作为缓存的Redis机器 通过持久化将热点数据写到磁盘 某时刻该Redis单点机器发生故障宕机 此期间缓存失效 主存储服务将承受所有的请求压力倍增 监控程序将宕机Redis机器拉起。

重启之后 该机器可以Load磁盘RDB数据进行快速恢复 恢复的时间取决于数据量的多少 一般秒级到分钟级不等 恢复完成保证之前的热点数据还在 这样存储系统的CacheMiss就会降低 有效降低了缓存击穿的影响。

在单点Redis中持久化机制非常有用 只写文字容易让大家睡着 我画了张图

image

作为一个高可用的缓存系统单点宕机是不允许的 因此就出现了主从架构 对主节点的数据进行多个备份 如果主节点挂点 可以立刻切换状态最好的从节点为主节点 对外提供写服务 并且其他从节点向新主节点同步数据 确保整个Redis缓存系统的高可用。

如图展示了一个一主两从读写分离的Redis系统主节点故障迁移的过程 整个过程并没有停止正常工作 大大提高了系统的高可用

image

从上面的两点分析可以得出个小结论【划重点】

持久化让单点故障不再可怕 数据同步为高可用插上翅膀。

我们理解了数据同步对Redis的重要作用 接下来继续看数据同步的实现原理和过程、重难点等细节问题吧

2 Redis系统中的CAP理论

对分布式存储有了解的读者一定知道CAP理论 说来惭愧笔者在2018年3月份换工作的时候 去Face 旷视科技面后端开发岗位时就遇到了CAP理论 除了CAP理论问题之外其他问题都在射程内 所以最终还是拿了Offer。

在理论计算机科学中 CAP定理又被称作布鲁尔定理Brewer s theorem 这个定理起源于加州大学伯克利分校的计算机科学家埃里克·布鲁尔在2000年的分布式计算原理研讨会PODC上提出的一个猜想。

在2002年麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明 使之成为一个定理。它指出对于一个分布式计算系统来说 不可能同时满足以下三点

C Consistent 一致性 连贯性
A Availability 可用性
P Partition Tolerance 分区容忍性

来看一张阮一峰大佬画的图

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? image

举个简单的例子 说明一下CP和AP的兼容性

理解CP和AP的关键在于分区容忍性P 网络分区在分布式存储中再平常不过了 即使机器在一个机房 也不可能全都在一个机架或一台交换机。

这样在局域网就会出现网络抖动 笔者做过1年多DPI对于网络传输中最深刻的三个名词 丢包、乱序、重传。所以我们看来风平浪静的网络 在服务器来说可能是风大浪急 一不小心就不通了 所以当网络出现断开时 这时就出现了网络分区问题。

对于Redis数据同步而言 假设从结点和主结点在两个机架上 某时刻发生网络断开 如果此时Redis读写分离 那么从结点的数据必然无法与主继续同步数据。在这种情况下 如果继续在从结点读取数据就造成数据不一致问题 如果强制保证数据一致从结点就无法提供服务造成不可用问题 从而看出在P的影响下C和A无法兼顾。

其他几种情况就不深入了 从上面我们可以得出结论 当Redis多台机器分布在不同的网络中 如果出现网络故障 那么数据一致性和服务可用性无法兼顾 Redis系统对此必须做出选择 事实上Redis选择了可用性 或者说Redis选择了另外一种最终一致性。

3 Redis的最终一致性和复制

Redis选择了最终一致性 也就是不保证主从数据在任何时刻都是一致的 并且Redis主从同步默认是异步的 亲爱的盆友们不要晕 不要蒙圈

1.最终一致性

最终一致性通过异步复制来实现。

我来一下解释同步复制和异步复制(注意 考虑读者的感受 我并没有写成同步同步和异步同步 )

image

一图胜千言 看红色的数字就知道同步复制和异步复制的区别了

异步复制 当客户端向主结点写了hello world 主节点写成功之后就向客户端回复OK 这样主节点和客户端的交互就完成了 之后主节点向从结点同步hello world 从结点完成之后向主节点回复OK 整个过程客户端不需要等待从结点同步完成 因此整个过程是异步实现的。
同步复制 当客户端向主结点写了hello world 主节点向从结点同步hello world 从结点完成之后向主节点回复OK 之后主节点向客户端回复OK 整个过程客户端需要等待从结点同步完成 因此整个过程是同步实现的。

Redis选择异步复制可以避免客户端的等待 更符合现实要求 不过这个复制方式可以修改 根据自己需求而定吧。

2.复制1.从从复制

假如Redis高可用系统中有一主四从 如果四个从同时向主节点进行数据同步 主节点的压力会比较大 考虑到Redis的最终一致性 因此Redis后续推出了从从复制 从而将单层复制结构演进为多层复制结构 笔者画了个图看下

image

2.全量复制和增量复制

全量复制是从结点因为故障恢复或者新添加从结点时出现的初始化阶段的数据复制 这种复制是将主节点的数据全部同步到从结点来完成的 所以成本大但又不可避免。

增量复制是主从结点正常工作之后的每个时刻进行的数据复制方式 涓涓细流同步数据 这种同步方式又轻又快 优点确实不少 不过如果没有全量复制打下基础增量复制也没戏 所以二者不是矛盾存在而是相互依存的。

image

全量复制过程分析

Redis的全量复制过程主要分三个阶段

快照阶段 从结点向主结点发起SYNC全量复制命令 主节点执行bgsave将内存中全部数据生成快照并发送给从结点 从结点释放旧内存载入并解析新快照 主节点同时将此阶段所产生的新的写命令存储到缓冲区。
缓冲阶段 主节点向从节点同步存储在缓冲区的操作命令 这部分命令主节点是bgsave之后到从结点载入快照这个时间段内的新增命令 需要记录要不然就出现数据丢失。
增量阶段 缓冲区同步完成之后 主节点正常向从结点同步增量操作命令 至此主从保持基本一致的步调。

借鉴参考1的一张图表 写的很好

image

考虑一个多从并发全量复制问题

如果此时有多个从结点同时向主结点发起全量同步请求会怎样

Redis主结点是个聪明又诚实的家伙 比如现在有3个从结点A/B/C陆续向主节点发起SYNC全量同步请求。

主节点在对A进行bgsave的同时 B和C的SYNC命令到来了 那么主节点就一锅烩 把针对A的快照数据和缓冲区数据同时同步给ABC 这样提高了效率又保证了正确性。
主节点对A的快照已经完成并且现在正在进行缓冲区同步 那么只能等A完成之后 再对B和C进行和A一样的操作过程 来实现新节点的全量同步 所以主节点并没有偷懒而是重复了这个过程 虽然繁琐但是保证了正确性。

再考虑一个快照复制循环问题

主节点执行bgsave是比较耗时且耗内存的操作 期间从结点也经历装载旧数据- 释放内存- 装载新数据的过程 内存先升后降再升的动态过程 从而知道无论主节点执行快照还是从结点装载数据都是需要时间和资源的。

抛开对性能的影响 试想如果主节点快照时间是1分钟 在期间有1w条新命令到来 这些新命令都将写到缓冲区 如果缓冲区比较小只有8k 那么在快照完成之后 主节点缓冲区也只有8k命令丢失了2k命令 那么此时从结点进行全量同步就缺失了数据 是一次错误的全量同步。

无奈之下 从结点会再次发起SYNC命令 从而陷入循环 因此缓冲区大小的设置很重要 二话不说再来一张图

image

增量复制过程分析

增量复制过程稍微简单一些 但是非常有用 试想复杂的网络环境下 并不是每次断开都无法恢复 如果每次断开恢复后就要进行全量复制 那岂不是要把主节点搞死 所以增量复制算是对复杂网络环境下数据复制过程的一个优化 允许一段时间的落后 最终追上就行。

增量复制是个典型的生产者-消费者模型 使用定长环形数组(队列)来实现 如果buffer满了那么新数据将覆盖老数据 因此从结点在复制数据的同时向主节点反馈自己的偏移量 从而确保数据不缺失。

这个过程非常好理解 kakfa这种MQ也是这样的 所以在合理设置buffer大小的前提下 理论上从的消费能力是大于主的生产能力的 大部分只有在网络断开时间过长时会出现buffer被覆盖 从结点消费滞后的情况 此时只能进行全量复制了。

image

3.无盘复制

理解无盘复制之前先看下什么是有盘复制呢 所谓盘是指磁盘 可能是机械磁盘或者SSD 但是无论哪一种相比内存都更慢 我们都知道IO操作在服务端的耗时是占大头的 因此对于全量复制这种高IO耗时的操作来说 尤其当服务并发比较大且还在进行其他操作时对Redis服务本身的影响是比较大大 之前的模式时这样的

image

在Redis2.8.18版本之后 开发了无盘复制 也就是避免了生成的RDB文件落盘再加载再网络传输的过程 而是流式的遍历发送过程 主节点一边遍历内存数据 一边将数据序列化发送给从结点 从结点没有变化 仍然将数据依次存储到本地磁盘 完成传输之后进行内存加载 可见无盘复制是对IO更友好。


Redis分布式锁与RedLock算法1 基于Redis的分布式锁简介

最初分布式锁借助于setnx和expire命令 但是这两个命令不是原子操作 如果执行setnx之后获取锁但是此时客户端挂掉 这样无法执行expire设置过期时间就导致锁一直无法被释放 因此在2.8版本中Antirez为setnx增加了参数扩展 使得setnx和expire具备原子操作性。

image

在单Matster-Slave的Redis系统中 正常情况下Client向Master获取锁之后同步给Slave 如果Client获取锁成功之后Master节点挂掉 并且未将该锁同步到Slave 之后在Sentinel的帮助下Slave升级为Master但是并没有之前未同步的锁的信息 此时如果有新的Client要在新Master获取锁 那么将可能出现两个Client持有同一把锁的问题 来看个图来想下这个过程

? image.png

为了保证自己的锁只能自己释放需要增加唯一性的校验 综上基于单Redis节点的获取锁和释放锁的简单过程如下:

// 获取锁 unique_value作为唯一性的校验
SET resource_name unique_value NX PX 30000
// 释放锁 比较unique_value是否相等 避免误释放
if redis.call( get ,KEYS[1]) ARGV[1] then
 return redis.call( del ,KEYS[1])
 return 0
复制代码

这就是基于单Redis的分布式锁的几个要点。

2 Redlock算法基本过程

Redlock算法是Antirez在单Redis节点基础上引入的高可用模式。在Redis的分布式环境中 我们假设有N个完全互相独立的Redis节点 在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。

现在假设有5个Redis主节点(大于3的奇数个) 这样基本保证他们不会同时都宕掉 获取锁和释放锁的过程中 客户端会执行以下操作:

获取当前Unix时间 以毫秒为单位
依次尝试从5个实例 使用相同的key和具有唯一性的value获取锁
当向Redis请求获取锁时 客户端应该设置一个网络连接和响应超时时间 这个超时时间应该小于锁的失效时间 这样可以避免客户端死等
客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁 并且使用的时间小于锁失效时间时 锁才算获取成功
如果取到了锁 key的真正有效时间等于有效时间减去获取锁所使用的时间 这个很重要
如果因为某些原因 获取锁失败 没有在半数以上实例取到锁或者取锁时间已经超过了有效时间 客户端应该在所有的Redis实例上进行解锁 无论Redis实例是否加锁成功 因为可能服务端响应消息丢失了但是实际成功了 毕竟多释放一次也不会有问题

上述的5个步骤是Redlock算法的重要过程 也是面试的热点 有心的读者还是记录一下吧


管道技术

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤

客户端向服务端发送一个查询请求 并监听Socket返回 通常是以阻塞模式 等待服务端响应。服务端处理命令 并将结果返回给客户端。


Redis 管道技术

Redis 管道技术可以在服务端未响应时 客户端可以继续向服务端发送请求 并最终一次性读取所有服务端的响应。

$(echo -en PING\r\n SET runoobkey redis\r\nGET runoobkey\r\nINCR visitor\r\nINCR visitor\r\nINCR visitor\r\n ; sleep 10) | nc localhost 6379
 PONG
redis
:3

以上实例中我们通过使用 PING 命令查看redis服务是否可用 之后我们设置了 runoobkey 的值为 redis 然后我们获取 runoobkey 的值并使得 visitor 自增 3 次。

在返回的结果中我们可以看到这些命令一次性向 redis 服务提交 并最终一次性读取所有服务端的响应


Redis事务

Redis 可以通过 MULTI EXEC DISCARD 和 WATCH 等命令来实现事务(transaction)功能。

 MULTI
 SET USER Guide哥 
QUEUED
 GET USER
QUEUED
 EXEC
1) OK
2) Guide哥 

使用 MULTI命令后可以输入多个命令。Redis 不会立即执行这些命令 而是将它们放到队列 当调用了EXEC命令将执行所有命令。

这个过程是这样的

开始事务 MULTI 。命令入队(批量操作 Redis 的命令 先进先出 FIFO 的顺序执行)。执行事务(EXEC)。

你也可以通过 DISCARD 命令取消一个事务 它会清空事务队列中保存的所有命令。

 MULTI
 SET USER Guide哥 
QUEUED
 GET USER
QUEUED
 DISCARD
OKCopy to clipboardErrorCopied

WATCH 命令用于监听指定的键 当调用 EXEC 命令执行事务时 如果一个被 WATCH 命令监视的键被修改的话 整个事务都不会执行 直接返回失败。

 WATCH USER
 MULTI
 SET USER Guide哥 
 GET USER
Guide哥
 EXEC
ERR EXEC without MULTICopy to clipboardErrorCopied

Redis 官网相关介绍 https://redis.io/topics/transactions 如下

image

但是 Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性 1. 原子性 2. 隔离性 3. 持久性 4. 一致性。

原子性 Atomicity 事务是最小的执行单位 不允许分割。事务的原子性确保动作要么全部完成 要么完全不起作用 隔离性 Isolation 并发访问数据库时 一个用户的事务不被其他事务所干扰 各并发事务之间数据库是独立的 持久性 Durability 一个事务被提交之后。它对数据库中数据的改变是持久的 即使数据库发生故障也不应该对其有任何影响。一致性 Consistency 执行事务前后 数据保持一致 多个事务对同一个数据读取的结果是相同的

Redis 是不支持 roll back 的 因而不满足原子性的 而且不满足持久性 。

Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚 这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。

image

你可以将 Redis 中的事务就理解为 Redis 事务提供了一种将多个命令请求打包的功能。然后 再按顺序执行打包的所有命令 并且不会被中途打断。



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


缓存击穿


? ? ?描述


? ? ?缓存在某个时间点过期的时候 恰好在这个时间点对这个Key有大量的并发请求过来 这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存 这个时候大并发的请求可能会瞬间把后端DB压垮。缓存被“击穿”的问题 这个和缓存雪崩的区别在于这里针对某一key缓存 缓存雪崩则是很多key。


? ? ?解决方案


1.设置热点数据永远不过期

2.加互斥锁 (热点数据缓存中没有的话使用setnx方法设置,设置成功则取loaddb并设置缓存 否则重试get方法)。


互斥锁参考代码如下

//2.6.1前单机版本锁
String get(String key) { 
 String value redis.get(key); 
 if (value null) { 
 if (redis.setnx(key_mutex, 1 )) { 
 // 3 min timeout to avoid mutex holder crash 
 redis.expire(key_mutex, 3 * 60) 
 value db.get(key); 
 redis.set(key, value); 
 redis.delete(key_mutex); 
 } else { 
 //其他线程休息50毫秒后重试 
 Thread.sleep(50); 
 get(key); 

? ?

缓存穿透

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中 导致请求直接到了数据库上 根本没有经过缓存这一层。举个例子 某个黑客故意制造我们缓存中不存在的 key 发起大量请求 导致大量请求落到数据库。

解决方法

1.缓存无效的key,下次请求直接返回

2.布隆过滤器 布隆过滤器是一个非常神奇的数据结构 通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法 有没有感觉布隆过滤器就是我们想要找的那个“人”。

加入布隆过滤器之后的缓存处理流程图如下。

image

但是 需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是 布隆过滤器说某个元素存在 小概率会误判。布隆过滤器说某个元素不在 那么这个元素一定不在。

为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说

我们先来看一下 当一个元素加入布隆过滤器中的时候 会进行哪些操作

使用布隆过滤器中的哈希函数对元素值进行计算 得到哈希值 有几个哈希函数得到几个哈希值 。根据得到的哈希值 在位数组中把对应下标的值置为 1。

我们再来看一下 当我们需要判断一个元素是否存在于布隆过滤器的时候 会进行哪些操作

对给定元素再次进行相同的哈希计算 得到值之后判断位数组中的每个元素是否都为 1 如果值都为 1 那么说明这个值在布隆过滤器中 如果存在一个值不为 1 说明该元素不在布隆过滤器中。

然后 一定会出现这样一种情况 不同的字符串可能哈希出来的位置相同。 可以适当增加位数组大小或者调整我们的哈希函数来降低概率


缓存雪崩


缓存在同一时间大面积的失效 后面的请求都直接落到了数据库上 造成数据库短时间内承受大量请求。?这就好比雪崩一样 摧枯拉朽之势 数据库的压力可想而知 可能直接就被这么多请求弄宕机了。

针对 Redis 服务不可用的情况

采用 Redis 集群 避免单机出现问题整个缓存服务都没办法使用。限流 避免同时处理大量的请求。

针对热点缓存失效的情况

设置不同的失效时间比如随机设置缓存的失效时间。缓存永不失效。


如何保证缓存和数据库数据的一致性

细说的话可以扯很多 但是我觉得其实没太大必要。我个人觉得引入缓存之后 如果为了短时间的不一致性问题 选择让系统设计变得更加复杂的话 完全没必要。

下面单独对 Cache Aside Pattern 旁路缓存模式 来聊聊。

Cache Aside Pattern 中遇到写请求是这样的

写操作 更新 DB 然后直接删除 cache 。

读操作 先从缓存中读 没读到从数据库中读 再更新到缓存。

如果更新数据库成功 而删除缓存这一步失败的情况的话 简单说两个解决方案

缓存失效时间变短 不推荐 治标不治本 我们让缓存数据的过期时间变短 这样的话缓存就会从数据库中加载数据。另外 这种解决办法对于先操作缓存后操作数据库的场景不适用。增加 cache 更新重试机制 常用 如果 cache 服务当前不可用导致缓存删除失败的话 我们就隔一段时间进行重试 重试次数可以自己定。如果多次重试还是失败的话 我们可以把当前更新失败的 key 存入队列中 等缓存服务可用之后 再将 缓存中对应的 key 删除即可。



本文转自网络,原文链接:https://developer.aliyun.com/article/785410
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文

  • 周排行
  • 月排行
  • 总排行

随机推荐