缓存
哪些数据适合放入缓存?
- 即时性,数据一致性要求不高的
- 访问量大,且更新频率不高的数据(读多写少)
整合redis
- 引入data-redis-starter
- 简单配置redis的host信息
- 使用SpringBoot自动配置好的StringRedisTemplate来操作redis
堆外内存溢出异常
原因spring boot 2.0以后 默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信 lettuce的bug导致堆外内存溢出 -Xmx300m 如果netty没有指定堆外内存大小,默认使用-Xmx300m ,可以通过-Dio.netty.maxDirectMemory 进行设置 解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存
- 升级lettuce客户端
- 或切换使用jedis
问题
高并发下缓存失效问题-缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无次记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义 风险 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃 解决 null结果缓存,并加入短暂过期时间
高并发下缓存失效问题-缓存雪崩
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬间压力过重雪崩 解决 在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每个缓存的过期时间的重复率就会降低就很难引发集体失效的事件
高并发下缓存失效问题-缓存击穿
对于一些设置了过期事件的key,如果这些key可能会在某些时间点被超高并发的访问,是一种非常"热点"的数据, 如果这个key在大量强强同时进来前刚好失效,那么对这个key的数据查询都将落到db上,我们称为缓存击穿 解决 加锁 大量并发只让一个人去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不会去DB
锁
本地锁
只要是同一把锁,就能锁住需要这个锁的所有线程,
- synchronized(this): SpringBoot 所有的组件在容器中都是单例的,this这个service也是单例的
本地锁只能锁住当前进程,所以我们需要分布式锁
分布式锁
实现原理: redis setnx(“lock”,1111) 能放进去所有占到坑了,占到坑返回ok就执行业务,占不到坑就自旋等待 一百个并发进来,缓存中没有数据,要去调数据库,多个服务先一起去redis抢坑,抢到的那个服务的那个线程去查数据库,然后给缓存放东西,剩下99个“排队”拿到redis的坑,然后发现缓存又有了,直接返回。 后面的数据再过来,缓存有东西就不会再去找redis占坑 问题: setnx占好坑位了, 但是查数据库的时候出现异常或者程序宕机,没有执行删除锁的逻辑,这就造成了死锁 解决 设置锁的自动过期时间,即使没有删除,也会自动删除。 问题: 【设置过期时间的时候服务器断电了,还是死锁】 解决 所以占坑和设置过期时间,必须是一个原子命令 获取锁同时设置过期时间命令: setnxex(“lock”,111,10s) 问题: 【如果业务时间长,锁自己过期了,然后我们又删除锁,这可能把别人持有的锁删除了】 解决 占锁的时候,值指定为uuid,每个人匹配的是自己的锁才删除
问题: 【删除锁的时候查询到了是自己的锁,准备删的时候,自己锁过期了,别人拿到了锁】 解决 查询和删除这也必须是一个原子操作,不能分为两步 使用 lua 脚本删锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList("lock"), uuid);
总结
保证加锁【占位+过期时间】 和 删除锁【判断+删除】的原子性 更复杂的锁自动续期, 这个可以把锁过期时间设长点
public Map<String, List<Catalog2Vo>> getCatalogJsonRedisLockFromDB() {
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock){
System.out.println(" 获取分布式锁成功!");
Map<String, List<Catalog2Vo>> dataFromDb;
try {
dataFromDb = getCatalogJsonFromDB();
}finally {
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, Long.class), Collections.singletonList("lock"), uuid);
}
return dataFromDb;
}else {
System.out.println("获取分布式锁失败,等待重试");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonRedisLockFromDB();
}
}
如何保证redis和mysql双写一致性
|