预防缓存穿透方案设计

预防缓存穿透方案设计

前言

最近公司有一些公司频繁发生一些重大故障, 加上最近核心域凌晨比较多的一些缓存超时(Caused by: net.spy.memcached.internal.CheckedOperationTimeoutException: Timed out waiting for operation),不得不让人提高警惕。此时间段超时比较多,是因为该时间段是缓存预热高峰期。

排查有3种原因:

  1. memcached服务端支持的并发连接数已满,spymemcache客户端操作超时;
  2. memcache客户端添加获取数据时,主要spymemcache是基于nio异步获取的,所以当获取数据时会把任务添加任务队列等待执行,同时spymemcache也会做数据获取的链接超时验证,如果超过设置的超时时间(默认时间2500ms)就会报异常;
  3. 一次性get的key过多;

目前该超时在总请求数占比相对很小,假如某个时间点,有大部分超时,将导致大部分缓存穿透,对mysql数据库造成巨大压力。下面我们讨论一些关于缓存穿透的一些预防措施及相关设计。

缓存穿透

我们先来了解一下缓存穿透的定义

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题.

业界有两种常用预防缓存穿透的方法

(1)采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
(2)如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,这个根据业务上允许的时间为准。通过这个直接设置的默认值存放到缓存,这样第二次到缓存中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!

针对第一种方法,大体的预防方案设计如下图
在这里插入图片描述

在访问所有资源(cache, storage)之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,例如: 我们的价格服务有1亿个sku, 我们可以对所有sku对应的key做一份布隆过滤器,这样可以过滤掉一些不在本系统定价的一些请求,减少cache\storage资源的压力。

伪代码如下

if (!bloomfilter.mightContain(key)) {
   return null;
}
String value = redis.get(key);
if (value == null) {
   return null;
} else {
   //这里用mutex锁实现单线程回源i)
   value = getFromCacheDb(key);
}
return value;

private String getFromCacheDb(String key) {
   String redis = null;
   String value = redis.get(key);
   if (value == null) {
      value = db.get(key);
      redis.set(key, value, expire_secs);
      redis.del(key_mutex);
   } else{
      sleep(50);
      getFromCacheDb(key);//重试
   }
   return value;
}

一、布隆过滤器是什么?

  1. 又快又小的处理方法
  2. 布隆过滤器(Bloom Filter):是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset)。
  3. 它的核心一个很长的二进制向量和一系列hash函数
  4. 数组长度以及hash函数的个数都是动态确定的
  5. Hash函数:SHA1,SHA256,MD5…
    在这里插入图片描述

二、优势和劣势

优势

  1. 全量存储但是不存储元素本身,在某些对保密要求非常严格的场合有优势;
  2. 空间高效率
  3. 插入/查询时间都是常数O(k),远远超过一般的算法

劣势

  1. 存在误算率(False Positive),随着存入的元素数量增加,误算率随之增加;
  2. 一般情况下不能从布隆过滤器中删除元素;
  3. 数组长度以及hash函数个数确定过程复杂;

参考文章:
https://en.wikipedia.org/wiki/Bloom_filter
http://www.cnblogs.com/haippy/archive/2012/07/13/2590351.html
https://github.com/erikdubbelboer/Redis-Lua-scaling-bloom-filter
https://www.cnblogs.com/atomicbomb/p/8979582.html

### 什么是缓存穿透 缓存穿透指的是客户端发起的查询请求未能在缓存中找到对应的数据,进而这些未命中的请求直接访问底层存储系统(如数据库),如果此类情况频繁发生,则可能导致后端服务负载过高,影响性能乃至稳定性[^1]。 ### 缓存穿透的原因 造成缓存穿透的主要因素在于某些恶意攻击者故意构造不存在于缓存内的键值进行大量读取操作;或是应用程序逻辑缺陷使得部分本应存在于缓存里的数据缺失。另外,对于确实不存在的数据项,每次查询都会绕过缓存直达数据库验证其存在性也会引发此现象[^4]。 ### 解决方案概述 为了应对上述挑战,可采取如下措施: #### 存储空结果并设定短暂有效期 当检测到特定key对应的value为空时,不是立即返回错误而是将其作为一个特殊的“null”记录写入缓存,并赋予较短的有效期限(通常不超过几分钟)。这样做的好处是可以减少短期内重复对该条目执行相同查找所带来的开销。 ```python import redis from datetime import timedelta def get_with_cache(redis_client, key): value = redis_client.get(key) if not value: # 假设db_get是从数据库获取数据的方法 db_value = db_get(key) if db_value is None: # 如果数据库里也没有该数据,则向redis插入一条带有TTL的时间戳为当前时间加上指定秒数(这里假设为300s即5分钟)的NULL字符串表示这条数据确实不存在. redis_client.setex(key, timedelta(seconds=300), 'NULL') return None redis_client.setex(key, timedelta(hours=1), db_value) return db_value elif value.decode('utf-8') == 'NULL': return None return value.decode('utf-8') ``` #### 使用布隆过滤器预筛选 通过引入布隆过滤器来提前判断某个key是否存在,只有确认存在的才会继续尝试从缓存或数据库加载实际内容。这种方法能够有效地拦截掉大部分根本就不可能存在的查询请求,从而减轻服务器负担[^2]。 ### 预防措施总结 除了以上提到的技术手段外,还应该注意合理规划缓存机制的设计原则,比如避免使用容易被猜测出来的简单模式命名缓存Key,以及定期清理不再使用的陈旧缓存项目等做法也有助于降低遭遇缓存穿透的风险[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值