一、缓存读取数据
public Provinces detail(String provinceid) {
Provinces provinces = null;
//在redis查询
provinces = (Provinces)redisTemplate.opsForValue().get(provinceid);
if (null != provinces){
// redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);
System.out.println("缓存中得到数据");
return provinces;
}
provinces = super.detail(provinceid);
if (null != provinces){
redisTemplate.opsForValue().set(provinceid,provinces);//set缓存
redisTemplate.expire(provinceid,20000, TimeUnit.MILLISECONDS);//设置过期
}
return provinces;
}
二、缓存一致性
1、缓存该淘汰还是更改
1.1更新缓存存在的问题
- 如果value存储的是json格式的object,更新时需要将数据从redis读取出来,再进行相关字段的设置,有可能还要进行相关计算,再进行set回redis。增加了应用进程和redis的交互次数
- 并发写会存在数据不一致问题
不论是先操作数据库还是先操作缓存,都存在一个问题:分布式不能保证顺序性。
先set缓存,再更新数据库。
1.1.set缓存
2.1、set缓存、更新数据库
1.2、更新数据库
结论:数据库中的数据是旧数据
先更新数据库再操作缓存同理。
2、先操作数据库还是先淘汰缓存
2.1 先操作缓存,再操作数据库
存在的问题、并发读写存在数据不一致问题
- 请求 A 进行写操作(key = 1 value = 2),先删除缓存 key = 1 value = 1
- 请求 B 查询发现缓存不存在
- 请求 B 去数据库查询得到旧值 key = 1 value = 1
- 请求 B 将旧值写入缓存 key = 1 value = 1
- 请求 A 将新值写入数据库 key = 1 value = 2
缓存中数据永远都是脏数据
建议:先删除缓存,再更新数据库,再删缓存(双删,第二次删可异步延时)
2.2 先更新数据库再淘汰缓存
当程序原子性被破坏时,会导致数据不一致。
- 更细数据库成功
- 淘汰缓存失败
导致缓存中的数据时旧数据
如果是先淘汰缓存,再删除数据库,原子性被破坏时,数据都是旧值
建议:先淘汰缓存、再更新数据库
1、采用双删除记录
2、监控数据库从库的binlog,更新缓存
三、缓存击穿、雪崩
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于 并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力 瞬间增大,造成过大压力。
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压 力过大甚至 down 机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩 是不同数据都过期了,很多数据都查不到从而查数据库。
对mysql的操作进行加锁操作
public Provinces detail(String provinceid) {
// 1.从缓存中取数据
Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
if (valueWrapper != null) {
logger.info("缓存中得到数据");
return (Provinces) (valueWrapper.get());
}
//2.加锁排队,阻塞式锁---100个线程走到这里---同一个sql的取同一把锁
doLock(provinceid);//32个省,最多只有32把锁,1000个线程
try{//第二个线程进来了
// 一次只有一个线程
//双重校验,不加也没关系,无非是多刷几次库
valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二个线程,能从缓存里拿到值?
if (valueWrapper != null) {
logger.info("缓存中得到数据");
return (Provinces) (valueWrapper.get());//第二个线程,这里返回
}
Provinces provinces = super.detail(provinceid);
// 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
if (null != provinces){
cm.getCache(CACHE_NAME).put(provinceid, provinces);
}
return provinces;
}catch(Exception e){
return null;
}finally{
//4.解锁
releaseLock(provinceid);
}
}
private void doLock(String lockcode) {//给一个搜索条件,对应一个锁
//provinceid有不同的值,参数多样化
//provinceid相同的,加一个锁,---- 不是同一个key,不能用同一个锁
ReentrantLock newLock = new ReentrantLock();//创建一个锁
Lock oldLock = locks.putIfAbsent(lockcode, newLock);//若已存在,则newLock直接丢弃
if(oldLock == null){
newLock.lock();
}else{
oldLock.lock();
}
}
四、缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为 id 为“-1”的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导 致数据库压力过大。
解决方案:使用布隆过滤器
初始化布隆过滤器
@PostConstruct //对象创建后,自动调用本方法
public void init(){//在bean初始化完成后,实例化bloomFilter,并加载数据
List<Provinces> provinces = this.list();
//当成一个SET----- 占内存,比hashset占得小很多
bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), provinces.size());// 32个
for (Provinces p : provinces) {
bf.put(p.getProvinceid());
}
}
过滤请求
@Cacheable(value = "province")
public Provinces detail(String provinceid) {
//先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库
if(!bf.mightContain(provinceid)){
System.out.println("非法访问--------"+System.currentTimeMillis());
return null;
}
System.out.println("数据库中得到数据--------"+System.currentTimeMillis());
Provinces provinces = super.detail(provinceid);
return provinces;
}