这涉及两个地方的数据操作,常见方式:
双写模式:在修改数据库数据后,再查一遍数据库,将查到的数据覆盖原有的缓存。
失效模式:在修改数据库数据后,删除缓存数据,等到再查的时候重新放入缓存。
上面两种模式在线程大并发时,都会出现漏洞
例子:双写模式,线程1和线程2同时修改数据库同一条数据,线程1先修改Mysql,但还没来得及修改Redis,线程2进来了,修改Mysql,又修改了Redis,此时线程1才来修改Redis,此时数据就是脏数据。
同理失效模式也会出现类似的问题,假设现在缓存没有数据,但数据库有数据值为1,此时有两个并发线程,线程1将数据库值1修改成2但没有提交,就卡顿一下,Redis没有改,线程2来读数据库值为1,它也卡顿一会,Redis也没有改,这时线程1缓过神提交并修改Redis,数据库的值变成2,缓存也更新成2,好了,现在线程2也要更新缓存,变为1,这时缓存的数据与数据库就不一致了。
解决方案:加分布式读写锁,写时不可以读,读的是时候不可以写,读读共享锁,让操作Mysql与Redis两步操作同步完成,缺点效率会慢。(读写互斥)
缓存的使用原则 (读多写少,实时要求不高);如果数据经常修改操作并且要求数据即时视化,不走缓存,应该直接查数据库。
其它解决方案:
1.基础数据可以使用Mysql的伪从服务器Canal订阅binlog同步修改Redis方式,补充一点Canal还 可以做大数据分析,比如电商里根据浏览记录,推荐兴趣商品;
结论:
1.读多写少的数据可以使用缓存,并加上过期时间,已经能满足大多数场景;
2.放入缓存的数据是实时性不高的,要求实时性高的直接查数据库获取.
SpringCache操作Redis,先走缓存,如果Redis中没有数据,则去Mysql查,并将查到数据放入Redis
一、SpringCache依赖与使用
<!--SpringCache 操作缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
application.yml 配置文件 (使用Redis做缓存)
spring:
redis:
host: 192.168.126.131
port: 6379 #默认是6379
application.properties配置文件 (使用SpringCache操作Redis)
spring.cache.type=redis
spring.cache.redis.time-to-live=360000
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透问题
spring.cache.redis.cache-null-values=true
由于Redis默认使用jdk序列化,为了能看懂和数据通用转成JSON格式,需要自定义反序列化配置
package com.atguigu.gulimall.product.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* User: ldj
* Date: 2022/9/5
* Time: 20:23
* Description: 缓存配置
*/
@EnableCaching
@Configuration
@EnableConfigurationProperties({CacheProperties.class}) //往容器添加Bean
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()));
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;
}
}
配置完后,主启动类加@EnableCaching开启缓存注解,使用注解就能实现缓存的增删改查
#触发缓存保存
@Cacheable(value = {"数据逻辑分区"}, key = "#root.method.getName()",sync = true}
#触发缓存移除
@CacheEvict
#不影响方法执行更新缓存
@CachePut
#组合操作缓存
@Caching(evict = {
@CacheEvict(value = {"数据逻辑分区"}, key = "'key1'"),
@CacheEvict(value = {"数据逻辑分区"}, key = "'key2'")
})
#在类级别共享相同的缓存配置
@CacheConfig
二、SpringCache存在问题(面试题)
-
缓存穿透:空命中,查缓存和数据库都不存在的数据;
解决:允许存空对象+过期时间。SpringCache可以配置spring.cache.redis.cache-null-values=true
-
缓存击穿:热点数据缓存刚好过期,大量并发涌入查数据,压垮数据库;
解决:增大缓存过期时间 或者 双检加锁 [参考链接] SpringCache可以使用注解@Cacheable(value = {"category"}, key = "#root.method.getName()",sync = true)
-
缓存雪崩:大量key同时过期或者失效,多出现原因是断电或自然灾难;
解决:加随机时间(不推荐),大系统机房做好容灾分区,中小系统不考虑这问题