设计规范

缓存设计问题

前提条件

使用缓存的两个前提条件:

一、 数据访问频率不均衡,某些数据会被更频繁的访问,这些数据应该放在缓存中;

二、 数据在某个时间段内有效,不会很快过期,否则缓存的数据就会因已经失效而产生脏读,影响结果的正确性。

一致性问题

无论你的设计做的多么好,缓存数据与后端真实数据一定存在着一定时间窗口的数据不一致性,这个时间窗口的大小可大可小,具体多大还要看一下你的业务允许多大时间窗口的不一致性。一般CRUD业务场景下,将查询的结果进行缓存,当对数据进行修改或删除操作时要删除对应的缓存,这又分方法调用前或调用后删除。

  1. 方法调用前删除缓存:比如进程A调用修改方法,调用前删除了缓存,若此时进程B调用读取方法,发现没有缓存,读取数据库,并将结果缓存起来,进程A修改了数据并提交数据库,这样就导致缓存与数据库的数据不一致了,这个不一致的时间为缓存的TTL(Time To Live生存时间值)。

  2. 方法调用后删除缓存:比如进程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;
    }
}

results matching ""

    No results matching ""