为什么用 Redis 作为 MySQL 的缓存?
主要是因为 Redis 具备「高性能」和「高并发」两种特性
- 高性能
- 用户第一次访问 MySQL 中的数据会比较慢,因为是从硬盘读取
- 之后访问相同的数据缓存在 Redis 中,直接操作内存,速度相当快
- 如果修改数据需要同时修改 Redis 和 MySQL 两个地方,会有双写一致性的问题
- 高并发
- 单台设备 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 个哈希函数」两部分组成
标记存在:
- 使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值
- 将 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的索引
- 将每个哈希值在位图数组的索引的值设置为 1
查询:
- 以同样的算法拿到 N 个位图数组索引
- 查询响应索引的值:如果都是 1,代表存在;如果有一个为 0,就代表不存在
因为存在哈希冲突的可能性,所以查询到数据存在,并不一定证明存在,会有一定可能不存在(虽然可能性很低);但查询到数据不存在,则一定不存在