为什么用 Redis 作为 MySQL 的缓存?

主要是因为 Redis 具备「高性能」和「高并发」两种特性

  1. 高性能
    • 用户第一次访问 MySQL 中的数据会比较慢,因为是从硬盘读取
    • 之后访问相同的数据缓存在 Redis 中,直接操作内存,速度相当快
    • 如果修改数据需要同时修改 Redis 和 MySQL 两个地方,会有双写一致性的问题
  2. 高并发
    • 单台设备 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w
    • 直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存而不经过数据库

缓存雪崩

大量缓存数据在同一时间过期或 Redis 故障宕机时,用户的请求会大量直接访问数据库,从而导致数据库压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃

发生缓存雪崩有两个原因:

  • 大量数据同时过期
  • Redis 故障宕机

大量数据同时过期

针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法:

  • 均匀设置过期时间
  • 互斥锁
  • 后台更新缓存

均匀设置过期时间

给缓存数据设置过期时间,避免将大量的数据设置成同一个过期时间,可以在设置过期时间加上一个随机数

互斥锁

当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 中),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或默认值

实现互斥锁时,最好设置超时时间,不然当拿到锁的请求发生阻塞时,其他请求一直拿不到锁,整个系统就会出现无响应的现象

后台更新缓存

缓存不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。事实上,不设置有效期并不是永久有效,当内存紧张时会执行淘汰策略

在缓存被“淘汰”到下一次后台定时更新缓存期间,业务线程读取不到缓存键值。解决这个问题有两种方式:

  • 轮询:后台线程不仅负责定时更新缓存,也负责频繁地检测缓存是否有效,检测到缓存失效就从数据库读取数据,并更新到缓存
  • 通知:业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程监听消息队列,检测到缓存失效就从数据库读取数据,并更新到缓存

在业务刚上线时,最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情

Redis 故障宕机

针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法:

  • 缓存雪崩发生后:服务熔断或请求限流
  • 避免缓存雪崩的发生:构建 Redis 高可靠集群

服务熔断或请求限流机制

  • 可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误
  • 或启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务

等到 Redis 恢复正常并把缓存预热完后,再解除服务熔断或请求限流

构建 Redis 高可靠集群

通过主从节点的方式构建 Redis 缓存高可靠集群,如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮

可以发现缓存击穿和缓存雪崩很相似,可以认为缓存击穿是缓存雪崩的一个子集

应对缓存击穿可以采取两种方案:

  • 互斥锁:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
  • 后台更新缓存:不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据即将过期前,提前通知后台线程更新缓存并重新设置过期时间

缓存穿透

当发生缓存雪崩或击穿时,数据库中还是保存了数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力

而缓存穿透不一样:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求先访问缓存再访问数据库,没办法构建缓存,后续请求也是先访问缓存再访问数据库。当有大量这样的请求到来时,数据库的压力骤增

缓存穿透的发生一般有两种情况:

  • 业务误操作:导致缓存和数据库中的数据都被误删除
  • 黑客恶意攻击:故意大量访问某些读取不存在数据的业务

应对缓存穿透,常见的方案有:

  • 非法请求的限制
  • 缓存空值或默认值
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

非法请求限制

在 API 入口处需要判断请求参数是否合理、是否含有非法值、请求字段是否存在等,如果判断是恶意请求就直接返回错误,避免进一步访问缓存和数据库

缓存空值或默认值

当数据库也不存在时,在缓存中设置一个空值或默认值,这样后续请求就可以从缓存中读取到值,而不会继续查询数据库

布隆过滤器

在写入数据库数据时,使用布隆过滤器做标记,用户请求时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用查询数据库了

Redis 自身支持布隆过滤器

布隆过滤器的实现

由「初始值都为 0 的位图数组」和「N 个哈希函数」两部分组成

标记存在:

  1. 使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值
  2. 将 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的索引
  3. 将每个哈希值在位图数组的索引的值设置为 1

查询:

  1. 以同样的算法拿到 N 个位图数组索引
  2. 查询响应索引的值:如果都是 1,代表存在;如果有一个为 0,就代表不存在

因为存在哈希冲突的可能性,所以查询到数据存在,并不一定证明存在,会有一定可能不存在(虽然可能性很低);但查询到数据不存在,则一定不存在