Redis 缓存穿透

缓存相信好多项目大家都用到过,并发下缓存的问题也是比较多的,前几天有个朋友还问我知道什么是缓存穿透吗?我一脸茫然回答道:不知道。然后正好有幸看到某个学堂,讲解到了这个东西。特此来记录下。
一、 什么是缓存穿透
关于缓存衍生了好多问题,这里暂且不一一介绍了,老实说我也不是很清楚,但是接下来肯定会慢慢地进行了解记录学习。
至于缓存是什么,这个这次就先不介绍了,朋友们自行百度学习吧,网上教程挺多的。关键变成自己的才是最重要的,希望以后大家都是大牛。好了言归正传。
什么是缓存穿透呢?
大家都从网上买过东西,我们买好了以后由物流进行发货,物流商会提供一个快递单号给我进行查询,我们根据快递单号就可以把物流信息查询出来,那么我们后台怎么来做呢,一般来说都是放到数据库里面的,但是为了提高效率,避免平凡跟我们数据库进行IO操作,我们放到缓存中去,直接从缓存中取得。所以流程是这样:先从缓存中看看有没有,没有再去数据库中查询,查询出来返回,这是我们有数据的情况。那如果我们没有数据的时候,我们是不是每次都会去数据库,但是返回null的数据,但是这个查询是不是也要耗时呢?如果大批量的查询呢?那是不是就会影响效率呢。这样引出缓存穿透的问题。
缓存穿透:就是指查询一个不存在的数据,这样的请求到数据库查询,失去了缓存的意义。再流量大时,可能DB就挂掉了。
****二、怎么解决这个问题呢
我们可以再访问缓存之前,加上一张网,来过滤不在我们数据库中的数据。也就是说:怎么判断当前单号是否在上百万数据中或者千万级别的数据中呢。我们使用一个布隆过滤器来解决这个问题。
引入jia包

	<dependency>
   		<groupId>com.google.guava</groupId>
   		<artifactId>guava</artifactId>
   		<version>19.0</version>
	</dependency>

注意:版本太低可能没有那个类

来看下这个布隆过滤器,底层是算法,有兴趣的朋友自行研究下吧。大致讲下这个类。
它里面有两个重要的属性:随机函数方法和二进制向量
一个快递单号过来以后,通过随机函数方法,我们假设随机函数方法有:f1,f2,f3,首先通过f1算法,然后在二进制向量中存储了某个地方,然后经过f2算法,再二进制向量中,存储了某个地方,再经过f3算法,把这个快递单号的信息存在了二进制向量中。这是存,来看下判断是否有这个值。一个快递单号经过f1.f2.f3看看二进制数组里面返回的是不是1,0代表没存。如果都返回1说明有这个号,我们从缓存取,当然需要提前把数据缓存到缓存中去。当然这个东西有优点也就有缺点,缺点是存在误判。所有没有就去数据库取一下。
代码

public class BloomFilterDemo {
	
	private static final int insertions = 1000000;//100W
	
	@Test
	public void bfTest() {
		//初始化一个存储String数组的布隆过滤器,初始化大小100W
		BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions);
		//初始化一个存储String数组的set,初始化大小100W
		Set<String> set = new HashSet<String>(insertions);
		//初始化一个存储String数组的list,初始化大小100W
		List<String> list = new ArrayList<String>(insertions);
		//向三个初始化容器放入100W的随机并且唯一的数据
		for(int i=0;i<insertions;i++) {
			String uuid = UUID.randomUUID().toString();
			bf.put(uuid);
			set.add(uuid);
			list.add(uuid);
		}
		//布隆过滤器错误判断次数
		int wrong = 0;
		//布隆过滤器正确判断次数
		int right = 0;
		//大街上找10000个人
		for(int i=0;i<10000;i++) {
			String test = i % 100 ==0?list.get(i/100):UUID.randomUUID().toString();//按照一定比例选择bf中存在的值
			if(bf.mightContain(test)) {
				if(set.contains(test)) {
					right ++;
				}else {
					wrong ++;
				}
			}
		}
		System.out.println("==========rigth========="+right);
		System.out.println("==========wrong========="+wrong);
	}

}

结果:==========rigth=========100 ==========wrong=========289
为啥有差不多300误差呢,因为我们的bloomFilter默认是3%。那我们可以控制这个计算率,现在是千分之一进行计算,来看下结果

BloomFilter<String> bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), insertions,0.001);

==========rigth=========100
==========wrong=========18

至于这个算法大家可以看下底层怎么写。

内容概要:该研究通过在黑龙江省某示范村进行24小时实地测试,比较了燃煤炉具与自动/手动进料生物质炉具的污染物排放特征。结果显示,生物质炉具相比燃煤炉具显著降低了PM2.5、CO和SO2的排放(自动进料分别降低41.2%、54.3%、40.0%;手动进料降低35.3%、22.1%、20.0%),但NOx排放未降低甚至有所增加。研究还发现,经济性和便利性是影响生物质炉具推广的重要因素。该研究不仅提供了实际排放数据支持,还通过Python代码详细复现了排放特征比较、减排效果计算和结果可视化,进一步探讨了燃料性质、动态排放特征、碳平衡计算以及政策建议。 适合人群:从事环境科学研究的学者、政府环保部门工作人员、能源政策制定者、关注农村能源转型的社会人士。 使用场景及目标:①评估生物质炉具在农村地区的推广潜力;②为政策制定者提供科学依据,优化补贴政策;③帮助研究人员深入了解生物质炉具的排放特征和技术改进方向;④为企业研发更高效的生物质炉具提供参考。 其他说明:该研究通过大量数据分析和模拟,揭示了生物质炉具在实际应用中的优点和挑战,特别是NOx排放增加的问题。研究还提出了多项具体的技术改进方向和政策建议,如优化进料方式、提高热效率、建设本地颗粒厂等,为生物质炉具的广泛推广提供了可行路径。此外,研究还开发了一个智能政策建议生成系统,可以根据不同地区的特征定制化生成政策建议,为农村能源转型提供了有力支持。
### Redis缓存穿透的解决方案及其原理 #### 1. 使用布隆过滤器(Bloom Filter) 布隆过滤器是一种空间效率高的概率型数据结构,用于测试某个元素是否属于集合的一部分。其核心思想是在请求到达缓存之前,通过布隆过滤器快速判断该键是否存在。如果布隆过滤器返回“不存在”,则可以直接拒绝访问数据库,从而避免缓存穿透的发生。 布隆过滤器的工作原理基于多个哈希函数和位数组。对于每一个加入集合的元素,它会被多个哈希函数映射到位数组中的不同位置,并将这些位置置为1。查询时,只需检查对应的位是否全为1即可得出结论[^5]。然而需要注意的是,布隆过滤器可能会产生误判,即认为某些不存在的元素存在,但这并不会直接影响系统的安全性,因为最终仍然可以通过数据库验证真伪。 ```java // 布隆过滤器初始化示例 import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class BloomFilterExample { private static final int EXPECTED_INSERTIONS = 1000000; // 预期插入数量 private static final double FPP = 0.01; // 可接受的错误率 public static void main(String[] args) { BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), EXPECTED_INSERTIONS, FPP); String keyToCheck = "nonexistentKey"; if (!bloomFilter.mightContain(keyToCheck)) { System.out.println("Key does not exist in database."); } else { // Proceed to check the actual database. } } } ``` #### 2. 缓存空对象 另一种简单的解决方法是对那些确实不存在的数据,在第一次查询后将其设置为空值并存储到缓存中。这样后续对该数据的任何请求都可以直接从缓存获取而无需再次查询数据库。不过这种方法也有缺点:如果恶意用户不断尝试不同的不存在键,则会造成大量无意义的缓存条目占用内存资源[^1]。 为了缓解这一问题,可以设定较短的有效时间来定期清理这些空对象记录。 ```lua -- Lua脚本实现缓存空对象逻辑 if redis.call('GET', KEYS[1]) == nil then -- 如果缓存中没有找到对应key,则向DB发起查询... local valueFromDb = fetchValueFromDatabase(KEYS[1]) if valueFromDb ~= nil then redis.call('SETEX', KEYS[1], ARGV[2], valueFromDb) else -- 对于null/nil的结果也进行缓存处理 redis.call('SETEX', KEYS[1], '0', ARGV[1]) end return valueFromDb or '' end return redis.call('GET', KEYS[1]) ``` #### 3. 加锁机制(Mutex Locking) 加锁策略适用于高并发环境下的单次加载情况。当发现某项数据不在缓存中时,程序不会立刻去读取数据库,而是先尝试获得一个分布式锁。只有成功拿到锁的那个线程才会继续执行数据库查询并将结果更新至缓存;其他未能取得锁的任务则需等待一段时间后再重新尝试获取缓存内容[^4]。 此方式能够有效减少重复计算以及不必要的IO开销,但在极端条件下仍可能出现短暂的服务延迟现象。 --- ### 总结 以上介绍了三种主流应对Redis缓存穿透的技术手段——分别是利用高效的空间节省算法构建前置屏障、合理管理零值反馈周期以及引入同步控制单元防止竞争条件引发额外负载等问题。具体选用哪种方案取决于应用场景的具体需求和技术栈现状等因素综合考量决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值