一. 缓存
1. 缓存使用
- 为了系统性能的提升,我们一般会将部分数据放入缓存中,而db承担数据落盘工作
- 哪些数据适合放入缓存?
- 即时性和数据一致性要求不高的
- 访问量大且更新频率低的数据(读多,写少)
data = cache.load(id);//从缓存中加载数据
if(data==null){
data = db.load(id);//从数据库中加载
cache.put(id,data);//保存到缓存中
}
return data;
- 注意: 在开发中,凡是放入缓存中的数据,我们应该要指定过期时间,使其可以在 系统即使没有主动更新数据时,也能自动触发数据加载进缓存的流程,避免业务崩溃导致的数据永远不一致的问题
2.高并发下缓存失效问题
(1).缓存穿透
- 缓存穿透: 指查询一个一定不存在的数据,由于缓存是不命中,将会去查询数据库,但是数据库也无此记录,我们没有将这一次查询的null写入缓存,这将导致整个不存在的数据每次请求都要到存储层查询,失去了缓存的意义,
- 风险: 利用不存在的数据可绕过缓存进行攻击,数据库瞬时压力增大,最终导致崩溃
- 解决: null结果缓存,并接入短暂过期时间,也就是说,就说数据库第一次查到null,也要将这个key写入缓存,value可以用一个占位符或者0
(2). 缓存雪崩
- 缓存雪崩: 指在我们设置缓存时,key采用了相同的过期时间,导致缓存某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
- 解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件,其实就是不是过期时间错开,只要不在某一时刻同时失效即可
(3). 缓存击穿
- 对于一些设置了过期时间的key,如果这些可以可能会在某些时间点被超高并发地访问,是一种非常"热点"的数据
- 如果这个可以在大量请求同时进来前刚好失效,那么所有对这个可以的数据查询都落到DB,称之为缓存击穿
- 解决: 加锁,大量并发只让一个人去查,让其他人等待,查到后释放锁,其他人获得锁,先查缓存,就会有数据,这样就不会到DB
二. 分布式锁
redis实现简单 分布式锁
-
-
加锁时必须加上过期时间,不设置的话,如果在占锁成功后,系统宕机,或者运行异常导致运中断,那么锁永远都不会被释放,并且占锁和设置过期时间也要是一个原子操作 setIfAbsent是原子操作
-
释放锁的时候要进行判断,是不是自己的锁,有可能自己的锁已经过期了,如果不判断的话,可能删别人的锁
-
判断锁 和 删除锁 也要是一个原子操作,所以if判断是不可行的
-
使用lua脚本解锁 原子操作
-
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1.占分布式锁,去redis抢锁 //必须加上过期时间,不设置的话,如果在占锁成功后,系统宕机,或者运行异常导致运中断,那么锁永远都不会被释放,并且占锁和设置过期时间也要是一个原子操作 String token = UUID.randomUUID().toString(); Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", token, 300, TimeUnit.SECONDS);//setIfAbsent key不存在才set,此方法是原子操作 if (lock) { //加锁成功 Map<String, List<Catalog2Vo>> dataFromDb; try { dataFromDb = getDataFromDb(); } finally { //操作完后,要释放锁 //释放锁的时候要进行判断,是不是自己的锁,有可能自己的锁已经过期了,如果不判断的话,可能删别人的锁 //判断锁 和 删除锁 也要是一个原子操作,所以if判断是不可行的 // if (token.equals(stringRedisTemplate.opsForValue().get("lock"))) { // stringRedisTemplate.delete("lock"); // } //使用lua脚本解锁 原子操作 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; //lua脚本 stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList("lock"), token); } return dataFromDb; } else { //加锁失败...重试 //休眠200ms try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock();//自旋锁 } }
redisson分布式锁
参照官网
三. 缓存数据一致性
1. 双写模式
- 更新数据库时,同时更新缓存
- 问题:并发情况下,更新数据库和更新缓存不是原子操作,可能出现脏数据
- 解决:1.加锁,2.业务出现脏数据是否能容忍,能容忍的话,就等到缓存自动过期,数据趋向一致,所以说这个问题是暂时性的脏数据问题
- 读到的最新数据有延迟:最终一致性
2. 失效模式
- 更新数据库时,同时删除缓存
- 问题:和双写模式的问题一样,两个操作不熟原子操作
3. 解决方案
- 无论是双写还是失效模式,都会导致缓存不一致的问题,即多个实例同时更新会出问题,
- 如果是用户维度数据(订单数据,用户数据),这种并发几率小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以使用canal[^canal]订阅binlog的方式
- 缓存数据+过期时间也足够解决大部分业务对应缓存的要求
- 通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓,所以适合使用读写锁(业务不关心脏数据,运行临时脏数据可忽略)
- 总结:
- 我们能放入缓存的数据本就不应该是实时性,一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到最新的数据即可
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点
四. Spring Cache
1. 简介
- spring从3.1开始定义了
org.springframework.cache.Cache
和org..springframework.cache.CacheManage
r接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解来简化开发 - Cache接口为缓存的组件规范定义,包含缓存的各种操作集合,Cache接口下spring提供了各种xxxCache的实现,如 RedisCache,EhCacheCache,ConcurrentMapCache等.
- 每次调用需要缓存功能的方法时,spring会检查指定参数的指定目标方法是否已经被调用过,如果有,就直接从缓存中获取方法调用后的结果,如果没有,就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取
2. 使用
-
配置
spring.cache=redis
使用redis做缓存- 主启动了开启缓存功能**@EnableCachinng**
-
注解
- @Cacheable : 触发将数据保存到缓存的操作
- @CacheEvict : 触发将数据从缓存删除的操作
- @CachePut : 不影响方法执行更新缓存
- @Caching : 组合以上多个操作
- @CacheConfig : 在类级别共享缓存的相同配置
-
使用
- 每一个需要缓存的数据我们都要来指定要放到哪个名字的缓存(缓存分区)
-
默认行为:
- 如果缓存中有需要的数据,将不会调用方法
- 缓存中key默认生成, 缓存的名字::SimpleKey[] (不指定前缀时,使用缓存的名字作为前缀)
- 缓存中value的值,默认使用jdk序列化机制
- 默认TTL:-1
-
自定义:
-
指定缓存使用的key,使用注解的key属性,值是一个SpEL,例如使用方法名作为key:
key = "#root.method.name"
所以如果要指定固定的值,就需要使用key=" ‘key’ "格式 -
指定缓存TTL,配置文件中
spring.cache.redis.time-to-live=3600000
,一个小时,单位毫秒 -
指定数据保存格式
-
@Configuration @EnableCaching @EnableConfigurationProperties(CacheProperties.class)//加载配置 public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//使用json格式保存数据 //将配置文件中的值全部拿过来,不然如果使用了自己的RedisCacheConfiguration,那就读不到配置文件中的配置了 CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if(redisProperties.getTimeToLive()!=null) config = config.entryTtl(redisProperties.getTimeToLive()); if (redisProperties.getKeyPrefix()!=null) config = config.prefixKeysWith(redisProperties.getKeyPrefix()); if (!redisProperties.isCacheNullValues()) config = config.disableCachingNullValues(); if(!redisProperties.isUseKeyPrefix()) config = config.disableKeyPrefix(); return config; } }
-
-