Redis
2024-01-29 14:16:46 0 举报
AI智能生成
极客时间Redis 系统思维导图
作者其他创作
大纲/内容
《Redis 设计 与 实现》
Redis 底层数据结构
SDS 动态字符串
字节数组 二进制安全
空间预分配,惰性释放 (free记录空闲空间) ,来减少内存重分配
<1MB free = len;>1MB free = 1MB
双向链表
压缩列表
可以存储字符串或整数
无序,只能通过遍历查找
连锁更新
压缩列表节点有一个 previous_entry_length 来记录前一个结点长度,长度 < 254 用一个字节保存,否则用 5 个 字节,如果将一个大于等于结点插入到列表中,可能引起后续,previous_entry_length 全部改变
哈希表
跳表 O(logn)
为什么不用平衡树 (红黑树)
跳表更容易实现,不易出错
区间查询跳表效率更高
更加灵活,可以通过改变节点抽取的间隔,平衡时间和空间复杂度
Redis 中跳表节点高度是
1 - 32 的随机数,也就是说 redis 中的跳跃表层数最大为 32
整数数组
只能存储整数
从小到大有序,不重复,支持二分
升级
引发升级
添加的元素所占字节数比所有元素大
升级好处
我们不用担心内心添加元素的类型与数组类型不一致
为什么压缩列表不适合存储大型字符串
因为如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗
整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢
内存利用率高
数组对 CPU 高速缓存支持更友好
程序局部性原理
查找的时间复杂度
整数数组、双向链表
它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N)
压缩列表
头尾 O(1) 其余 O(n)
跳表
增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位 O(logn)
哈希表
O(1)
键和值用什么结构组织
全局哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶,哈希桶中的 entry 元素中保存了 *key 和 *value 指针, (*next),分别指向了实际的 键 和 值
过期键删除策略
注意 过期 和 淘汰不是一个意思
定时删除
对内存友好,但是会占用CPU,影响吞吐量
惰性删除
下次访问到键,才会删除,对CPU友好,但是导致内存泄漏
定期删除
隔一段时间进行一次删除,通过限制删除操作 执行时间 和 频率 减少操作使用 CPU时间,也一定层度上避免了内存浪费
Redis 每 100 毫秒采集默认 20 个 key,删除过期 key
如果超过 25% 的 key 过期了会持续删,导致阻塞,可以在过期函数上加一个随机数
Redis 采用惰性删除和定期删除
AOF 与 RDB 对过期键的处理
RDB
生成RDB不会保存过期键
载入RDB,主库不会加载过期键,从库会,但是主从同步后,数据又一致了
AOF
AOF写入时候,过期键会被写入,当过期键被删除,会AOF文件末尾显示追加删除操作
AOF重写,不会写入过期键
事件
文件事件
文件事件处理器是基于单 Reactor 单线程模式
套接字
I/O多路复用程序
文件事件分派器
事件处理器
时间事件
定时事件
周期事件
serverCron 函数负责管理服务器的资源
渐进式 rehash
为什么哈希表操作变慢了
hash 冲突
rehash 不是一次完成的,有一个 rehashidx 属性,在 rehash 期间,每次增删改查除了执行指定的操作之外,还会顺带将原 hash 表 rehashidx 索引上的键值对 rehash 到新 hash表,完成后 rehashidx+1
服务端处理命令请求过程
客户端将命令转成协议通过连接到服务端的套接字发送给服务器
服务端读取套接字中命令请求,将其保存到客户端输入缓冲区,并提取出命令参数和参数个数保存在客户端状态中,清空输入缓存
查询命令表,并将查询到的命令保存到客户端状态中
执行预备操作:如检查命令是否合法,客户端是否有权限,是否要先进行内存回收
调用命令的实现函数,并将命令回复保存到客户端状态的输出缓冲区,将客户端套接字的可写事件和命令回复处理器关联在一起,当客户端准备好接收消息时,触发可写事件,命令回复器将信息全部写入客户端套接字,解除命令回复处理器与套接字事件关联,清空客户端输出缓冲区
主从复制将命令发给从服务器;开启了AOF 持久化,将命令写入 AOF 文件的内存缓冲区
客户端接收到回复并将回复转换格式后打印
基础篇
一个键值数据库包含什么
一个键值数据库包括了 访问框架、索引模块、操作模块 和 存储模块四部分
Redis 为什么快
内存数据库
底层数据结构
使用的单线程
Redis 的网络 IO 和 键值对读写是由一个线程来完成的,多线程存在共享资源的并发访问控制问题,使用单线程可以避免,同时实现简单
多路复用机制
Redis 6.0 之后为何引入了多线程
硬件性能提升,瓶颈在 IO
宕机了,Redis 如何避免数据丢失
写后日志 AOF
写后日志好处
可以避免出现记录错误命令的情况
不会阻塞当前的写操作
写后日志存在的两个潜在的风险
写 AOF 日志也是在主线程中执行的,可能阻塞后续操作
如果 Redis 当数据库,写完内存没写AOF宕机了,导致恢复的时候数据丢失
三种写回策略
Always
同步写回:每个写命令执行完,立马同步地将日志写回磁盘
Everysec
每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
No
操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
AOF 重写
为什么需要重写
文件系统本身对文件大小有限制
如果文件太大,之后再往里面追加命令记录的话,效率也会变低
如果发生宕机,用AOF恢复过程慢
重写机制:一个拷贝,两处日志
主线程 fork 出一个子进程,由子进程执行 AOF 重写,采用了 COW ,在此期间,如果 Redis 执行了写命令,会将其写入 AOF 缓冲区并定期写入同步到 AOF 文件,也会写入到 AOF 重写缓冲区,当 AOF 重写完成,写入新的 AOF 文件
内存快照: 宕机后,Redis 如何实现快速恢复
Redis 生成 RDB 的两种方式
save:在主线程中执行,会导致阻塞
bgsave(默认):创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞。使用写时复制,在执行快照的同时,正常处理写操作
AOF RDB 如何选择
数据不能丢失时,内存快照和 AOF 的混合使用
RDB 只是单纯用来快速恢复的, 数据不丢失还是得依赖 AOF 的 always
如果允许分钟级别的数据丢失,丢失数据不敏感,可以只使用 RDB
如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡
写时复制
子进程复制了主进程的页表,所以通过页表映射,能读到主线程的原始数据,而当有新数据写入或数据修改时,主线程会把新数据或修改后的数据写到一个新的物理内存地址上,并修改主线程自己的页表映射
Redis 如何高可靠性
一是数据尽量少丢失
AOF
二是服务尽量少中断
增加副本冗余量,即使有一个实例出现了故障,其他实例也可以对外提供服务,并使用主从库模式,以保证数据副本的一致
从库比较多可以采用主 — 从 — 从模式
从库比较多,都和主库复制,fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢
主从同步原理
replicaof ip port 建立主从关系
全量复制 3个阶段
第一阶段
psync runID 为 ?,offset -1
FULLRESYNC 主库 runID 和 目前的复制进度 offset,返回给从库
第二阶段
bgsave 命令,生成 RDB 文件发给从库
replication buffer
第三阶段
发送 replication buffer
长连接复制
主从之间建立长网络连接,进行基于长连接的命令传播
Redis 2.8 网络断连 增量复制
psync
repl_backlog_buffer 复制挤压缓冲区
主库挂了,如何不间断服务
哨兵机制的基本流程
监控
监控任务中,哨兵需要判断主库是否处于下线状态
主观下线
哨兵进程会周期性使用 PING 命令检测它自己和主、从库的网络连接情况,如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为 "主观下线" 给其他实例发送 is-master-down-by-addr,同时为了减少误判,通常采用多实例组成哨兵集群
客观下线
当有 quorum 个哨兵实例做出 Y 响应,就可以标记主库为 "客观下线"
quorum 一般配置为 n/2+1
选举领头 Sentinel
判断客观下线后,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票,并且他会投自己一票,后续接收到别人的投票请求会投反对票
拿到半数以上的赞成票并且拿到的票数大于等于哨兵配置文件中的 quorum,成为 Leader 由它来执行主从切换
本轮没有选出 Leader,哨兵集群会等待一段时间 (也就是哨兵故障转移超时时间的 2 倍),再重新选举
选出新主库
检查从库的当前在线状态,排除不在线,并判断在线从库的网络连接状态 (使用配置项 down-after-milliseconds(主从库断连的最大连接超时时间) * 10 (超过十次) 就排除它)
依据按照从库优先级 、从库复制进度以及从库 ID 号选出新主库
通知
让从库复制新主库;客户端把请求发送到新主库
哨兵检测主库多久没有响应就提升从库为新的主库,这个时间是可以配置的(down-after-milliseconds参数)
哨兵挂了,主从库还能切换吗
组成哨兵集群就可以,即使有哨兵实例出现故障挂掉了, 其他哨兵还能继续协作完成监控、选主、通知工作
如何配置集群
sentinel monitor <master-name> <ip> <redis-port> <quorum> 哨兵与主库建立连接
哨兵集群的组成和运行机制
主库提供发布 / 订阅机制,哨兵通过在主库上发布自己连接信息 , 并订阅消息, 获取其他哨兵发布的连接信息,互相建立网络连接,进行通信 __sentinel__:hello 频道
哨兵如何建立与从库的连接
哨兵向主库发送 INFO 命令,会把从库列表返回给哨兵,哨兵就可以与每个从库建立连接
哨兵和客户端之间如何进行信息同步
通过 pub/sub 机制
切片集群:数据增多了,是该加内存还是加实例
纵向扩展
INFO 命令 , 里面有一个 latest_fork_usec 表示最近一次 fork 的耗时发现很长
RDB 进行持久化时 Redis 会 fork 子进程来完成,fork 在执行时会阻塞主线程,数据量越大,阻塞时间越长,导致 Redis 响应变慢
当数据量大生成RDB成本高,fork 阻塞主线程,成本逐渐增高
横向扩展
搭建多个实例 Redis 切片集群
数据切片后,在多个实例之间如何分布
Redis Cluster集群方案,采用哈希槽来处理数据和实例之间的映射关系,有16384个槽,key 按照 CRC16 算法计算一个 16 bit 的值;然后再用这个 16bit 值对16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。N 个实例,每个实例上的槽个数为 16384/N 个
创建集群的两种方式
cluster create 创建集群
Redis 会自动把 Hash Slot 平均分布在集群实例
cluster meet 命令手动建立实例间的连接,cluster addslots 指定实例的哈希槽数量
必须全分完集群才能工作
实例和哈希槽的对应关系发生变化
在集群中,实例有新增或删除,Redis 需要重新分配哈希槽
为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍
这些变化对客户端是无感的,Redis Cluster 提供了重定向功能,当客户端发请求给一个实例,实例上没有这个键值对应的哈希槽,实力会给客户端返回 MOVED 命令响应。客户端就会再次向相应实例发送请求,同时更新本地缓存
槽的数据未迁移完毕,但请求的 key 已经被迁移过去
客户端会收到ASK,返回键值所在实例,客户端向对应实例发送ASKING,让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送命令。 ASK 并不会更新客户端的本地缓存。下次还是会发请求给原来的实例,直到迁移完成,请求返回 MOVED
数据结构篇
String 记录小数据时,元数据占用空间大
String 为什么占内存
key 和 value 都是 RedisObject,RedisObject 由元数据信息和 ptr 构成,各占8B,共16B
另外全局 hash 表 key value next 指针占 24B,由于 jemalloc 会分配 32B
String 如何保存数据
有三种编码 int 编码、embstr 编码、raw编码
int 编码
保存 64 位有符号整数,会将它保存为8字节的Long类型整数,指针就直接赋值为整数数据
embstr 编码
保存字符串数据,并且字符串小于等于 44 字节时 RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域
raw 编码
> 44 字节时,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构
用什么数据结构可以节省内存
压缩列表(ziplist),这是一种非常节省内存的结构
如何用集合类型保存单值的键值
基于 Hash 类型的二级编码
Hash 类型的键,Hash 类型值中的 key 和 value
把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为 Hash 集合的 value
Hash 类型 底层使用ziplist key相同下,新增加一个value,只会新增加一个entry
hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。超过转哈希表
hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。超过转哈希表
有一亿个 keys 要统计,应该用哪种集合
Redis 常用的 4 个集合类型
List、Hash、Set、Sorted Set
集合类型的特点就是一个键对应一系列的数据
集合类型常见的四种统计模式
聚合统计 (并交差)
App 每天的新增用户数
Set 类型合适
排序统计
分页显示最新的评论列表
Sorted Set
按评论时间设置权重,越新的评论权重越大
List
插入数据可能导致数据重复展示
二值状态统计
统计一个月签到
Bitmap
Bitmap 用 String类型作为底层数据结构实现的一种统计二值状态的数据类型,String 类型底层是一个字节数组
基数统计
网页 UV (去重)
需要准确还是 Set 或 Hash 但当UV很大,页面很多时耗费内存
HyperLogLog节省内存,但不能精确统计
概率算法,标准误算率是 0.81%
GEO
基于位置信息服务(LBS)
key(例如车 ID)对应一个 value(一组经纬度)
使用Hash 类型可以保存信息,但是不是有序的,不能进行范围查询
Sorted Set 权重是一个浮点数 经纬度包含的是经度和纬度两个值,是没法直接保存为一个浮点数
GEO 类型的底层数据结构就是用 Sorted Set 来实现的
GEO 类型是把经纬度所在的区间编码作为 Sorted Set 中元素的权重分数,把和经纬度相关的车辆 ID 作为 Sorted Set 中元素本身的值保存下来
GeoHash 的编码原理("二分区间,区间编码"),(偶经 奇维 从左往右)
有的编码值虽然在大小上接近,但实际对应的方格却距离比较远
解决:同时查询给定经纬度所在的方格周围的 4 个或 8 个方格
如何在 Redis 中保存时间序列数据
时间序列数据的读写特点
要求写入快
查询模式多
单点查询、范围查询和聚合计算
数据类型的选择
String 不适合,因为记录小数据时,元数据占用空间大
Hash 和 Sorted Set 组合起来
Hash 不支持范围查询,SortSet 不能直接进行聚合计算(求平均值等)
RedisTimeSeries
底层数据结构使用了链表,它的范围查询的复杂度是 O(N)
查询只能返回最新的数据,不能像 Hash 返回任意时间点数据
时序数据库
消息队列的考验:Redis 有哪些解决方案
消息队列的消息存取需求
消息保序
重复消息处理
消息可靠性保证
List
本身有序
通过全局唯一ID 实现幂等性
消费者收到一次的处理结果和收到多次的处理结果是一致的
从一个 List 中读取消息, 同时 Redis 会把这个消息再插入到另一个 List
Streams
相比 List 支持消费组形式的消息读取,能更快处理消息,防止消息堆积
布隆过滤器
布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在
标记某个数据存在
首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值
然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置
最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作
查询
使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值,与数组长度取模,得到这个数据在 bit 数组中对应的 N 个位置
只要这 N 个 bit 值有一个不为 1,就代表带数据不存在
如何降低误判概率
增加 hash 函数的个数
缓存篇
旁路缓存:Redis 是如何工作的
什么是旁路缓存
读取缓存、读取数据库和更新缓存的操作都需要在应用程序中来完成
为什么要用缓存
高性能
使用缓存只需要第一次需要查询数据库,可以减少读磁盘的随机 IO,当然要保证缓存一致性
高并发
缓存的特征、为什么 Redis 适合做缓存
缓存的特征
快
容量小
Redis 天然就具有高性能访问 和 数据淘汰机制,正好符合缓存的这两个特征的要求
Redis缓存 处理请求的两种情况
缓存命中
缓存缺失,进行缓存更新 (读数据库,写入Redis)
Redis 缓存时,我们基本有三个操作
先读取 Redis
发生缓存缺失时,需要从数据库读取数据
发生缓存缺失时,还需要更新缓存
缓存的类型
只读缓存
写请求直接发给数据库 ,删改数据要删除缓存 ,下次会发生缓存缺失,从数据库中读
只读缓存直接在数据库中更新数据的好处是,所有最新的数据都在数据库中,而数据库是提供数据可靠性保障的,这些数据不会有丢失的风险
适用于写请求很少,或者是只需要提升读请求的响应速度的话,我们选择只读缓存
读写缓存
读写请求都会发给 Redis
有同步直写,异步写回两种策略
同步直写: Redis MySQL 两者都写完了才返回,增加了缓存的响应延迟
两操作的原子性,失败重试
异步写回: 等待数据将要被淘汰的时候才会写回,有丢失风险
数据一致性的要求不高
对写请求进行加速,我们选择读写缓存,异步写回策略
缓存和数据库的数据一致性
什么是数据的一致性
缓存中有数据,那么缓存的数据值需要和数据库中的值相同;缓存中本身没有数据,那么数据库中的值必须是最新值
什么情况下会出现缓存和数据库的数据不一致 (只读缓存)
删改数据
如果不能保证删除缓存和更新数据库操作的原子性就会出现
删除缓存值和更新数据库的过程中有大量并发请求
解决缓存不一致的方法 (只读缓存)
先更新数据库 + 再删除缓存
异步重试:避免重试占用线程资源,把重试请求写到消息队列中
不重试防止失败的话可以设置过期时间,过期后重新读 (短暂不一致)
读写分离 + 主从复制延迟,先更新数据库,再删除缓存也不一致 (通过延时消息),核心还是应该减少主备延迟
线程A更新主库,线程A删除缓存,线程B查询缓存缺失,从从库读取到旧值,更新缓存,主从同步完成(缓存是旧值,主从是新值)
订阅数据库变更日志,再操作缓存
替换策略:缓存满了怎么办
为什么要缓存替换策略
为了保证较高的性价比 , 缓存的空间容量必然要小于后端数据库的数据总量,缓存被写满,就需要缓存替换策略
设置多大的缓存容量合适
需要结合应用数据实际访问特征(长尾 重尾 效应) 和 成本来综合考虑
CONFIG SET maxmemory 4GB
缓存替换机制
allkeys-lru 使用 LRU 算法在所有数据中进行筛选
allkeys-lfu 使用 LFU 算法在所有数据中进行筛选
allkeys-random 从所有键值对中随机选择并删除数据
volatile-lru 使用 LRU 算法筛选设置了过期时间的键值对
volatile-lfu 使用 LFU 算法选择设置了过期时间的键值对
volatile-random 设置了过期时间的键值对中,进行随机删除
volatile-ttl 设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除
no - eviction 不删除策略
3.0 及之后默认,Redis 使用的内存空间超过 maxmemory,不淘汰直接报错,不用于 Redis 缓存
redis 3.0 之前,默认是 volatile-lru
Redis LRU 的实现
LRU 优化
并未用链表管理缓存数据
RedisObject 中的 lru 字段记录,会记录每个数据的最近一次访问的时间戳,在决定淘汰的数据时,第一次会随机选出 N 个数据,把 lru 字段值最小的数据从缓存中淘汰出去,之后淘汰数据时,只有比该集合中最小 lru 还小才会进入集合,达到 N个 后淘汰 lru 最小的数据
CONFIG SET maxmemory-samples 100 设置候选集的个数
LRU 策略存在的问题
LRU 只看数据的访问时间,在处理扫描式单次查询操作时,无法解决缓存污染
如何选择淘汰策略
MySQL 里有 2000 w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据
保证 Redis 只存 20w 的数据
CONFIG SET maxmemory xxx
保留热点数据
allkeys-lru 淘汰策略
业务数据中有明显的冷热数据区分
allkeys - lru 策略
业务中有置顶的需求
volatile - lru 策略,置顶的数据不设置过期时间
没有明显的冷热数据区分
allkeys-random
缓存雪崩、缓存击穿、缓存穿透
缓存雪崩
大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增
什么情况下会发生雪崩
某一个时刻,大量数据同时过期
EXPIRE 时加一个较小的随机数
服务降级 来应对缓存雪崩 非核心业务 直接返回预定义信息核心业务仍允许先查询缓存再查数据库
Redis 实例宕机
搭建高可用集群
服务熔断,直到其恢复
请求限流
缓存击穿
热点数据过期失效,对热数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库
访问特别频繁的热点数据不设置过期时间
缓存穿透
缓存穿透 是 指要访问的数据既不在 Redis 缓存中,也不在数据库中
业务层误操作,数据被删除
恶意攻击
应对
针对查询的数据,缓存并返回一个默认值
布隆过滤器快速判断数据是否存在
前端进行请求检测,过略掉恶意的请求
缓存污染
什么是缓存污染
数据被访问后不会再被访问却一直留存在缓存中,占用缓存空间
如何避免缓存污染
在明确知道数据被访问的时长时使用 volatile-ttl,并设置过期时间,可以有效避免缓存污染
4.0 引入的 LFU
LFU 是怎么淘汰数据的
LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存
LFU 实现原理
LFU 是 在 LRU 上的改进,把 lru 字段 24bit 前 16bit 表示数据时间戳,后 8bit 表示访问次数
但是只能记录 8bit 访问 255 次,所以并未采取访问 1 加 1 ,采用了非线性递增的计数器方法,避免短时间大量访问,后不访问使用衰减因子
但是只能记录 8bit 访问 255 次,所以并未采取访问 1 加 1 ,采用了非线性递增的计数器方法,避免短时间大量访问,后不访问使用衰减因子
LRU 与 LFU 对比
LRU 策略更加关注数据的时效性;而 LFU 策略更加关注数据的访问频次
无锁的原子操作:Redis 如何应对并发访问
并发访问中需要对什么进行控制
对多个客户端访问操作同一份数据的过程进行控制,保证操作具有互斥性
Redis 提供了什么方法保证并发访问的正确性
加锁 和 原子操作
什么是分布式锁
锁是保存在一个共享存储系统中的,可以被多个客户端共享访问和获取
分布式锁有什么要求 (两个)
保证共享存储系统的可靠性
加锁和释放锁的过程的原子性
Redis 为什么可以 Redis 实现分布式锁
Redis 本身就是共享存储系统 (基于多个节点,高可靠),支持原子操作,Redis 的读写性能高可以应对高并发的锁操作场景
Redis 如何实现分布式锁
基于单个 Redis 节点实现分布式锁
加锁 SET + 唯一标识 + 过期时间 + NX 解锁 LUA (需要判断标识)
唯一标识 防止误释放锁
超时时间,防止客户端异常,一直持有锁
单个 Redis 节点实现分布式锁的缺点
实例发生故障宕机了,锁变量没有了,也就无法进行锁操作了
基于多个 Redis 节点实现高可靠的分布式锁
RedLock 算法
客户端和多个独立的 Redis 实例依次请求加锁 (和单实例加锁操作一样),如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败
RedLock 算法加锁三个过程
客户端获取当前时间
客户端按顺序依次向 N 个 Redis 实例执行加锁操作
一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时
加锁成功必须满足的条件
客户端获取锁的总耗时没有超过锁的有效时间
大于等于 N/2+1 个实例加锁成功
什么时候释放锁
条件不满足
锁剩余的有效时间来不及完成共享数据的操作
Redis 如何实现原子操作
把多个操作在 Redis 中实现成一个操作 —— 单命令操作
INCR / DECR
Lua 脚本
把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本
EVAL 命令来执行脚本
事务机制:Redis 能实现 ACID 属性吗
Redis 如何实现事务
MULTI 开启一个事务,命令入队,EXEC 执行队列中的命令
如何放弃一个事务
DISCARD 主动放弃事务,清空任务队列
Redis 的事务机制能保证哪些属性
原子性
命令入队时就报错,会放弃事务执行,保证原子性
命令入队时没报错,实际执行时报错,不保证原子性
命令和操作的数据类型不匹配
EXEC 命令执行时实例故障,如果开启了 AOF 日志,可以保证原子性
使用 redis-check-aof 工具,把未完成的事务操作从 AOF 文件中去除,使用 AOF 恢复实例后能保证原子性
一致性
都可以保证
命令入队时就报错,事务本身就会被放弃执行
命令入队时没报错,实际执行时报错,正确的命令可以正常执行
EXEC 命令执行时
没有开启 RDB 或 AOF,数据就没有了,能保障
RDB 快照不会在事务执行时执行
开启 AOF 可以利用 redis-check-aof 清除事务中已经完成的操作,也能保障
隔离性
MULTI 命令前有 WATCH命令,就能保证隔离性
持久性
无论采用什么持久化模式,事务的持久性属性是得不到保证的,内存数据库,持久性不是重点
Redis 主从同步 与 故障切换,可能存在的问题
读到旧数据
什么原因会导致主从数据不一致
主从库间的网络延迟
从库正在执行高复杂度的命令,之后才会执行传播过来的命令
从库 和 主库间的复制进度差值大于预期就不在该从库上读
slave-serve-stale-data no (从库只能执行 INFO、SLAVEOF)
读到过期数据
为什么会导致读到过期数据
与 Redis 过期键删除策略有关
定期删除策略
不能保证过期数据全被被删除,还是会有留存
惰性删除
对于主库,数据过期了下次访问会删除。对于从库 Redis <3.2 会直接返回
给数据设置的过期时间在从库上被延后
使用 3.2 以上,数据过期从库返回 null
使用 EXPIREAT / PEXPIREAT 将过期时间设置为具体的过期时间点,并且主从库同步时钟
不合理配置项导致的服务挂掉
protected - mode
protected-mode yes 哨兵实例只能在部署的服务器本地进行访问,与其它服务器上的哨兵无法通信,主库故障,无法判断主库下线,无法进行主从切换,最终 Redis 服务不可用
protected-mode no + bind
cluster- node-timeout 过小
Cluster 集群每个实例都是一主一从,当半数以上实例主从切换(可能)导致半数以上实例心跳超时 (cluster-node-timeout) ,集群挂掉
cluster-node-timeout 定义了集群实例被判断为故障的心跳超时时间
脑裂:一次奇怪的数据丢失
什么是脑裂
采用哨兵机制进行主从切换,如果原主库只是 "假故障" ,它会触发哨兵启动主从切换,一旦等它从假故障中恢复后,又开始处理请求,这样一来,就会和新主库同时存在,形成脑裂
什么情况下可能数据丢失
主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了;比对原主库 master_repl_offset 和 原从库 slave_repl_offset 的差值看是否是这个原因
脑裂
等到哨兵让原主库执行 (slave of) 和 新主库做全量同步后,原主库会清空本地的数据,加载新主库发送的 RDB 文件,原主库在主从切换期间保存的新写数据就丢失了
脑裂发生的原因
和主库部署在同一台服务器上的其他程序临时占用了大量资源 (例如 CPU 资源) 导致主库资源使用受限,短时间内无法响应心跳
主库自身遇到了阻塞的情况,如处理 bigkey 或是发生内存 swap 短时间内无法响应心跳
如何解决脑裂
通过配置让其在假故障期间不能接收请求,只能有新主库接受请求,等切换完成后,原主库会被哨兵降为从库,就不会出现脑裂进而出现数据丢失了
原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了
min-slaves-to-write 设置了主库能进行数据同步的最少从库数量
min-slaves-max-lag 主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟
Redis 支撑秒杀
秒杀场景的负载特征
读多写少
瞬时并发访问量非常高
Redis 如何支撑秒杀场景
高速处理请求的特性支持高并发,并且可以使用分片集群存储商品,避免请求到一个实例
保证库存查验和库存扣减原子性执行
LUA 脚本保证原子性
使用分布式锁
秒杀的各个阶段
第一阶段是秒杀活动前
CDN缓存静态化元素
第二阶段是秒杀活动开始
库存查验
库存扣减
订单处理
要保证处理的事务性(会涉及支付、商品出库、物流等,涉及多张表)
秒杀活动结束后
服务器端一般都能支撑
查验库存 和 库存扣减为什么必须放在 Redis 处理
数据库处理较慢,大量请求读取到旧值,出现超售
库存查验 和 库存扣减这两个操作需要保证原子性
数据分布优化:如何应对数据倾斜
什么是数据倾斜
实例上的数据分布不均衡,某个实例上的数据特别多
数据访问倾斜 实例的数据分布均衡,但是某个实例的数据是热点数据,导致访问频繁
如何避免数据倾斜
数据分布倾斜
bigKey
避免 bigKey
bigkey 是集合类型则把 bigkey 拆分成很多个小的集合类型数据,分散保存在不同的实例上
Slot 手工分配不均
使用迁移命令迁移到其他实例
数据访问倾斜
热点只读数据多副本
每一个数据副本的 key 中增加一个随机前缀,让他们被映射到不同的 Slot 中,并将Slot分配到不同的实例,这样不同的实例就都能提供访问
为什么读写数据不能使用多副本
要保证多副本间的数据一致性,会带来额外的开销
通信开销:限制Redis Cluster规模的关键因素
如何让集群中的每个实例都知道其它所有实例的状态信息
Gossip 协议
Gossip 协议工作原理
节点彼此不断通信交换信息 (PING PONG),一段时间后所有的节点都会知道集群完整的信息,类似流言传播
PING / PONG
实例自身 + 1 / 10 集群
16,384 Hash Slot
具体发送规则
每秒会随机选取5个节点,找出最久没有通信的节点发送 PING 消息
每隔 100 毫秒 都会扫描本地节点列表,如果发现节点最近一次接受 PONG 消息的时间大于 cluster-node-timeout/2 ,则立刻发送 PING 消息
每秒发送 PING 消息数量 ~= 1 + 10 * 实例数
如何降低实例间的通信开销
通信消息大小不能改变,因为要维持集群状态的统一
降低实例间发送消息的频率
调整 cluster-node-timeout 时间 缓解PONG消息接收超时的情况,但也要避免设置过大
过大,发生故障,就需要等待更长时间才能检测出,从而导致了故障恢复时间被延长,影响集群服务的使用
最核心就是 合理设置集群数目,避免因为集群过大,集群之间通信开销大于处理请求的开销
为什么 Redis Cluster 是16384 个槽位 2^14
Redis 的作者测试发现这个数对 2^14 求模的会将 key 在 0-2^14-1 之间分布得很均匀
如果槽位为 65536,这个 PING/PONG 消息太大
Redis 6.0
为什么引入多线程
随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度
网络 IO 方面
采用多个 IO 线程来处理网络请求,主线程会创建和客户端的连接,通过轮询方法把 Socket 连接分配给 IO 线程,同时仍然使用单线程执行命令操作
例如:多个请求进来了,轮询分发给多个线程包括主线程,多线程并行读取,解析,主线程按照请求的顺序依次执行命令
细粒度的权限控制,更好的安全性
支持创建不同用户来使用 Redis
支持以用户为粒度设置命令操作的访问权限
6.0 之前通过客户端连接实例前输入密码
服务端协助的客户端缓存功能
客户端就可以把读取的数据缓存在业务应用本地
数据被修改如何做失效处理
普通模式
key 的值发生变化,服务端会给客户端发送 invalidate 消息,通知客户端缓存失效
广播模式
RESP 3
直接通过不同的开头字符,区分不同的数据类型,来实现数据转换
性能篇
异步机制:如何避免单线程模型的阻塞
和客户端交互时的阻塞点
集合全量查询 (HGETALL,SMEMBERS) 和 聚合统计操作 (交、并和差集)
删除 bigkey 在应用程序释放内存时,空闲内存块链表操作时间就会增加
清空数据库 涉及到删除 和 释放所有的键值对
和磁盘交互时的阻塞点
AOF 日志同步写回,写磁盘,会阻塞
主从节点交互时的阻塞点
接收 RDB 快照要 FLUSHDB
加载 RDB 文件会阻塞
切片集群实例交互时的阻塞点
负载均衡或者有实例增删时,数据会在不同的实例间进行迁移(渐进式),哈希槽的信息量不大影响不大,如果有 bigkey 就会造成主线程的阻塞
应对阻塞
使用 Redis 的异步子线程机制,然后把一些(非关键路径上的任务)任务交给这些子线程,让它们在后台完成
异步的子线程机制
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,主线程会通过一个任务队列和三个子线程进行交互。主线程会把操作封装成一个任务,放入到任务队列中,然后返回给客户端已执行完成,子线程会从任务队列中取出任务,根据任务的具体类型,来执行相应的异步操作
只有非关键路径操作才可用子线程完成
什么是关键路径
读
从库加载 RDB 文件
bigkey 删除、清空数据库、AOF 日志同步写 (everysec)
4.0 之后 提供了惰性(异步) 删除
键值对删除 当集合类型中有大量元素 UNLINK 命令
4.0之前 SCAN 命令读取数据,再删除,避免一次性删除大量的 key
清空数据库 FLUSHDB ASYNC
不能用子线程如何应对阻塞
集合全量查询 和 聚合统计操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算
从库加载 RDB 文件:把主库的数据量大小控制在 2~4 GB 左右,以保证 RDB 文件能以较快的速度加载
CPU 核和 NUMA 架构的影响
绑核可以降低尾延迟
什么是尾延迟
请求的处理延迟从小到大排个序,99% 的请求延迟小于的值就是 99% 尾延迟
CPU 多核对 Redis 性能的影响
Redis 如果在不同的核上运行,就需要频繁地进行上下文切换,会增加 Redis 的执行时间
把实例和某个核绑定
CPU 的 NUMA 架构对 Redis 性能的影响
CPU Socket(插口) 就是 CPU处理器
非统一内存访问架构
在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟不一致
为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket
绑核带来的问题
物理核通常都会运行两个超线程,也叫作逻辑核
如果 Redis 实例绑定的是逻辑核,子进程和后台线程会导致主线程阻塞
应该绑定物理核,而不是逻辑核,以缓解 CPU 资源竞争
波动的响应延迟:如何应对变慢的 Redis
判断 Redis 是否变慢
基于基线性能判断
redis - cli -- intrinsic(基本) - latency(延迟时间) 120 (监控多少秒内)
系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定
响应延迟是基线性能两倍以上,就认为 Redis变慢了
避免网络影响,应该运行在服务端
要测网络对Redis性能影响可以用 iPerf 工具
影响 Redis 性能的三大要素
1. Redis 自身操作特性的影响
慢查询命令
通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求
如何应对
用其他高效命令代替 聚合计算读取到客户端,再自己处理
过期 key 操作
key 的自动删除机制 默认情况下, Redis 每 100 毫秒采样并删除过期 key
如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下
同一秒内有大量的 key 同时过期 会触发重复删除
可以在过期函数上加一个随机数
2. 文件系统对 Redis 性能的影响
潜在的风险点
AOF
everysec Redis 会使用后台的子线程异步完成 fsync 的操作
always 策略使用的主线程
AOF 重写占用了大量的磁盘 IO 带宽,可能会阻塞后台线程执行 fsync
当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞
解决方法
换机械硬盘
3. 操作系统对 Redis 性能的影响
操作系统 SWAP
Redis 是内存数据库,它本身会导致内存占用很大,如果内存不足就可能触发SWAP,Redis读取数据,则要从磁盘读入,直接导致了Redis主线程变慢
解决方法
增加机器的内存或者使用 Redis 集群(分摊每个实例服务的数据量,进而减少每个实例所需的内存量)
选用内存大的做主库
操作系统 内存大页
Redis 做持久化保存过程中,主线程仍然能够接收命令,Redis 会采用写时复制,会导致拷贝大量的页,导致性能变慢
关闭内存大页
删除数据后,为什么内存占用率还是很高
数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统,所以,操作系统仍然会记录着给 Redis 分配了大量内存
潜在的风险点:释放的内存空间可能不是连续的成为难以利用的内存碎片,还会降低 Redis 运行机器的成本回报率
内存碎片是如何形成
外因:键值对大小不一样 和 键值对修改删除 导致空间的扩容和释放
内因:按照jemalloc(默认) 2^n 次幂 固定分配,可能用不完
判断是否有内存碎片
INFO memory 中 mem_fragmentation_ratio=操作系统分配的内存空间/Redis使用的空间
mem_fragmentation_ratio 1-1.5正常
解决内存碎片的方法
重启
Redis 是单线程,清理碎片(拷贝数据)时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低
Redis 4.0 提供了碎片清理机制 将不连续的空间清理为连续的(阻塞)
config set activedefrag yes 启用自动内存碎片清理
通过合理的配置参数,减小对 Redis 性能的影响
开始清理的条件
active-defrag-ignore-bytes 100mb
碎片达到100mb
active-defrag-threshold-lower 10
占总内存10%
占CPU时间
active-defrag-cycle-min
占用 CPU 百分比最少是多少
active-defrag-cycle-max
缓冲区:一个可能引发 "惨案" 的地方
Redis 中的缓冲区的功能主要就是用一块内存空间来暂时存放命令数据,以免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题
缓冲区在Redis中的应用场景
1.暂存客户端发送的命令数据的客户端输入缓冲区
输入缓冲区溢出
写入了 bigkey,比如一下子写入了多个百万级别的集合类型数据
服务器端处理请求的速度过慢,导致客户端输入缓冲区堆积
CLIENT LIST
qbuf已使用, qbuf-free尚未使用 如果溢出了 Redis 会把客户端连接关闭
通常存在多个客户端
当多个客户端所占内存总量超过maxmemory(4GB),就会触发数据淘汰,降低了业务应用的访问性能
多个客户端,导致 Redis 内存占用过大 ,导致OOM,Redis 崩溃
应对溢出方法
没有办法调整输入缓冲区,都是固定1GB
避免bigKey
避免Redis主线程阻塞
2.暂存服务器端返回给客户端的数据结果的客户端输出缓冲区
输出缓冲区溢出
输出缓冲区 16KB 固定缓冲空间(OK+出错信息) + 动态增加的缓冲空间(存可变的响应结果)
发生输出缓冲区溢出
返回bigkey
MONITOR 命令输出结果会持续输出监测到的各个命令操作占用输出缓冲区
缓冲区大小设置得不合理
应对输出缓冲区溢出
避免 bigkey 操作返回大量数据结果
避免在线上环境中持续使用 MONITOR 命令
根据不同客户端类型设置缓冲区大小
客户端类型
常规和 Redis 服务器端进行读写命令交互的普通客户端
阻塞式发送,不做限制
订阅了 Redis 频道的订阅客户端
不属于阻塞式发送,需要设置限制
超过多大直接关闭;持续超过某大小,直接关闭
主节点上的从节点客户端
3.主从节点间进行数据同步期间,用来暂存主节点接收的写命令和数据
复制缓冲区 (全增量复制)
传输 RDB 的同时,将接收到的客户端信息保存在复制缓冲区,传输完毕后,再发给从节点执行,保证数据同步
溢出导致主节点会直接关闭和从节点进行复制操作的连接,导致全量复制失败
如何避免复制缓冲区溢出
控制主节点保存数据量的大小,2-4 GB,让全同步执行的快,避免复制缓冲区积累过多的命令
client-output-buffer-limit 配置项,来设置合理的复制缓冲区大小
控制从节点的数量,来避免主节点中复制缓冲区占用过多内存
复制积压缓冲区 (增量复制)
是一个环形缓冲区,溢出就相当于覆盖旧命令数据,一旦覆盖就要主从节点全增量复制
repl_backlog_size
缓冲空间大小 = (主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小) x 2
补充
什么是 Redis
Redis 是 C 语言开发的内存数据库,由于是内存数据库,所以读写速度非常快,常用来做缓存,除了做缓存还可以实现分布式锁,和 轻量级的消息队列,它提供了多种数据结构来应对不同的场景,还支持 持久化 、集群 与 事务
数据类型及其应用场景
String
缓存 (票证 ticket)
计数器 incr
分布式锁 setnx
List
链表、队列、微博关注人 时间轴列表等
Hash
Hash 表
Set
去重、赞、踩,共同好友 (交集)
App 每天的新增用户数
Sorted Set
评论列表 (按权重)
Redis 场景
缓存
缓存 ticket 解决分布式 Session
分布式锁
使用 SETNX 和 DEL 命令组合来实现加锁和释放锁
限流 (String incr+ LUA)
如何控制一段时间用户的访问次数
用户第一次访问的时候,设置一个过期键,key为用户 IP,值为访问次数,每次访问就增加次数,超过限制就报错。键过期后下次用户访问重新生成键值对。
基数统计 Hyp
保存时间序列数据
计数器
通过 incr 命令
GEO
基于位置信息服务 (LBS)
排行榜,Redis 的有序集合
分布式缓存常见的技术选型方案有哪些
Memcached,Redis
说一下 Redis 和 Memcached 的区别和共同点
相同
都采用哈希表作为 key - value 索引
都是基于内存的数据库,一般都用来当做缓存使用
都有过期策略
不同点
Memcached 支持的 value 类型仅为 String 类型
Redis 提供了持久化功能
Redis 支持事务
Memcached 是多线程,Redis 6.0 也是
0 条评论
下一页