高并发下缓存失效问题
1、缓存穿透
查询一个一定不存在的数据,由于缓存不会命中,将去数据库查询,但是数据库也无此记录,我们没有将这次查询的Null写入缓存,就导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义
风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:null结果缓存,并加入短暂过期时间
2、缓存雪崩
缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到db,DB瞬时压力过大崩溃
解决:原有的失效时间基础上增加一个随机值,这样每一个缓存的过期时间的重复率就会降低,很难引起机体失效的事件
3、缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某个时间点被超高并发访问,是一种非常热点的数据。如果这个key在大量请求同时进来之前刚好失效,那么所有对这个key的数据查询都会去查数据库,称之为缓存击穿
解决:大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查询缓存,就会有数据,不会去查db
读写锁
保证一定能读到最新数据,修改期间,写锁时一个排他锁,读锁时一个共享锁
写锁未释放,读锁就必须等待
读+读:相当于无锁,并发读,指挥在redis中记录所有当前的读锁,他们都会同时加锁成功
写+读:读需要等待写锁释放
写+写:阻塞
读+写:有读锁,写锁必须等待读锁释放
只要有写的存在,就必须等待
缓存数据一致性---双写模式
双写模式:写数据库的同时更新缓存,会有并发问题,解决办法:lock--写数据库--写缓存--unlock
失效模式:1、写数据库的同时删除缓存,等业务来拉取时自己更新数据
2、给缓存中的数据设置有效期,有效期一到会自动去数据库更新数据
缓存一致性解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题,即多个实例同时更新,怎么办?
1、如果是用户维度数据(订单数据、用户数据),并发几率非常小,可以无视,缓存数据加过期时间,每隔一段时间出发读的主动更新即可
2、如果是菜单、商品介绍等基础数据,也可以使用canal订阅binlog的方式
3、缓存数据+过期时间足够解决大部分业务对于缓存的要求
4、通过加锁保证并发读写,写写的时候按顺序排队,读读无所谓,所以适合使用读写锁(业务不关心脏数据,允许临时脏数据可忽略)
总结:
1、我们能放入缓存的数据本就不应该是实时性、一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到当前的最新数据即可
2、不应该过度设计,增加系统的复杂性、
3、遇到实时性、一致性要求高的数据,就应该差数据库,即使慢点
整合SpringCache简化缓存开发
1、引入依赖 spring-boot-starter-cache, spring-boot-starter-data-redis
2、写配置
1、自动配置了哪些
CacheAutoConfiguration会导入RedisCacheConfiguration
自动配置好了缓存管理器RedisCacheManager
2、配置使用redis作为缓存 spring.cache.type=redis
3、测试使用缓存
@Cacheable: Triggers cache population. 触发将数据保存到缓存的操作
@CacheEvict: Triggers cache eviction. 出发将数据从缓存中移除的操作
@CachePut: Updates the cache without interfering with the method execution. 不影响方法执行更
@Caching: Regroups multiple cache operations to be applied on a method. 组合以上多个操作
@CacheConfig: Shares some common cache-related settings at class-level. 在类级别共享缓存的相
1、开启缓存功能 @EnableCaching
2、只需要使用注解就能完成缓存操作
4、原理
CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager ->初始化所有的缓存配置
-> 每个缓存决定使用什么配置 ->如果redisCacheConfigureation有就用自己的,没有就用默认配置 -> 想改缓存配置,只需给容器中放一个RedisCacheConfiguraction即可 -> 就会应用到当前RedisCacheManager管理的所有缓存分区中
//Cacheable放在查询方法上,key为缓存的key
@Cacheable(value = {"category"}, key = "#root.method.name")
//CacheEvict放在修改、删除的方法上
//key为删除的缓存Key,但此写法只能删除单条缓存
@CacheEvict(value = "category", key = "#root.method.name")
//删除多条缓存的写法(allEntries=true 删除value下的所有缓存)
1、@CacheEvict(value = "category", allEntries = true)
2、@Caching(evict = {
@CacheEvict(value = "category", key = "'listTree'"),
@CacheEvict(value = "category", key = "'list'")
/**
* 级联更新所有关联的数据
* @CacheEvict 失效模式
* 1、同时进行多种缓存操作 @Caching
2、指定删除某个分区下的所有数据 @CacheEvict(value="category", allEntities=true)
3、存储同一类型的数据,都可以指定成同一个分区,缓存前缀不在配置文件中指定,使用默认的分区名作为前缀
*/
Spring Cache的不足
1、读模式:
缓存击穿: 解决方案,加锁, Cacheable加属性sync=true(加本地锁,get方法上加synchronized,并非分布式锁)
缓存穿透: 解决方案:缓存空数据 cache-null-values=true
缓存雪崩: 加随机时间,加过期时间 spring.cache.redis.time-to-live=3600000
2、写模式
1、读写锁
2、引入Canal,感知到Mysql的更新去更新缓存
3、读多写少,直接去查询数据库
总结:常规数据(读多写少、及时性、一致性要求不高的数据)的使用,完全可以使用springcache,写模式(只要缓存的数据有过期时间就够了)
特殊数据,特殊设计