多级缓存在微服务的架构设计中可谓随处可见,多级缓存作为提升系统高并发的常规手段,在各类大中小型的系统设计中都有体现;
下图是一张简单的服务端多级缓存设计示意图,多级缓存的常用解决方案,像ehcache + redis,或caffeine + springcache等,即利用JVM内存缓存 + redis缓存配合;
![springboot 缓存一致性常用解决方案](https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2022%2F0905%2F09538894j00rhq59b000uc000hs00bpg.jpg&thumbnail=660x2147483647&quality=80&type=jpg)
文章插图
一、缓存一致性问题
多级缓存带来的好处是显著的,一定程度上可以应对较高的并发,但随之带来了一个比较大的问题就是缓存一致性问题;
我们知道,JVM缓存属于进程级的缓存,和当前服务实例是绑定的,而redis缓存可以作为分布式缓存,通常JVM缓存的是那些生命周期较短的热点查询数据,即过期时间不会太久,而redis缓存相对来说,过期时间相对长一点,JVM缓存通常作为服务端扛压的第一道屏障,如果设置的过期时间太长,将会对JVM内存的开销非常大,所以一般作为短频使用;
设想这么一个场景,服务A采用多实例部署,这里假设部署了两个节点,首次根据ID查询一个用户信息的对象数据将会同时被JVM缓存,同时也会被redis缓存,下一次过来同样参数的请求时,首先走JVM缓存,查到了直接返回,否则走redis缓存;
上面是一个正常的关于缓存存取的过程,问题是,JVM缓存是同进程绑定的,如果第一个节点的数据发生了变更,比如删除了,对于redis缓存来说,可以做到动态刷缓存的效果,但是redis缓存和本地缓存之间并没有一种强同步的机制确保两者的缓存保持一致;
甚至来说,第一个节点与第二个节点之间,两者是无状态的,当第一个节点上面的数据被删除时,假如此刻并发的查询请求到达第二个节点,JVM缓存查询到必然是上一次缓存的数据;
于是,我们的问题就是,在多级缓存模式下,如何解决缓存一致性的问题呢?
二、一个简单的案例
基于之前的一篇springcache 详细使用和spring boot 二级缓存案例基础上我们进行案例演示和改造;
在案例中,我们提供了几个核心的接口:
- 根据用户ID查询用户,并缓存到redis;
- 根据用户ID查询用户,并缓存到JVM,这里采用caffeine;
- 根据用户ID删除用户;
import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.JsonInclude;import com.fasterxml.jackson.annotation.JsonTypeInfo;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.MApperFeature;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.SerializationFeature;import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;import com.fasterxml.jackson.datatype.jsr310.JAVATimeModule;import com.github.benmanes.caffeine.cache.Caffeine;import org.springframework.cache.CacheManager;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.caffeine.CaffeineCacheManager;import org.springframework.cache.interceptor.KeyGenerator;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.redis.cache.RedisCacheConfiguration;import org.springframework.data.redis.cache.RedisCacheManager;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.RedisSerializationContext;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.util.StringUtils;import java.lang.reflect.Method;import java.time.Duration;import java.util.concurrent.TimeUnit;@Configurationpublic class RedisConfig extends CachingConfigurerSupport {@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(mapper);template.setValueSerializer(jackson2JsonRedisSerializer);//使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.afterPropertiesSet();return template;* 分钟级别* @param connectionFactory* @return@Bean("cacheManagerMinutes")public RedisCacheManager cacheManagerMinutes(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3 * 60L);return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 小时级别* @param connectionFactory* @return@Bean("cacheManagerHour")@Primarypublic RedisCacheManager cacheManagerHour(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3600L);return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 天级别* @param connectionFactory* @return@Bean("cacheManagerDay")public RedisCacheManager cacheManagerDay(RedisConnectionFactory connectionFactory){RedisCacheConfiguration configuration = instanceConfig(3600 * 24L);;return RedisCacheManager.builder(connectionFactory).cacheDefaults(configuration).transactionAware().build();* 正常时间的本地缓存@Bean("caffeineCacheManager")public CacheManager caffeineCacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(50, TimeUnit.SECONDS).initialCapacity(256).maximumSize(10000));return cacheManager;private RedisCacheConfiguration instanceConfig(long ttl){Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);objectMapper.registerModule(new JavaTimeModule());objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);//只针对非空的值进行序列化objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);//将类型序列化到属性的json字符串objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,JsonTypeInfo.As.PROPERTY);jackson2JsonRedisSerializer.setObjectMapper(objectMapper);return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(ttl)).disableCachingNullValues().serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));* 自定义key生成策略* @return@Bean("defaultSpringKeyGenerator")public KeyGenerator defaultSpringKeyGenerator(){return new KeyGenerator() {@Overridepublic Object generate(Object o, Method method, Object... objects) {String key = o.getClass().getSimpleName() + "_"+ method.getName() +"_"+ StringUtils.arrayToDelimitedString(objects,"_");System.out.println("key :" + key);return key;}2、配置文件开启使用 springcache
推荐阅读
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AMD|AMD RX 7000显卡最终规格定了!无限缓存缩水一半
- AMD|100MB缓存天下无敌!AMD锐龙7 5800X3D要第一次大降价
- SpringBoot的六边形架构案例
- AMD|AMD Zen4三级缓存带宽暴涨近60%:最多3倍碾压12代酷睿
- 「SpringBoot」 Java中如何封装Http请求
- 教你怎么清除浏览器缓存 如何删除浏览器缓存
- iPhone正确清理缓存的方法 苹果手机如何删除
- 深入浅出理解分布式一致性Paxos算法
- 如何保证数据库与缓存数据一致性?
- springboot 外部配置文件的引入