实践案例
海量数据统计选型
聚合统计
统计小程序每天新增用户数和第二天的留存用户
set的差集、并集和交集
排序统计
最新列表或者排行榜
List按照先进先出排序
sorted set根据元素的权重来排序<br>
基数统计
网页不重复的UV
HyperLogLog 有误差率0.81%
面向LBS的GEO
底层是sorted set
按照经纬度对二维地图区间划分并编码,即为权重分数
时间序列数据
查询特点
点查询,根据一个时间戳查询对应时间的数据
范围查询,根据开始时间和截止时间查询
方案一
hash && sorted set
缺点
内存保存两份数据
聚合时要把数据读取到客户端
方案二
RedisTimeSeries模块
优点
直接在Redis实例上进行数据聚合,避免数据在客户端传输
缺点
底层数据结构是链表,查询复杂度是O(N)
只能返回最新的数据,无法返回任意时间点的数据
消息队列
基于Streams
自动生成全局唯一ID
使用PENDING List自动留存消息,使用XPENDING查询,使用XACK确认消息
单线程模型性能阻塞点
集合全量查询和聚合操作
bigkey删除
在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序,所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞。
异步删除 unlink
清空数据库
AOF日志同步写
从库加载RDB文件
redis变慢
方法
响应延迟
–intrinsic-latency 选项,可以用来监测和统计测试期间内的最大延迟
可能原因
慢查询命令
用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成线程阻塞。
当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
KEYS 命令需要遍历存储的键值对,所以操作延时高。所以,KEYS 命令一般不被建议用于生产环境中。
过期key操作
排查过期 key 的时间设置,并根据实际使用需求,设置不同的过期时间。
Redis 键值对的 key 可以设置过期时间。默认情况下,Redis 每 100 毫秒会删除一些过期 key,具体的算法如下:采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。
AOF重写日志
AOF 重写会对磁盘进行大量 IO 操作,同时,fsync 又需要等到数据写到磁盘后才能返回,所以,当 AOF 重写的压力比较大时,就会导致 fsync 被阻塞。虽然 fsync 是由后台子线程负责执行的,但是,主线程会监控 fsync 的执行进度。
操作系统swap
产生的原因
触发 swap 的原因主要是物理机器内存不足,对于 Redis 而言,有两种常见的情况:Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生 swap。
解决方案
增加机器的内存或者使用 Redis 集群。
内存大页
Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。
虽然内存大页可以给 Redis 带来内存分配方面的收益,但是,不要忘了,Redis 为了提供数据可靠性保证,需要将数据做持久化保存。这个写入过程由额外的线程执行,所以,此时,Redis 主线程仍然可以接收客户端写请求。客户端的写请求可能会修改正在进行持久化的数据。在这一过程中,Redis 就会采用写时复制机制,也就是说,一旦有数据要被修改,Redis 并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。两者相比,你可以看到,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,这就会影响 Redis 正常的访存操作,最终导致性能变慢。
缓存删除后内存占用率高
内存碎片
info memory可以查看碎片率
mem_fragmentation_ratio 大于 1 但小于 1.5是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由 Redis 负载决定,也无法限制。所以,存在内存碎片也是正常的。<br>mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。
清理碎片
Redis 需要启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes
active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;<br>active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。
active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;<br>active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。
缓冲区溢出
客户端的输入和输出缓冲区
主从集群中主节点上的复制缓冲区和复制积压缓冲区
子主题
影响
缓冲区溢出导致网络连接关闭:普通客户端、订阅客户端,以及从节点客户端,它们使用的缓冲区,本质上都是 Redis 客户端和服务器端之间,或是主从节点之间为了传输命令数据而维护的。这些缓冲区一旦发生溢出,处理机制都是直接把客户端和服务器端的连接,或是主从节点间的连接关闭。网络连接关闭造成的直接影响,就是业务程序无法读写 Redis,或者是主从节点全量同步失败,需要重新执行。
缓冲区溢出导致命令数据丢失:主节点上的复制积压缓冲区属于环形缓冲区,一旦发生溢出,新写入的命令数据就会覆盖旧的命令数据,导致旧命令数据的丢失,进而导致主从节点重新进行全量复制。
解决方案
针对命令数据发送过快过大的问题,对于普通客户端来说可以避免 bigkey,而对于复制缓冲区来说,就是避免过大的 RDB 文件。
针对命令数据处理较慢的问题,解决方案就是减少 Redis 主线程上的阻塞操作,例如使用异步的删除操作。
针对缓冲区空间过小的问题,解决方案就是使用 client-output-buffer-limit 配置项设置合理的输出缓冲区、复制缓冲区和复制积压缓冲区大小。当然,我们不要忘了,输入缓冲区的大小默认是固定的,我们无法通过配置来修改它,除非直接去修改 Redis 源码。
缓存淘汰
noevction(不淘汰)
进行淘汰
在设置过期时间的数据中淘汰
volatile-lru
voilatile-random
voilatile-ttl
voilatile-lfu
在所有数据中进行淘汰
allkeys-lru
allkeys-random
allkeys-lfu
最佳时间
优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,我建议你使用 allkeys-lru 策略。
如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。
如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。
缓存和数据库不一致
先删除缓存值,再更新数据库
缓存删除后,尚未更新数据库,有并发读请求
并发请求从数据库读到旧值,并且更新到缓存,导致后续请求都读区旧值
延迟双删
先更新数据库,再删除缓存
数据库更新成功后,尚未删除缓存,有并发读请求
并发请求从缓存中读到旧值
等待缓存删除完成,期间会有不一致数据短暂存在
cache aside
类似只读模式,读操作命中缓存直接返回,否则从后端数据库加载到缓存再返回。写操作直接更新数据库,然后删除缓存。这种策略的优点是一切以后端数据库为准,可以保证缓存和数据库的一致性。缺点是写操作会让缓存失效,再次读取时需要从数据库中加载。这种策略是我们在开发软件时最常用的,在使用Memcached或Redis时一般都采用这种方案。
read/write through
应用层读写只需要操作缓存,不需要关心后端数据库。应用层在操作缓存时,缓存层会自动从数据库中加载或写回到数据库中,这种策略的优点是,对于应用层的使用非常友好,只需要操作缓存即可,缺点是需要缓存层支持和后端数据库的联动。
Write Back
类似于读写缓存模式+异步写回策略。写操作只写缓存,比较简单。而读操作如果命中缓存则直接返回,否则需要从数据库中加载到缓存中,在加载之前,如果缓存已满,则先把需要淘汰的缓存数据写回到后端数据库中,再把对应的数据放入到缓存中。这种策略的优点是,写操作飞快(只写缓存),缺点是如果数据还未来得及写入后端数据库,系统发生异常会导致缓存和数据库的不一致。这种策略经常使用在操作系统Page Cache中,或者应对大量写操作的数据库引擎中。
缓存雪崩
合理地设置数据过期时间
搭建高可靠缓存集群
缓存击穿
在缓存访问非常频繁的热点数据时,不要设置过期时间;
缓存穿透
提前在入口前端实现恶意请求检测,或者规范数据库的数据删除操作,避免误删除。
使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。
分布式锁
set nx px
redisson
redlock
事务机制
ACID
只能保证一致性和隔离性
因为日志可能丢失,无法保证持久性
因为当事务中有语法错误时,会继续执行其他正常的命令,故无法保证原子性
命令
WATCH
检测一个或者多个键的值在事务执行期间是否发生变化,如果发生变化,那么当前事务放弃执行
主从同步不一致
主从数据不一致
原因:主从数据异步复制
方案:使用外部监控程序对比主从库的复制进度,不让客户端从落后的从库中读数据
读取到过期的数据
原因:过期数据删除策略
方案:使用expireat pexireat命令给数据设置过期时间点<br>
不合理配置导致服务挂掉
原因:protected-mode cluster-node-timeout配置不合理
方案:protected-mode设置为no, 调大cluster-node-timeout
脑裂
和主库部署在同一台服务器上的其他程序临时占用了大量资源(例如 CPU 资源),导致主库资源使用受限,短时间内无法响应心跳。其它程序不再使用资源时,主库又恢复正常。
主库自身遇到了阻塞的情况,例如,处理 bigkey 或是发生内存 swap(你可以复习下第 19 讲中总结的导致实例阻塞的原因),短时间内无法响应心跳,等主库阻塞解除后,又恢复正常的请求处理了。
codis&&redis cluster
codis proxy 和 codis server 负责处理数据读写请求,其中,codis proxy 和客户端连接,接收请求,并转发请求给 codis server,而 codis server 负责具体处理请求。
codis dashboard 和 codis fe 负责集群管理,其中,codis dashboard 执行管理操作,而 codis fe 提供 Web 管理界面。
Zookeeper 集群负责保存集群的所有元数据信息,包括路由表、proxy 实例信息等。这里,有个地方需要你注意,除了使用 Zookeeper,Codis 还可以使用 etcd 或本地文件系统保存元数据信息。
数据倾斜
数据量倾斜
存在bigkey
业务层避免创建bigkey
把集合类型的bigkey拆分为多个小集群,分散保存
slot手工分配不匀
制定运维规范,避免把过多slot分配到一个实例上<br>
根据hash tag,导致大量数据集中到一个slot
如果hash tag会造成数据倾斜,优先避免数据倾斜,不使用hash tag
redis cluster通信<br>
gossip协议通信
Redis Cluster 运行时,各实例间需要通过 PING、PONG 消息进行信息交换,这些心跳消息包含了当前实例和部分其它实例的状态信息,以及 Slot 分配信息。这种通信机制有助于 Redis Cluster 中的所有实例都拥有完整的集群状态信息。
集群中大规模的实例间心跳消息会挤占集群处理正常请求的带宽。而且,有些实例可能因为网络拥塞导致无法及时收到 PONG 消息,每个实例在运行时会周期性地(每秒 10 次)检测是否有这种情况发生,一旦发生,就会立即给这些 PONG 消息超时的实例发送心跳消息。集群规模越大,网络拥塞的概率就越高,相应的,PONG 消息超时的发生概率就越高,这就会导致集群中有大量的心跳消息,影响集群服务正常请求。
Redis Cluster 的规模控制在 400~500 个实例。
cluster-node-timeout 配置项减少心跳消息的占用带宽