设计规范
缓存设计问题
前提条件
使用缓存的两个前提条件:
一、 数据访问频率不均衡,某些数据会被更频繁的访问,这些数据应该放在缓存中;
二、 数据在某个时间段内有效,不会很快过期,否则缓存的数据就会因已经失效而产生脏读,影响结果的正确性。
一致性问题
无论你的设计做的多么好,缓存数据与后端真实数据一定存在着一定时间窗口的数据不一致性,这个时间窗口的大小可大可小,具体多大还要看一下你的业务允许多大时间窗口的不一致性。一般CRUD业务场景下,将查询的结果进行缓存,当对数据进行修改或删除操作时要删除对应的缓存,这又分方法调用前或调用后删除。
方法调用前删除缓存:比如进程A调用修改方法,调用前删除了缓存,若此时进程B调用读取方法,发现没有缓存,读取数据库,并将结果缓存起来,进程A修改了数据并提交数据库,这样就导致缓存与数据库的数据不一致了,这个不一致的时间为缓存的TTL(Time To Live生存时间值)。
方法调用后删除缓存:比如进程A调用了修改方法修改了数据并提交数据库,在未删除缓存前,进程B此时调用读取方法,将先读取缓存,也会导致读取缓存和数据库的数据不一致,这个不一致的时间为进程A提交数据库到删除缓存的时间,当然也存在这删除缓存失败而导致更长时间(一般为该缓存的TTL)的不一致的风险。
由此可见,我们更推荐采用方法调用后删除缓存的做法,在ZCache高可用的保证下,删除缓存失败的风险更低。具体请参考后面章节里的注解式和编程式缓存操作。
缓存穿透
是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库中查询。如果需要一定程度(由于数据库操作和缓存操作之间有个延迟,在此时间内的并发请求还会访问数据库,请注意)的避免缓存穿透,对此可以对Null值进行适当的缓存,并且强烈建议对相应的cache配置失效值。
例如:对于使用ZCache做缓存的,在定义RedisCacheManager时指定其cacheNullValues为true(其构造函数第三个参数),且使用的RedisTemplate的值序列化方式必须是ZCacheValueRedisSerializer,示例如下:
@Bean
public <K, V> RedisTemplate<K, V> crmRedisTemplate() {
RedisTemplate<K, V> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(crmRedisConnectionFactory());
redisTemplate.setKeySerializer(new ZCacheKeyRedisSerializer());
redisTemplate.setHashKeySerializer(new ZCacheKeyRedisSerializer());
redisTemplate.setHashValueSerializer(new ZCacheValueRedisSerializer());
redisTemplate.setValueSerializer(new ZCacheValueRedisSerializer());
return redisTemplate;
}
@Bean
public CacheManager cacheManager() {
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
map.put("Billing:AcctItemTypeDto:id", 1000L); //配置缓存名及其TTL
RedisCacheManager cacheManager = new RedisCacheManager(crmRedisTemplate(), map.keySet(), true);
cacheManager.setExpires(map);
cacheManager.setUsePrefix(true);
return cacheManager;
}
注意:当支持Null值缓存时,存储到ZCache的Null值实际是一个json串{"@class":"org.springframework.cache.support.NullValue"},ZCacheValueRedisSerializer反序列化时会将它作为null值返回。
缓存Key的命名
ZCache是一个Key/Value内存数据库,为防止多个产品共用一个ZCache而导致Key的名称冲突,Key的命名应当使用合适的命名规则,并且添加前缀来识别数据。前缀应当尽量短小,见名知意,而连接符必须使用冒号,推荐前缀规则如下:产品名:存储对象名(或简称):key对应的字段名(或简称或集合名):,示例:
单个对象,key命名示例:
Billing:AcctItemTypeDto:Id:9000
Billing:AcctItemTypeDto:Code:test
集合对象,key命名示例:
Billing:AcctItemTypeDto:List:all
Billing:AcctItemTypeDto:List:id1-100
Billing:AcctItemTypeDto:List:codeSMS
注意事项:实际代码里的key可能出现单引号(如下),这是因为代码里的key的取值是一个SpEL,对于字符串常量必须使用单引号,否则会报错。对于这个缓存(名为Billing:GstTypeDto:List)而言,它的key是这个SpEL的结果,而对于ZCache,最终存储的key将是字符串Billing:AcctItemTypeDto:List:all。
@Cacheable(cacheNames = "Billing:GstTypeDto:List", key = "'all'")
ZCache的缓存名规范
为了方便对ZCache的缓存名的管理,应该使用静态常量来标识缓存名,统一定义在一个类里,代码中用静态常量来替代字符缓存名。示例如下:
public class CacheRegistry {
public static final String CACHE_MODULE = "Billing";
public static final String CACHE_DELIMITER = ":";
public static final String CACHE_NAME_PREFIX = CACHE_MODULE + CACHE_DELIMITER;
public static final String CACHE_ACCT_ITEM_TYPE_DTO_BY_ID = "Billing:AcctItemTypeDto:id";
public static final String CACHE_ACCT_ITEM_TYPE_DTO_BY_CODE = "Billing:AcctItemTypeDto:code";
public static final String CACHE_GST_TYPE_DTO_LIST = "Billing:GstTypeDto:List";
public static final long DEFAULT_CACHE_TTL = 30 * 60L; //默认缓存失效时间,单位秒
public static final String CACHE_FOR_TEST = "Test:put";
public static final String CACHE_BUF_PROTO_BY_ID = "ftf:TestBuf:id";
public static final String CACHE_BUF_PROTO_LIST = "ftf:TestBuf:List";
public static String resolveKey(String cacheName, String literalKey) {
return cacheName + CACHE_DELIMITER + literalKey;
}
}
使用示例:
@Cacheable(cacheNames = CacheRegistry.CACHE_ACCT_ITEM_TYPE_DTO_BY_ID, key = "#acctItemTypeDto.acctItemTypeId",
condition = "#acctItemTypeDto.acctItemTypeId ne null")
Cache cache = cacheManager.getCache(CacheRegistry.CACHE_ACCT_ITEM_TYPE_DTO_BY_ID);
@Test
public void testHasKey() {
String key = CacheRegistry.resolveKey(CacheRegistry.CACHE_ACCT_ITEM_TYPE_DTO_BY_CODE, "Test");
Boolean hasKey = redisTemplate.hasKey(key);
Assert.assertTrue(hasKey);
}
缓存的单元测试
本地开发时如果需要绕过缓存,可以考虑使用一个dev环境的Profile,实现一个DevZCacheConfiguration来定制的CacheManger,示例如下:
@Profile("dev")
@Configuration
@PropertySource(value = {"classpath:config/zcache.properties"}) //注入zcache配置
@EnableCaching //用于开启Spring Cache支持
public class DevZCacheConfiguration {
@Bean
public CacheManager cacheManager() {
CacheManager cacheManager = new NoOpCacheManager();
return cacheManager;
}
}