首先我们要知道什么数据适合放入缓存
1、及时性,数据 一致性要求不高的:
例如存入数据,不需要马上访问的。
2、访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等
具体流程
伪代码参考如下:
data = cache.load(id);//从缓存加载数据
if(data == null){
data = db.load(id);//从数据库中加载
cache.put(id,data);//保存到cache
}
return data;
Java中最简单的缓存方式Map
在类上声明一个Map(本地缓存)
本地缓存
缺点:
在分布式架构下
1、当被负载均衡到其他服务器上时,要重新录入缓存
2、数据被修改时,不仅要在db上改而且还要再其他cache上修改,这样如果是被负载到1号
服务器上,修改器cache的时候那么与其他的服务器上的本地缓存数据会不一致
所以下面介绍分布式缓存数据的流程
优势:
1、当内存不足时可以做集群工作。
2、还可以进行分片操作,使容量提升。
Redis的使用:
1、引入datra-redis-starter依赖
2、简单配置redis的host信息
3、使用spring自带的StringRedisTemplate
ValueOperations<String,String> ops = stringRedisTemplate.opsForValue();
//保存
ops.set("hello","world");
//查询
String hello = ops.get("hello");
System.out.println(hello);
整合逻辑代码:
//1。缓存逻辑
//JSON跨语言
String catelogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
//2.缓存中没有,查数据库
Map<String, List<Catelog2VO>> catelogJsonFromDb = getCatelogFromDb(0
//3.查到的我数据再放入缓存,要将对象转为json
String s = JSON.toJSONString(catelogJsonFromDb);
redisTemplate.opsForValue().set("catelogJSON",s);
return catelogJsonFromDb;
}
//收到数据后要转为指定对象。
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catelogJSON,new TypeRerence<Map<String,List<Caterlog2VO>);
return result;
高并发下缓存失效问题:
1、缓存穿透
原因:查询一个一定不存在的数据,由于缓存不命中,所以去查询数据库,但是数据库也没有记录,所以就导致了每次查询到要去储存层查询,失去了缓存的意义。
风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,导致最终崩溃。
解决:null结果缓存,并加入短期过期事件
2、缓存雪崩
原因:我们数值缓存时key采用了相同的国企事件,导致缓存存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方法:原有的失效时间的基础上增加一个随机值,重复率降低,就难以引发集体失效的事件。
redisTemplate.opsForValue().set("catelogJSON",1,TimeUnit.DAYS);
3、缓存击穿
原因:对于一些设置了过期事件的key,如果这些key可能会在某些时间超高并发访问,导致频繁查询这个key最终查询都落到db
解决方法:加锁 只让一个人去查,其他人等待,查到后释放锁,
锁:
本地锁:优点:快,缺点:只能锁住当前进程。
synchronized,JUC(Lock)只能锁住this
分布式锁:优点:锁住所有进程,缺点:慢。
原理:所有的服务去同一个地方占坑,如果占到就执行逻辑。否则等待
阶段一:
使用SET NX(如果非空,就设置字段),如果保存失败,就是占锁失败。
分布式锁使用方法(保证原子性):
String uuid = UUID.randomUUID().toString();
//设置字段的同时设置过期时间
redisTemplate.opsForValue().setIfAbsent("lock","111",300,TimeUnit.SECONDS);
if(lock){
try{
//执行逻辑
}finally{
//删除锁
//唯一标识相同的的时候才能删锁(也要是原子操作)Lua脚本删锁
String script = "if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end";
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>
(script,Integer.class),Arrays.asList("lock",uuid);
}
}else{
//休眠100ms
//枷锁失败,重试,自旋的方式
}
这段代码可以防止其在各个字段出现故障:
1、try finally保证其无论如何都会执行删锁代码,
2、设置过期时间防止执行逻辑过程中崩溃,断电等导致没有及时删锁导致的死锁问题。
3、uuid保证不会在执行代码的的同时锁的时间到而导致其他进程进锁同时占用,删除的时候删到 了后面进来的进程的锁。(保证它只删除自己的锁)
4、Lua脚本:要么全成功要么全失败,保证原子性。
整合工具
Redisson
依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.39.0</version>
</dependency>
简单配置:
要写连接协议名:redis://