1、为何要引入缓存?
防止大量请求直接访问数据库,导致数据库压力过大,从而崩溃
访问数据库耗时太久,直接访问缓存可以加快访问速度
2、缓存
本地缓存----直接使用map将数据库查到的缓存放到map中,请求过来后先去map中查key有的化返回
缺点:对于分布式系统来说,map只能存在单台服务其中,当网关路由到其他服务器时,其他服务器还需要在查一遍数据库并放入缓存
分布式缓存---引入其他中间件做缓存管理,将所有服务器的缓存都放入在中间件中---redis
redist整合:引入starter 配置post和host 使用stringredistemplate操作
//依赖yml spring: redis: port: 6379 host: 192.168.111.149 stringRedisTemplate测试 @Autowired StringRedisTemplate stringRedisTemplate; public void testRedis(){ ValueOperations org.springframework.boot spring-boot-starter-data-redisio.lettuce lettuce-coreops = stringRedisTemplate.opsForValue(); ops.set("hello","w"+ UUID.randomUUID().toString()); }
可能会遇到的问题:
**内存泄漏及解决办法** 当进行压力测试时后期后出现堆外内存溢出 OutOfDirectMemoryError 产生原因: 1)、springboot2.0 以后默认使用 lettuce 操作 redis 的客户端,它使用通信 2)、lettuce 的 bug 导致 netty 堆外内存溢出 解决方案:由于是 lettuce 的 bug 造成,不能直接使用-Dio.netty.maxDirectMemory 去调大虚拟机堆外内存 1)、升级 lettuce 客户端。 2)、切换使用 jedis
3、高并发下的缓存问题
**缓存穿透** 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记 录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储 层去查询,失去了缓存的意义 风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃 解决: null 结果缓存,并加入短暂过期时间
**缓存雪崩** 缓存雪崩是指在我们设置缓存时 key 采用了相同的过期时间,导致缓存在某一时刻同时失效, 请求全部转发到 DB,DB 瞬时 压力过重雪崩。 解决: 原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重 复率就会降低,就很难引发集体失效的事件。
**缓存击穿** * 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是 一种非常“热点”的数据。 * 如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。 解决: 加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存, 就会有数据,不用去 db
4、缓存加锁解决缓存击穿问题
(1)本地缓存
直接使用lock或synchronized (this)【因为springboot每个服务器只有一个实例,所以只有一把锁】将查询 db 的方法加锁
缺点:只能锁住单个服务器,不能锁定全部服务器,导致每个服务器都要去查一遍db
(2)redis缓存
使用setnx进行上锁,谁抢到赋值权谁就拿到锁,服务结束后在释放锁
问题:如何保证释放的是该线程的锁
//上锁时,v设定一个uuid 和一个过期时间,释放锁时查看两者是否相等
String s = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", s,300,TimeUnit.SECONDS);
//保证上锁和解锁的原子行,防止出现突然停机等问题导致锁无法释放,造成死锁现象
//要使用脚本删锁
String script = "if redis.call(‘get’,KEYS[1]) == ARGV[1] then return redis.call(‘del’,KEYS[1])" +
"else return 0 end";
stringRedisTemplate.execute(new DefaultRedisScript(script,Long.class),Arrays.asList("lock"),s);
问题:
1、setnx 占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就
造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除
2、setnx 设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis 支持使用 setnx ex 命令
3、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为 uuid,每个人匹配是自己的锁才删除。
4、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那
么我们删除的是别人的锁
解决:
删除锁必须保证原子性。使用 redis+Lua 脚本完成
不足:无法解决自动续期(业务时间超过过期时间导致锁被提前释放)
(3)加锁和释放锁的时序问题
1、加锁 2、执行业务逻辑 3、更新缓存 4、释放锁
5、整合redisson解决自动续期
@Configuration
public class MyRedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.111.149:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
redisson测试及说明
@GetMapping("hello")
@ResponseBody
public String hello() {
//1、只要同一个名字就是统一把锁
//如果指定了到期时间,则即使业务没有执行完,看门狗也不会自动续期,而直接释放锁
//原理:如果我们传入时间,redisson直接执行lua脚本,进行占锁,超时释放
//如果未指定,默认传入-1,就是用30s(lockwatchTimeOut看门狗默认时间),只要占锁成功,就会开启一个定时任务(重新给锁设置过期时间30s){定时时长为10s}
RLock lock = redissonClient.getLock("lock");
//2、加锁
lock.lock();//阻塞等待
try {
System.out.println("加锁成功"+Thread.currentThread().getId());}
finally {
//解锁
lock.unlock();
System.out.println("解锁"+Thread.currentThread().getId());
}
return "hello";
}
6、springcache的整合



