何时该用缓存
- 某些应用消耗大量CPU去计算,例如正则表达式,就可以考虑将结果缓存下来
- 数据库连接池比较繁忙,经常报出连接不够的报警,也该考虑缓存
缓存的选择
单机缓存用guava或者caffeine,分布式用redis.
本地缓存用guava或者caffeine,远程用redis.
数据量不是很大,数据更新频率较低,可以使用本地缓存guava;如果数据量更新频繁,也想使用进程缓存的话,可以将其过期时间设置为较短,或者设置较短的自动刷新时间.
二级缓存方案
利用 Caffeine 做一级缓存,Redis 作为二级缓存,步骤如下:
- 首先去 Caffeine 中查询数据,如果有直接返回。如果没有则进行第 2 步。
- 再去 Redis 中查询,如果查询到了返回数据并在 Caffeine 中填充此数据。如果没有查到则进行第 3 步。
- 最后去 MySQL 中查询,如果查询到了返回数据并在 Redis,Caffeine 中依次填充此数据。
对于 Caffeine 的缓存,如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期缓存,超时设定可以有两种策略:
- 设置成写入后多少时间后过期。
- 设置成写入后多少时间刷新。
对于 Redis 的缓存更新,其他机器立刻可见,但是也必须要设置超时时间,其时间比 Caffeine 的过期长。
缓存更新
一般来说缓存的更新有两种情况:
- 先删除缓存,再更新数据库。
- 先更新数据库,再删除缓存。
1.先删除缓存再更新DB
结论:产生脏数据的概率较大(若出现脏数据,则意味着再不更新的情况下,查询得到的数据均为旧的数据)(这种情况问题可能出在写数据库期间)
比如:两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
2.先更新DB再删除缓存(使用场景多)
结论:产生脏数据的概率较小,但是会出现一致性的问题;若更新操作的时候,同时进行查询操作,若hit,则查询得到的数据是旧的数据。但是不会影响后面的查询。(代价较小)(这种情况问题可能出现在删除缓存期间)
( 一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。)
该设计模式产生脏数据的可能情况:
一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效;然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
该情况出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
缓存挖坑三剑客
缓存穿透
- 定义: 查询的数据在数据库不存在,所以缓存中也不会有,永远都无法命中而会去数据库查询,给数据库增加了无用的请求
- 解决方案: 对于返回为NULL的数据依然缓存一个自定义的值,并且设置较短的超时时间来减少缓存的维护成本.对于抛出异常的返回不进行缓存
- 优化思路: 制定一些规则过滤一些不可能存在的数据,小数据用 BitMap,大数据可以用布隆过滤器。比如你的订单 ID 明显是在一个范围 1-1000,如果不是 1-1000 之内的数据那其实可以直接给过滤掉。
缓存雪崩
- 定义: 是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩
- 解决方案:
- 采用多级缓存,不同级别缓存设置的超时时间不同
- 缓存根据类别来设置过期时间,例如热门数据过期时间长(或永不过期),冷门数据过期时间短
- 优化思路:
缓存击穿
-
定义: 跟缓存雪崩很像,都是由于缓存过期导致大量请求直接访问数据库,不同的是雪崩是一大片同时失效(面),击穿是某个热点数据失效(点).
-
解决方案:
- 加分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的key,在redis中直接使用setNX操作即可,对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库同一时间只能有一个线程来访问.
- 异步加载:对热点数据采取到期自动刷新的策略,而不是到期自动淘汰.
-
优化思路:
缓存的监控
缓存上线之后统计获取缓存的情况(成功,不存在,过期还是失败等),继而可有效对于统计情况来对缓存的参数进行优化
一般避免以上情况发生我们从三个时间段去分析下:
- 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL 被打死。
- 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
一致性哈希
简单哈希可用于数据分区,但机器变动的时候可能会发生数据迁移.而一致性哈希算法可减少因机器变动而带来的数据迁移量)仅会影响相邻的机器.