0%

Redis-app

缓存穿透、击穿、雪崩

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据, 在高并发下对不存在的 key 的操作. 由于缓存是不命中时被动写的, 并且出于容错考虑, 如果存储层查不到数据则不写入缓存, 这将导致这个不存在的数据每次请求都要到存储层去查询, 失去的缓存的意义. 在流量大时, 可能引起数据库崩溃. 或者有人利用不存在的 key 频繁攻击应用, 可能会引起应用的崩溃

解决办法

  • 接口层增加校验, 如用户鉴权校验、id 做基础校验、 id <= 0 的直接拦截
  • 从缓存取不到的数据, 在数据库中也取不到时,可以将 key-value 写为 key-null, 缓存有效时间设置短点, 这样可以防止攻击用户反复用同一个 key 暴力攻击
  • 布隆过滤器, 类似于一个 hash set, 用于快速判断某个元素是否存在于集合中, 其典型的应用场景就是快速判断一个 key 是否存在于某容器, 不存在就直接返回. 布隆过滤器的关键就在于 hash 算法和容器大小

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期), 在高并发下对同一 key 的操作. 如果在缓存中没有获取到数据, 又同时在数据库中获取到数据, 引起数据库压力过大.

解决办法

  • 设置热点数据永不过期
  • 接口限流与熔断、降级, 重要的接口一定要做好限流策略, 防止用户恶意刷接口, 同时要降级准备, 当接口中的某些服务不可用时, 进行熔断, 失败快速返回机制
  • 加互斥锁

缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间, 而查询数据量巨大, 引起数据库压力过大甚至崩溃. 和缓存击穿不同的是, 缓存击穿指并发查询同一条数据, 缓存雪崩是不同数据都过期了, 很多数据都查不到从而查询数据库

解决办法

  • 缓存数据的过期时间设置随机, 防止同一时间大量数据过期现象发生
  • 如果缓存数据库是分布式部署, 将热点数据均匀分布在不同的缓存数据库中
  • 设置热点数据永不过期

慢查询

Redis 慢查询和 Redis 定义慢查询的 阈值 有关

slowlog-log-slower-than 10000 单位微秒, 当 Redis 命令的执行时间超过该值时, Redis 将其记录在 Redis 的慢查询日志中
slowlog-max-len 128 记录的条数超过时会只存储最新的 slowlog-max-len 条

使用复杂度过高的命令

复杂的命令一般指 O(N)以上的命令, 如 sort、sunion、zunionstore 聚合类的命令, 或是 O(N)类的命令, 对于 O(N)以上的命令, Redis 在操作内存数据时耗时过高, 会耗费更多的 CPU 资源, 导致查询变慢
Redis 是单线程处理客户端请求的, 如果遇到处理上面的请求时, 就会导致后面的请求发生排队, 对于客户端来说响应时间就会变长

解决问题的原则

  • 尽量不使用 O(N)以上的命令, 某些数据需要排序或者聚合操作时, 可以放在客户端处理
  • 执行 O(N)命令时, 保证 N 尽量的小(推荐 N <= 300), 每次获取尽量少的数据, 让 Redis 可以及时处理返回

大 Key 问题

通常是 key 对应的 value 值过大, 此类问题在 SET/DEL 这类命令中也会出现慢查询
SET/DEL 的过程

  • 写入数据: 为该数据分配内存空间
  • 删除数据: 释放该数据对应的内存空间

当数据值较大时, Redis 分配数据内存和释放内存空间都比较耗时

解决问题的原则

  • 尽量避免写入大 Key(不要写入无关的数据, 数据实在过大进行拆分, 通过多 key 存储)
  • 如果 Redis 是 4.0 以上版本, 尽量使用 UNLINK代替 DEL命令, 此命令将删除 key 和内存回收放到其他线程执行, 从而降低对 Redis 的影响
  • 如果 Redis 是 6.0 以上版本, 可以开启 lazy-free, 在执行 DEL 命令时、释放内存也会放到其他线程中执行

lazyfree-lazy-user-del no 修改 DEL 默认命令的行为使其更接近于 UNLINK命令, 默认不开启

集中过期

Redis 过期策略

  • 被动过期: 只有当访问某个 key 时, 才会检测该 key 是否已经过期, 如果已经过期则从实例删除该 key
  • 主动过期: Redis 内部存在一个定时任务, 默认每间隔 100 毫秒就会从全局的过期哈希表中随机取出 20 个 key, 然后删除其中过期的 key, 如果过期 key 的比例超过了 25%, 则继续重复此过程, 直到过期 key 的比例下降到 25% 以下, 或者这次任务的执行耗时超过了 25 毫秒, 才会退出循环

主动过期 key 的定时任务是在 Redis 主线程中执行的, 如果在执行主动过期的过程中, 出现了集中过期, 就需要大量删除过期 key, 如果此时应用程序在访问 Redis 时, 必须等待这个过期任务执行结束, 此时 Redis 就有可能产生慢查询

解决问题的原则

  • 避免集中过期, 比如将过期时间随机化, 添加一个随机的值, 分散集中过期 key 的过期时间, 降低 Redis 清理过期 key 的压力
  • 如果 Redis 是 4.0 以上版本, 可以开启 lazy-free, 当删除过期 key 时, 把释放内存的操作放到其他线程中执行, 避免阻塞主线程

分布式 UUID

数据库自增主键

向表中插入一条记录返回主键 ID, 性能瓶颈在于数据库的访问量

1
2
3
4
5
CREATE TABLE id_generator (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
allocation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 分配时间戳
PRIMARY KEY(id) -- 自增主键, 记录的唯一标识符
)

数据库自增主键集群

数据库实例水平拆分, 每个数据库实例设置不同的初始值和相同的自增步长.
例如 DB1 初始值为 1, 自增步长为 3. DB2 初始值为 2, 自增步长为 3, DB3 的初始值为 3, 自增步长为 3, DB1 生成的 ID 将为 1, 4, 7, 10…

  • 多实例, 不能保证单调递增; 强依赖数据库
  • 数据库宕机服务不可用, 性能取决于数据库主库的写性能
  • 水平扩容比较难; 增加机器较复杂

数据库号段

数据库一次存储一个号段的基本信息, 并使用乐观锁的机制确保数据库记录的更新是安全的.
每次从数据库取一个号段出来, 并在内存中采用线程安全的原子操作进行发号, 用完后再去数据库获取新的号段

  • 高可用, 不强依赖数据库
  • 内存中缓存号段, 即使数据库宕机, 短时间内仍能对外提供服务
  • 不实时读取数据库, 性能高
  • 服务重启会丢号

Redis 实现分布式ID

INCR 命令实现分布式 ID

基于 UUID 实现分布式ID

UUID 是通过一系列算法生成的 128 位长度的数字, 通常基于时间戳、计算机硬件标识符、随机数等元素来生成

基于雪花算法实现分布式 ID