缓存问题解决方案

《服务器开发技术、方法与实用解决方案》

一、缓存预热

在系统刚启动或活动刚开始时,如果缓存中没有数据,那么大量请求将直接访问数据库。如果瞬时访问流量巨大,则可能导致数据库因过载而宕机,甚至引发系统雪崩。因此需要将缓存的数据加载到缓存系统中,以减轻数据库的压力

1. 应用启动预热策略

本地缓存,在应用启动时进行缓存预热。如在Spring中实现InitializingBean接口中的afterProperitesSet方法实现

但是该策略仅适合预热本地缓存,且与应用深度耦合

2.任务调度预热策略

如果预热场景涉及的数据规模非常大,可以采用任务调度预热

使用分布式任务调度中间件定时、周期触发缓存预热数据。常用的中间件有阿里的SchedulerX、蚂蚁的AntScheduler、XXL-JOB、ElasticJob

分布式任务调度中间件的基本架构:

  • Scheduler Console:控制台。可视化集中管理平台
  • Scheduler Server:服务端,负责客户端任务的调度触发、任务执行状态的监测、集群间任务分配等
  • Scheduler Client:客户端,多个,负责具体任务执行

预热步骤

  1. 数据分片:将需要处理的数据按照一定规则划分为若干份
  2. 数据加载:根据客户端业务服务器的数量、机房属性等将分片单元拆分为若干份,指派到多台客户端业务服务器中
  3. 数据处理:客户端根据任务调度中间件下发的业务参数(id)访问数据库,并将读取的商品数据写入分布式缓存

周期调度:数据规模巨大时,无法通过一次调度完成缓存预热,可以采用周期调度,指定每个调度周期预热的数据量。为了避免在数据加载环节重复服务数据,需要对已预热过的数据打标,如更新预热时间、预热版本号等。在加载数据时,通过标记来识别、加载数据

3. 模拟请求预热策略

任务调度预热和应用启动预热都需要定制开发,对业务应用有入侵。

可以采用模拟请求预热,通过模拟用户请求来实现数据预热,如模拟调用数据查询接口。该方式对业务应用的侵入非常小

4. 缓存预热思路

  1. 分析业务场景和用户行为来圈定需要预热的数据范围
  2. 根据待预热数据的特定设计合适的预热策略
  3. 根据业务需要为不同的预热策略编排执行时间

二、缓存淘汰

缓存大多是基于内存实现的,空间相对有限。为了高效利用存储空间,当缓存触及设定的空间上限时,通常需要借助淘汰算法将部分数据移除

1. LRU

LRU(最近最少使用)算法根据数据的历史访问记录优先淘汰最近未被使用的数据,核心思想为:如果数据最近被访问过,那么将来被访问的频率会更高。

实现一般采用Hash表+双向链表,将被访问的数据放到链表头部,淘汰链表尾部的数据

LRU算法简单、高效,但是一些偶发性、周期性的批量操作会导致LRU算法的缓存命中率急剧下降。如一次性读取大量数据块,这些数据块会占用大量空间,并淘汰最近最少使用的数据。但是这些数据并非是经常被访问的数据,从而导致缓存命中率低下

2. LFU

LFU(最近最不常用)算法根据数据的历史访问频次来淘汰数据,核心思想为:如果数据最近被使用的频率高,则将来大概率会被再次使用

LRU倾向于保留最近使用的数据,LFU倾向于保留使用频率较高的数据。LFU能避免偶发性情况对命中率的影响,但是一旦访问内容发生较大变化,LFU需要更长的时间来适应,因为历史的频次记录会使已无用的数据保留较长一段时间

3. ARC

ARC(自适应缓存替换)算法兼具LRU和LFU的优点,既可以根据时间进行优化,缓存最近使用的数据,又可以根据频率进行优化,同时还能根据负载来灵活调整缓存策略。ARC的命中率要显著优于LRU和LFU。

实现原理见P218

三、缓存更新

1. Cache Aside

  • 读场景:先读缓存、若缓存命中则从缓存取数据,直接返回;若缓存未命中,则从数据库读取数据,成功后将数据放到缓存中
  • 写场景:先更新数据库,再失效缓存
  1. 是否可以先更新缓存,再更新数据库
    一旦出现缓存成功而更新数据库失败的情况,会导致缓存错误值,导致业务上无法接受的一致性问题
  2. 是否可以先失效缓存,再更新数据库
    通常数据库的写操作耗时大于读操作,一旦更新数据库期间有读请求并未命中缓存,会导致写回的数据为脏数据
  3. 为什么是失效缓存,而不是更新缓存
    如A、B两个线程都执行写操作,A线程早于B执行(B数据是最新的),但A线程晚于B线程更新缓存,这样就会导致缓存中的数据是A线程更新的旧值,而不是B线程更新的最新值
  4. 先更新数据库,再失效缓存是否存在问题?
    可能存在更新数据成功,失效缓存失败,导致缓存脏数据的问题。这种场景最常用的解决方案是为缓存设置失效时间,到期自动失效

事实上,保证缓存和数据库的强一致性是非常困难的,一般须通过Paxos或2PC来保证一致性,但这些协议耗时或实现复杂,实用性不高。在大多数使用缓存场景中,通常有一个重要前提,可接受弱一致性

2. Read/Write Through 模式

Read/Write Through 模式将更新数据库的操作交由缓存代理,从应用的角度看,缓存是主存储,应用层的读写操作均面向缓存,缓存服务代理对数据库的读写

  • Read Through:若缓存未命中,则由缓存服务负责加载数据写入缓存
  • Write Through:数据更新时,若没有命中缓存,则直接更新数据库;如命中缓存,则更新缓存,由缓存服务更新数据库(同步更新)

3. Write Behind Caching

Read/Write Through 模式同步更新数据库和缓存,而Write Behind Caching只更新缓存,缓存会异步批量更新数据库(类似Linux的Writeback机制)

但是该机制无法保证数据强一致性,且可能会丢失数据(没有完美的机制)

四、缓存雪崩

缓存雪崩是指缓存因为某些原因(如大量key集中过期、服务器宕机等)导致大量查询请求到达服务端数据库,造成数据库压力骤增、甚至宕机、进而引起整个系统崩溃的现象

1. 缓存常驻策略

为缓存设置较长的有效期,保证缓存在业务高峰期一定不会过期

2. 多级缓存策略

本地缓存作为一级缓存,分布式缓存作为二级缓存。多级缓存相结合,可以有效分担查询请求压力,防止雪崩

3. 过期时间优化策略

  • 为缓存有效期增加随机值,防止集中失效
  • 将热点Key有效期设置为永久有效(内存到达限定值时依旧会淘汰)
  • 逻辑过期与异步更新:为每个Key维护一个逻辑过期时间,当逻辑过期时间小于当前时间时,说明当前缓存逻辑已经失效,需要进行更新。如Redis中设置Key的过期时间为60min,Value中设置逻辑过期时间为30min。若命中缓存时发现已到达30min逻辑过期时间,则在返回数据的同时,异步更新这个Key的缓存

4. 加锁重建策略

如果以上策略均失效,在高并发场景下大量Key依旧集中过期或热点Key过期,则需要控制通过查询数据库重建缓存的请求量,常用策略是加锁和限流

若缓存为命中,则对Key加锁,只有获得锁的线程才能访问数据库并加载数据重建缓存,最后释放锁。未获得锁的线程直接返回空结果或休眠一段时间后重试

缺点是不能保证系统的吞吐量,有损用户体验

五、缓存穿透

缓存穿透是指查询一条缓存和数据库都不存在的数据,会导致查询这条数据的请求会穿透缓存,直接查询数据库,最后返回空。如果用户发起大量请求去查询这条根本不存在的数据,则会对数据库造成极大压力

1. 缓存空值策略

未不存在的Key缓存一个空值,对于之后的请求会命中缓存中的空值,直接返回空数据,减少对数据库的访问。

缺点:

  • 浪费空间:需要缓存空值
  • 防御能力若:若攻击者构建不重复的请求,该策略就会被攻破,失去防御能力

2. 布隆过滤器

布隆过滤器可以在不查询数据库的情况下准确地判断数据是否一定不存在,从而过滤请求

布隆过滤器由一个固定大小的二进制向量(或位图)和一系列映射函数组成。元素被加入到布隆过滤器中时,该元素经k个哈希函数生成k个哈希值,然后将位图对应的k个点置为1。

如果需要判断某个元素是否在布隆过滤器中,只需对给定元素进行相同的哈希计算得到k个哈希值,如果有任何一个点为0,则说明这个元素一定不在布隆过滤器中

提前将真实存在的数据的Key(如商品Id)添加到布隆过滤器中,用户查询时,若不存在,则直接返回;若存在,继续查询缓存,缓存未命中则进一步查询数据库

实现方案:

  • Guava:适用于单机环境
  • Redis:Redis中的Bitmaps数据结构可以实现布隆过滤器
  • RedisBloom:开源的Redis插件,可提供布隆过滤器、布谷鸟过滤器、TopK等功能

缺陷:

  • 由于哈希碰撞存在误判,无法判断是否一定存在
  • 不支持删除元素,因为布隆过滤器每一个Bit位不是独占的,若直接删除可能会影响其他元素

3. 布谷鸟过滤器

布谷鸟过滤器支持删除元素,但是存在误删的概率,且插入操作的复杂度高,且会随着插入元素的增加而增加

六、缓存热点

当大量用户集中访问热点数据时,压力将汇聚于单一缓存服务器实例,可能导致热点数据所在的服务器过载,缓存服务不可用

1. 前置缓存策略

将热点数据提前缓存在应用服务器的本地内存中,对于数据查询请求,首先查询本地缓存,命中则直接返回

缺陷:

  • 无法防御未知热点,对于难以预见的突发热点无效
  • 数据更新存在延迟。本地缓存可能会存在短时数据不一致

2. 热点散列策略

多副本策略,将热点数据复制多份,分别缓存在多台缓存服务器上,减轻缓存热点导致的单台缓存服务器压力

实践中,散列数量一般与分布式缓存集群的服务器数量相等。

实现方案,如Redis Cluster

相关推荐
Hello.Reader4 小时前
Redis 延迟监控深度指南
数据库·redis·缓存
Hello.Reader9 小时前
Redis 延迟排查与优化全攻略
数据库·redis·缓存
在肯德基吃麻辣烫21 小时前
《Redis》缓存与分布式锁
redis·分布式·缓存
先睡1 天前
Redis的缓存击穿和缓存雪崩
redis·spring·缓存
CodeWithMe2 天前
【Note】《深入理解Linux内核》 Chapter 15 :深入理解 Linux 页缓存
linux·spring·缓存
大春儿的试验田2 天前
高并发收藏功能设计:Redis异步同步与定时补偿机制详解
java·数据库·redis·学习·缓存
likeGhee2 天前
python缓存装饰器实现方案
开发语言·python·缓存
C182981825752 天前
OOM电商系统订单缓存泄漏,这是泄漏还是溢出
java·spring·缓存
西岭千秋雪_2 天前
Redis性能优化
数据库·redis·笔记·学习·缓存·性能优化
en-route2 天前
HTTP 缓存
网络协议·http·缓存