缓存一致性
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题
-
第一种方案:采用延时双删策略
具体的步骤就是: 1)先删除缓存 2)再写数据库 3)休眠500毫秒 4)再次删除缓存 该方案的弊端 : 结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
-
第二种方案:异步更新缓存(基于订阅binlog的同步机制)
技术整体思路: MySQL binlog增量订阅消费+消息队列+增量数据更新到redis 1)读Redis:热数据基本都在Redis 2)写MySQL:增删改都是操作MySQL 3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
缓存穿透 (DB承受了没有必要的查询流量)
在数据库和redis移除key 布隆过滤器做不到删除这个key(布隆过滤器存在假存在概率)
- 解决方法一:是布隆过滤器。
它是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset)。它的核心是一个很长的二进制向量和一系列的hash函数。使用谷歌的guava实现布隆过滤器。
1)存在误算率,随着存入的元素数量增加,误算率也随着增加
-
一般情况下不能从布隆过滤器删除元素
-
数组长度以及hash函数个数确定过程复杂,布隆过滤器的使用场景?
- 垃圾邮件地址过滤(地址数量很庞大)
- 爬虫URL地址去重
- 解决缓存击穿问题
guava 的布隆过滤器
guava 的布隆过滤器 只能单机使用
引入jar包
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
BloomFilter.create()的四个参数
funnel:数据类型(一般是调用Funnels工具类中的)
expectedInsertions:期望插入的值的个数
fpp 错误率(默认值为0.03)
strategy 哈希算法(楼主也不懂啥意思)
// 创建布隆过滤器对象
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(),
1500,
0.01);
// 判断指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 将元素添加进布隆过滤器
// 放入redis中的key 同步放入到布隆过滤器中
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
当 mightContain() 方法返回 true 时,我们可以 99% 确定该元素在过滤器中,当过滤器返回 false 时,我们可以 100% 确定该元素不存在于过滤器中。
redis提供的布隆过滤器
redis提供的布隆过滤器可以集群使用
Redis 实现布隆过滤器的底层就是通过 bitmap 这种数据结构,
Redisson 是用于在 Java 程序中操作 Redis 的库,利用Redisson 我们可以在程序中轻松地使用 Redis。
下面我们就通过 Redisson 来构造布隆过滤器。
/**
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.14.104:6379");
config.useSingleServer().setPassword("123");
//构造Redisson
RedissonClient redisson = Redisson.create(config);
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("phoneList");
**/
RBloomFilter<SomeObject> bloomFilter = redisson.getBloomFilter("sample");
// initialize Bloom filter with
// expectedInsertions = 55000000
// falseProbability = 0.03
bloomFilter.tryInit(55000000L, 0.03);
//将号码10086插入到布隆过滤器中
bloomFilter.add("10086");
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
//判断下面号码是否在布隆过滤器中
System.out.println(bloomFilter.contains("123456"));//false
System.out.println(bloomFilter.contains("10086"));//true
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));
bloomFilter.count();
- 解决方法二:存储空结果,并设置空结果的时间
注:一般采用方法一+方法二混合使用
缓存雪崩 (缓存设置同一过期时间,引起的DB洪峰)
缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了 导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。
- 方法一:原有的失效时间+随机失效时间
- 对于缓存服务宕机的情况,解决方案要复杂些,分为事前、事中、事后三步走:
- 事前:使用集群缓存,保存缓存服务的高可用;这种方案就是在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。
- 事中:ehcache本地缓存 + Hystrix限流&降级,避免MySQL被打死;使用 ehcache 本地缓存的目的也是考虑在 Redis Cluster 完全不可用的时候,ehcache 本地缓存还能够支撑一阵。使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。然后去调用我们自己开发的降级组件(降级),比如设置的一些默认值呀之类的。以此来保护最后的 MySQL 不会被大量的请求给打死。
- 事后:开启Redis持久化机制,尽快恢复缓存集群;一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
缓存击穿(热点Key,大量并发读请求引起的小雪崩)
在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。
- 方法一:1.使用分布是缓存支持的互斥锁(mutex key),去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存,也就是load DB 只会一个线程处理。
- 方法二:提前"使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。增加了业务代码的侵入过多,以及增加了编码复杂性
- 方法三: “永远不过期”: 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
缓存系统常见的缓存满了和数据丢失问题
需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
参考
缓存穿透,缓存击穿,缓存雪崩解决方案分析
缓存一致性,缓存穿透,缓存击穿,缓存雪崩解决方案分析
redis缓存穿透及解决方案
JAVA实现较完善的布隆过滤器
应对缓存击穿的解决方法
如何解决Redis缓存和MySQL数据一致性的问题