布隆过滤器
特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
缓存血崩
新发布的系统,缓存池是空的,在早上10点高峰期的时候,大量的人员进行访问,系统开始初次建立每个人的缓存信息,大量的请求查询不到缓存,直接透过缓存池投向数据库,造成瞬时DB请求量井喷。这是典型的缓存雪崩了。
同时因为,失效时间相近(8小时失效),所以也有潜在的缓存雪崩。
应急处理方案:适当处理缓存的机制,采用布隆过滤器、空初始值、随机缓存失效时间方式来预防缓存击穿和缓存雪崩的产生。
最终解决方案:改回原来缓存全公司员工信息的方式,根据执行计划和SlowLog,优化获取员工信息的SQL脚本,去掉不需要的字段和无意义的连接。
缓存雪崩是指大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。
上面的哪个问题,初次访问的数据都是未建立缓存的,跟同时失效的情况一样,当峰值期到来的时候,会大量的请求查询不到缓存,直接透过缓存池投向数据库,造成瞬时DB请求量井喷。
解决方案分析
2.2.1 缓存集群+数据库集群
在系统容量设计的时候,应该能够预见后期会有大量的请求,所以在发生雪崩前对缓存集群实现高可用,如果是使用 Redis,可以使用 主从+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。
同样的,也需要对数据库进行高可用保障,因为透过缓存之后,真正考验的是数据库的抗压能力。所以 1主N从 甚至 数据库集群 是我们需要重点去考虑的。
2.2.2 适当的限流、降级
可以使用 Hystrix进行限流 + 降级 ,比如像上面那种情况,一下子来了1W个请求,不是当前系统的吞吐能力能够承受的,假设单秒TPS的能力只能是 5000个,那么剩余的 5000 请求就可以走限流逻辑。
可以设置一些默认值,然后调用我们自己降级逻辑去FallBack,保护最后的 MySQL 不会被大量的请求挂起。
2.2.3 随机过期时间
可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。
随机值我们团队的做法是:n 3/4 + n random() 。所以,比如你原本计划对一个缓存建立的过期时间为8小时,那就是6小时 + 0~2小时的随机值。
2.2.4 缓存预热
类似上面的那个案例,并不是还没过期,而是新功能发布,压根还没建设过缓存,所以可以在峰值期之前先做好部分缓存,避免瞬时压力太大。
所以如果10点是峰值期,那么可以预先在8~10点期间,可以逐渐的把大部分缓存建立起来。
缓存穿透
缓存穿透是指访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量井喷时会导致DB挂掉。
比如 我们查询用户的信息,程序会根据用户的编号去缓存中检索,如果找不到,再到数据库中搜索。如果你给了一个不存在的编号:XXXXXXXX,那么每次都比对不到,就透过缓存进入数据库。
这样风险很大,如果因为某些原因导致大量不存在的编号被查询,甚至被恶意伪造编号进行攻击,那将是灾难。
3.2.1 缓存空值
发生穿透的原因是缓存中没有存储这些空数据的key,或者压根这个数据的key是不会存在的,从而导致每次查询都进入数据库中。
我们就可以将这些key的值设置为null,并写到缓存池中。后面再出现查询这个key 的请求的时候,直接返回null,这样就在缓存池中就被判断返回了,压力在缓存层中,不会转移到数据库上。
3.2.2 BloomFilter
使用布隆过滤器将不存在的值直接返回Null
缓存击穿
一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。(注意跟上面两种的区别)
4.2.1 锁的方式
分布式锁场景,在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。
这种现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
锁不好的地方就是在其他线程在拿不到锁的时候就等待,这个会造成系统整体吞吐量降低,用户体验度也不好。
4.2.2 空初始值
这是一种短暂降级的方式:
如果一个缓存失效的时候,有无数个请求狂奔而来,而第一个请求从进入缓存池,判空,再到数据库检索,再查询出结果并返回设置缓存的这个过程里,缓存是不存在的。
这个就很危险,超高并发下这个短暂的过程足已让千千万万请求投向数据库。更别提这可能是个慢查询,整个过程可能长达2s以上,那对数据库是一种非常大的伤害。
业内有一种做法叫做空初始值,短暂的局部降级来保证整个数据库系统不被击穿。大概流程如下: