概述
为了提升访问效率,适应高并发访问,在程序中引入了缓存,常见的缓存有本地缓存和分布式缓存,分布式缓存中常使用Redis中间件实现。在高并发下缓存会存在失效问题,常见的缓存失效有以下三种:
缓存穿透
查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:null结果缓存,并加入短暂过期时间
缓存雪崩
在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这
样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
缓存击穿
对于一些设置了过期时间的key,如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到数据库
解决:加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去数据库
引入依赖
核心依赖如下:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--分布式锁redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.5</version>
</dependency>
<!--Spring Cache缓存操作-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置文件
主配置文件核心配置如下:
spring:
cache:
type: redis #缓存类型
redis:
time-to-live: 3600000 #存活时间(毫秒)
#key-prefix: CACHE_ #key前缀,默认使用缓存的名字作为前缀
#use-key-prefix: true #是否使用前缀
cache-null-values: true #是否缓存空值,防止缓存穿透
Redisson的相关属性需要配置在单独的配置文件中,核心配置如下:
singleServerConfig:
idleConnectionTimeout: 10000 #连接空闲超时(毫秒),默认10000
connectTimeout: 10000 #连接空闲超时(毫秒),默认10000
timeout: 3000 #命令等待超时(毫秒),默认3000
retryAttempts: 3 #命令失败重试次数
retryInterval: 1500 #命令重试发送时间间隔(毫秒),默认1500
password: null
subscriptionsPerConnection: 5 #单个连接最大订阅数量,默认5
clientName: null #客户端名称
address: "redis://127.0.0.1:6379"
subscriptionConnectionMinimumIdleSize: 1 #发布和订阅连接的最小空闲连接数,默认1
subscriptionConnectionPoolSize: 50 #发布和订阅连接池大小,默认50
connectionMinimumIdleSize: 24 #最小空闲连接数,默认32
connectionPoolSize: 64 #连接池大小,默认64
database: 0
dnsMonitoringInterval: 5000 #DNS监测时间间隔(毫秒),默认5000
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode": "NIO"
加载Redisson配置文件实例化RedissonClient和配置Redis序列化,核心代码如下:
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
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;
}
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = Config.fromYAML(new ClassPathResource("application-single.yml").getInputStream());
RedissonClient redisson = Redisson.create(config);
return redisson;
}
核心代码
@Cacheable(value = "user", key = "#root.methodName", sync = true)
public AjaxResult selectUserById(@PathVariable Long id) {
String key = "selectUserById";
RLock readLock = cacheService.getReadLock(key);
readLock.lock(20, TimeUnit.SECONDS);
SysUser user = new SysUser();
try {
logger.info("加锁成功,开始执行业务......");
user = userService.getById(id);
logger.info("从数据库中查询用户信息.....");
} catch (Exception e) {
e.printStackTrace();
logger.error("查询用户数据异常", e.getMessage());
} finally {
logger.info("释放锁,业务执行结束......");
readLock.unlock();
}
return AjaxResult.ok().data("user", user);
}
SpringCache常用注解
@Cacheable:触发将数据保存到缓存,在方法上标注,表示当前方法的结果需要缓存,如果缓存中有,方法不用调用,如果缓存中没有,会调用方法,最后将方法的结果放入缓存。
@CacheEvict:触发将数据从缓存中删除
@CachePut:不影响方法执行更新缓存
@Caching:组合以上多个操作
@CacheConfig:在类级别共享缓存的相同配置
使用缓存会存在缓存数据一致性问题,常见的解决办法是双写模式和失效模式,但也不能绝对的解决数据一致性问题。在使用缓存时应该是实时性和一致性要求并不是很高的数据,在缓存中加上过期时间,可以保证一定时间后能拿到最新的数据,对于实时性和一致性要求特高的数据,可以使用canal订阅binlog的方式解决数据一致性问题。
代码详见码云地址