单机 QPS
单机 QPS 能力参考范围为 8 - 10 万。
为什么 Redis 这么快
- 用 C 语言编写的,执行效率高
- 基于内存的数据库,避免磁盘IO操作
- 采用高效的数据结构
- 合理的数据编码,同样的数据结构在不同数据量的情况下采用不同的编码方式
- 采用单线程,避免上下文切换
- 多路IO复用,一个线程处理多个大量Socket请求。
- 虚拟内存
虚拟内存
Redis 的数据并不是完全在内存中,当 Redis 的数据量大于物理内存容量时,会通过虚拟内存的手段,将不经常使用的数据(冷数据)存储到硬盘上,以释放物理内存空间。
在虚拟内存中,数据被分为多个页面,每个页面大小默认 32 字节。显然,虚拟内存可以节省内存,但也带来一定的性能损失。
持久化
AOF
采用写后日志的方式,先执行命令,再将操作日志以文本形式追加到文件中。
为什么采用写后日志?(Mysql是采用写前日志)
- 避免出现记录错误命令的情况,并且 AOF 写日志也是在主线程中进行的。
优缺点
- 优点
- 故障不丢失,写回策略默认是每秒一次,保证故障最多丢失一秒的数据
- 缺点
- 文件体积较大
- 恢复速度更慢
写回(硬盘)策略
- Always(总是),每次写操作命令执行完成后,同步将 AOF 日志数据写回硬盘。本质是每次写操作都执行 fsync() 函数。
- EverySec(每秒),每次写操作命令执行完成后,将命令写入到 AOF 的内核缓冲区,每秒将缓冲区的内容写回到硬盘。本质是每秒创建一个异步任务执行 fsync() 函数。
- No,意味着交给操作系统控制写回的时机。本质是不执行 fsync() 函数。
重写(BGREWRITE)
当 AOF文件太大时,Redis fork出一个子进程调用 bgrewriteaof 方法重写一个新的文件,比如increase(1)和increase(1),会被合并成set(2)。也用到了 写时复制。
RDB
将某一时刻的内存数据以二进制的形式写入磁盘。
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
save 和 bgsave 两条命令触发 RDB 持久化操作。save 命令在主线程中执行,会导致阻塞。而 bgsave 创建一个子线程,避免阻塞,是默认的 RDB 的默认配置。并且 bgsave 允许执行快照命令期间修改数据,因为子线程会创建一个修改后的副本再写入(写时复制)。
优缺点
- 优点
- 体积更小,二进制文件
- 恢复更快,适合备份
- 性能更高,只需要fork子进程
- 缺点
- 故障丢失,每隔一段时间(最少五分钟)持久化一次,如果发生故障,意味着这段时间内的所有数据都丢失
- 耐久性差,数据量大的时候,fork会很耗时
Copy On Write(写时复制)
原理:fork() 后的子进程与父进程共享内存空间,如果是读取数据,相安无事;如果是写数据,会检测到内存页是只读然后触发中断最后复制一份数据再做修改。
好处:减少不必要的资源分配;减少分配和复制大量资源时带来的瞬间延时。
缺点:如果父子进程都在进行大量的写操作,会产生大量的分页错误。
混合持久化
结合 AOF 和 RDB 两种持久化方式。混合持久化只发生于 AOF 重写过程。 重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
缺点:不再是 AOF 格式的文件,可读性差。
数据结构
Redis 7之后使用 Listpack(紧凑列表) 数据结构代替 Ziplist。
- String: Simple Dynamic String(SDS)
- List: Quicklist(3.2之后);LinkedList、Ziplist(3.2之前)
- Hash: HashTable(哈希表)、ZipList。当同时满足以下两个条件的时候,Hash 对象使用 ZipList 编码否则使用 Hashtable:a、每个Field-Value的最大长度小于等于64字节;b、Field-Value最大数量小于512个。
- Set: HashTable、Intset。当同时满足以下两个条件的时候,Set 对象使用 Intset 编码否则使用 Hashtable:a、结合对象保存的所有元素都是整数值;b、集合对象保存的元素数量不超过512个。
- Sorted set: ZipList(压缩列表)、SkipList(跳表)。当同时满足以下两个条件的时候,Sorted set 对象使用 ZipList 编码否则使用 SkipList:a、每个元素空间小于64字节;b、集合中元素数量小于128个。
- Stream: 借鉴 Kafka 的设计,是一个支持多播且可持久化的消息队列。与 PUB/SUB 模式相比,无法持久化。与基于 List LPUSH 和 BRPOP 或 Sorted set 相比,不支持多播分组消费。
- Bitmap: 位图
- Hyperloglog: 基数(不重复的元素)统计,但是会存在一定误差。可用于比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等允许一定容错的业务场景。
- Geospatial index: 实现两个位置距离的计算、获取指定位置附近的元素等功能。
消息通信模式:pub/sub,不支持消息持久化
- 基于频道(channel)
- 基于模式(pattern)
注意事项:
- 客户端需要及时消费,否则会自动断开连接或丢数据
- 连接断开后需要重新连接,否则无法收到消息
- 该消息模式不是一种可靠的消息系统。当出现客户端连接退出,或者极端情况下服务端发生主备切换时,未消费的消息会被丢弃。
Hash 数据结构下的 rehash 过程:
- 为 ht[1] 分配空间,作为 rehash 后的哈希表。
- 构造一个索引技术器变量 rehashidx 并初始化为零,表示 rehash 正在执行,否则 rehashidx 为 -1 表示没在进行。
- 在 rehash 期间,删改查操作都是先在旧表上操作,并把旧表的数据迁移到新表;新增的操作直接在新表上新增,并且 rehashidx 自增。
- 除此之外,没有增删改查的操作时,还会有一个定时任务周期性(每100ms触发一次)的迁移数据
好处:将一次性的大批量拷贝分摊到多个请求中。
缓冲区
共有三个缓冲区:
- 客户端输入/输出缓冲区
为了解决客户端和服务器端的请求发送和处理速度不匹配所设置的。
输入缓冲区会先暂存客户端发送过来的命令,Redis 主线程从输入缓冲区中读取命令,进行处理。当 Redis 主线程处理完数据后,会把结果写入到输出缓冲区,再从输出缓冲区返回给客户端。
缓冲区溢出的可能情况:BigKey,缓冲区大小不合理。
- 复制缓冲区,复制积压缓冲区
用于Redis主从节点之间复制时使用的。由于主从节点间的数据复制包括全量复制和增量复制两种。因此也分为复制缓冲区和复制积压缓冲区两种。
复制缓冲区是 Redis 在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。
复制积压缓冲区是一个大小有限的环形缓冲区。当服务器断线重连后,复制积压缓冲区的内容会被发送到从节点,当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据,会造成主从节点的数据不一致。在命令传播阶段出现断网的情况,或者网络抖动时会导致连接断开,此时主节点会向复制积压缓冲区写数据。
- AOF 缓冲区,AOF 重写缓冲区
AOF 缓冲区:Redis在AOF持久化的时候,会先把命令写入到AOF缓冲区,然后通过回写策略(always、everysec、no)来写入硬盘AOF文件。
AOF 重写缓冲区:当需要进行AOF重写时,主进程会fork一个子进程进行AOF重写,此时主进程还要继续接收客户端传来的指令,此时这些指令就会存放在AOF重写缓冲区中;当AOF重写完成后,将AOF重写缓冲区中的指令追加到aof文件中,并将原来的aof文件替换,来保证数据的一致性。(与主从复制中的复制缓冲区类似)
单线程与多线程
redis 4.0 后部分命令开始使用多线程。
为什么使用单线程?
- Redis 的瓶颈是内存和带宽,而不是 CPU。
什么是多路IO复用:一个服务端进程可以同时处理多个客户端连接。用于AOF持久化任务和处理客户端请求。其实现函数有:
- select:数据结构是bitmap,采用轮询,限制连接数为1024个(最大文件描述符数量)
- poll:数据结构是数组,解决了select的个数限制,但依旧是轮询
- epoll:数据结构是红黑树,使用回调的方式,解决了个数限制,也解决了轮询方式
- 边缘触发(Edge Trigger,ET),更适合高流量或发送大文件的场景,例如 nginx
- 水平触发(Level Trigger,LT),默认,能保证事件一定被处理。
为什么 Redis 又支持多线程了?
- 使用多线程仅在部分非阻塞的删除操作上面,通过多线程非阻塞释放内存,减少对主线程的阻塞,提高执行效率。
高可用(主从,哨兵,集群)
主从模式
主节点可以读、写,从节点只能读
- slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照,并使用复制缓冲区记录保存快照这段时间内执行的写命令
- master将保存的快照文件发送给slave,并继续在复制缓冲区记录执行的写命令
- slave接收到快照文件后,加载快照文件,载入数据
- master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
- 此后master每次执行一个写命令都会同步发送给slave,同时也会写入复制积压缓冲区,保持master与slave之间数据的一致性,如果主从之间断开连接,那么重连后会根据该缓冲区中的偏移量将断开连接这段时间的数据修改写入从节点。
主从异步复制,因此有可能存在数据不一致的情况。
请求转发
主从模式下,服务端并不做转发处理。而要实现读写分离的功能,需要客户端自行处理了。比如要自行定位master节点,然后将写请求发送过去,读请求则可以做负载均衡处理。
哨兵模式
哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。
哨兵每2秒向pubsub频道接收和发送hello信息,实现哨兵之间的通信。
哨兵每1秒向所有主从节点和其他哨兵发送PING命令来判断其是否正常运行。
哨兵每10秒向主从节点发送INFO命令来获取节点信息(被标记为客观下线后每1秒一次)。
如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。其他哨兵会尝试与节点建立连接,如果大于配置文件中设定的值,则认定节点「客观下线」。
集群模式
Redis 集群无法保证强一致性,选择了高可用和分区容忍,即AP。
将不同的数据分布在不同的节点中,分布的规则采用哈希槽算法。共有 16384 (2^14)个哈希槽。
不使用一致性哈希算法的原因
- 一致性哈希环是顺时针映射,优先考虑的是最少的节点数据发生数据迁移
- 哈希槽是静态映射的,优先考虑的问题是数据均匀分布,根据不同机器的性能,可以手动分配不同的哈希槽数量到不同机器上
请求转发
在集群模式下,对于读/写请求,并非是服务端转发请求,而是服务端返回转移指令,通知客户端数据所在节点,并让客户端重新发起请求
事务
Redis 有事务但没有回滚,因为Redis认为事务的失败都是使用者造成的。
缓存雪崩、击穿、穿透
- 缓存雪崩:大量缓存失效或 Redis 宕机时,大量请求访问到数据库,导致数据库压力骤增。
- 解决方法:
- 大量失效:均匀设置过期时间或缓存预热
- 宕机:设置 Redis 高可用集群,熔断或限流
- 解决方法:
- 缓存击穿:热点数据过期,大量请求访问到数据库
- 解决方法:热点数据不设置过期时间
- 缓存穿透:访问的数据既不在缓存中也不在数据库中,被黑客攻击。
- 解决方法:使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。
缓存策略
- Cache-Aside Pattern 旁路缓存模式;读,先从缓存读,未命中从数据库读;写,先写数据库,再写缓存
- Read/Write-Through Pattern 读/写穿透模式;在旁路缓存模式的基础上封装一层缓存服务,读写操作都调用该缓存服务。读。先从缓存读,未命中数据库读并写到缓存中再返回。
- Write-Behind Pattern 异步回写模式;与读/写穿透模式类似,缓存系统只同步写缓存,异步批量写数据库
旁路缓存下的一致性问题
不存在实时一致性,只能保证最终一致性。
首选
- 写策略:先更新数据库,再删除缓存。
- 读策略:先读缓存中数据,如果缓存中没有,则读数据库的数据并且写入缓存中。
如果对缓存命中率有要求,可以采用先更新数据库再更新缓存 + 分布式锁的方案。分布式锁保证同一时间只运行一个请求更新缓存。
最终一致性方案:
主要考虑以下两点:
- 第一个操作成功,但第二个失败会有什么问题
- 高并发场景下数据会不会不一致
如果采用先删除缓存,再更新数据库如何避免出现脏数据?
延迟双删:先删除缓存,更新数据库,睡眠,再次删除缓存。睡眠的时间一般是一次查询的耗时 + 几百毫秒。第二次的删除可以清除掉因并发导致的缓存脏数据。
1
2
3
4
5
6
7
8
#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)
缓存删除重试:为了保证缓存删除成功,需要在缓存失败时增加重试机制。可以借助消息队列,将删除失败的数据进行异步重试。
BinLog缓存删除方案:BinLog存储了对数据库的更改操作日志记录,通过订阅该日志(如:canal),获取需要更新缓存的key和数据。
过期删除策略和内存淘汰策略
过期删除策略
- 定时删除策略:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
- 优点:内存可以最快的被释放。
- 缺点:删除过期key会占用 CPU 时间,对 CPU 不友好。
- 惰性删除策略:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 优点:只有在访问的时候检查是否过期,对 CPU 友好。
- 缺点:过期的 key 没被删除,对内存不友好。
- 定期删除策略:每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
- 优点:减少删除操作对 CPU 的影响,也能删除一部分过期的数据。
- 缺点:难以确定定期删除的频率。
Redis的删除策略是惰性删除+定期删除。定期删除如果检查到过期key的数据超过一定百分比,循环再次检查并删除。
内存淘汰策略
不进行数据淘汰(默认的内存淘汰策略),写入时禁止写入并报错。
进行数据淘汰
- 在设置了过期时间的数据中进行淘汰
- volatile-random: 随机淘汰设置了过期时间的任意键值;
- volatile-ttl:优先淘汰离过期时间最近的键值;
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值;
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
- 在所有数据范围内进行淘汰
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu:淘汰整个键值中最少使用的键值。
- 在设置了过期时间的数据中进行淘汰
近似 LRU 算法
出于节省内存的考虑(不需要为所有数据维护一条大链表),Redis 的 LRU 算法并非完整的实现,是一个基于哈希表的近似 LRU 算法,通过对少量键进行取样,然后回收其中的最久未被访问的键。通过调整每次回收时的采样数量 maxmemory-samples,可以实现调整算法的精度。
RedLock 红锁
红锁是一种分布式锁。在 Redis 单独节点的基础上,RedLock 使用了多个独立的 Redis 的 Master 实例(通常建议是奇数个,比如 5 个),共同协作来提供更强健的分布式锁服务。
特点
- 互斥性:任意时刻,只有一个客户端能持有锁
- 防止死锁:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
- 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
实现思路
RedLock 是对集群的每个节点进行加锁,如果大多数节点(N/2+1)加锁成功,则才会认为加锁成功。
这样即使集群中有某个节点挂掉了,因为大部分集群节点都加锁成功了,所以分布式锁还是可以继续使用的。
分布式锁超时了怎么办
答:设置看门狗(定时器)
大 Key/ 热 Key
大 Key
指 Redis 的 Key 对应的 Value 过大,并不是指 Key 过大。
危害
- 性能问题:读取大 Key 消耗更多的网络带宽和处理时间
- 引发网络阻塞:如果一个 Key 的大小是 1MB,每秒访问量是 1000,那么每秒会产生 1000 MB 的流量
- 内存分配不均
- 持久化问题:持久化变得更耗时
解决方案
- 拆分大 Key,将数据分散到多个小 Key 中
- 定期清理或设置过期时间
- 数据压缩
- 选取合适的数据结构
如何找到大 Key
- 使用
redis-cli --bigkeys
命令,不建议 - 使用
scan
命令 - 使用 RdbTools 工具
如何删除大 Key
- 系统低峰期使用
del
命令(会造成线程阻塞) - 使用
scan
命令(会造成线程阻塞) - 使用
unlink
异步删除
热 Key
指短时间内被频繁访问的数据
危害
- 可能会缓存击穿,使存储层访问量激增
- 负载不均衡,热 Key 可能导致某些 Redis 节点负载过高
- 性能瓶颈,占用大量 CPU 资源,影响其他请求
解决方案
- 读写分离(最重要)
- 对热点数据分片,分散到不同的 Redis 实例,提升吞吐量
如何找到热 Key
- 使用
monitor
命令 - 使用
redis-cli --hotkeys
命令,不建议
为什么100万QPS的热key请求可能会影响1000万QPS的Redis实例
- 100万个请求在单线程的模型下会串行处理,阻塞其他请求
- 热 key 的能承载的QPS是由单个 key 的处理速度决定的
- 1000万QPS可能是集群模式下能承载的请求量