使用缓存的思路:先查询缓存,若不存在则查询数据库,之后把数据放在缓存一份。
一、缓存穿透
使用不存在的key进行大量高并发查询,导致缓存无法命中,每次请求都穿透到数据库查询,数据库压力过大(缓存中没有数据,数据库中也没数据)
例如:请求id为1的数据,id为1的数据在数据库中根本就不存在,总是会请求数据库。好似缓存不存在一样(没有起到保护数据库作用)。
解决方案:
- 接口增加校验拦截非法请求,比如拦截id<0的数据。
- 将空值缓存起来
- 使用布隆过滤器(redis4.0之后有布隆过滤器插件可用,还可以基于redis位图实现布隆过滤器)
二、缓存击穿
缓存中没有数据,但数据库中有数据(一般是缓存时间到期),但是由于并发用户特别多,同时读缓存没读到,而去查询数据库,引起数据库瞬间压力过大。
解决方案:
- 使用互斥锁,一般jvm锁。如果是多部署则使用分布式锁
- 热点数据永不过期
三、缓存雪崩
缓存服务器重启,或是大量key在集中在某个时间段失效,引起导致数据库压力巨大甚至宕机。
解决办法:
- 对不同的数据使用不同的失效时间,相同的数据过期时间也增加一个随机数(错开失效时间如:60+(int)(Math.random()*3600))
- 热点数据永不过期
- 搭建高可用集群,保证高可用。
四、缓存缓存与数据库一致性
数据修改的三种操作:
(1)先更新数据库,再更新缓存(最差劲方式)
如果修改数据库成功,修改缓存失败则很容易出现数据不一致问题
(2)先更新缓存,再更新数据库
也有问题:
当多线程时,由于线程A比线程B先更新数据库,最终数据库的结果是线程B的值
但是由于网络原因线程B比线程A先更新缓存,最终缓存的结果是线程A的值
数据库是B,缓存是A,出现数据不一致问题
(3)先删除缓存,再更新数据库(推荐)
也有问题:
当多线程时,线程A删除了缓存,想要更新数据库时,线程B发现缓存为空,去查询数据库,并把结果加入到缓存中(旧值)
而线程A更新了数据库(新值)。好似缓存根本就没有被删除掉一样。缓存和数据库还是不一致
解决办法:
1、延时双删
线程A在更新数据库结束之后,再删除一遍缓存,但是要延时。目前大多数企业使用此方法,好维护效率也不低。
不过这种方法能解决大概率问题,并不能做到100%。
2、串行化
解决的主要是上面的线程A更新数据库、线程B查询数据库并加入缓存这两步的顺序,顺序确定了“先删除缓存,再更新数据库”并发问题就解决了。可以使用队列来控制
当线程A删除了缓存,想要更新数据库时,把更新数据库这步操作加入队列。线程B发现缓存为空,去查询数据库,把查询数据库这步操作加入队列。
这就保证了从队列中取出是先更新数据库,再查询数据库。
使用队列方式:
- 如果是单机,则可以使用java队列
-
如果是分布式,则可以使用mq或redis队列