美文网首页
缓存牺牲一致性

缓存牺牲一致性

作者: 朱万宇 | 来源:发表于2018-03-04 22:46 被阅读0次

    缓存-牺牲一致性

    在企业中, 为了性能的提升, 常常会使用缓存技术, 在业界中已经有很多非常优质的缓存框架,如Memcached 分布式缓存, ehcache 进程内缓存(也有分布式缓存), redis 作为缓存服务器. hazelcast 作为分布式缓存, guava进程内缓存。

    研究了一系列的缓存架构, 最后要集成到系统中, 在代码中, 我们不希望缓存增加代码的耦合性, 也就是类似下列伪代码:

        a = getFromCache(key) 从缓存中获取
        if(a!=null) return a;
        a = doSomething(params);
        save2cache(key, a, time expired); // 将a存入缓存
        return a;
    

    以上代码大大增加了代码的耦合性, 并且一旦缓存架构换了, 所有与之相关的代码可能都需要改变。使得维护的成本大大增加。那么如何能做到缓存的抽象呢?

    我们希望, 只用一个注解, 告诉这个方法要缓存了或者要删除缓存了,然后存储或更新或删除的逻辑会进行抽象。 并且当缓存出现问题了, 并不会影响源方法的执行。同时我们想要更换缓存的存储介质不需要修改原来的代码。

    spring cache

    基于这点, 我们发现, spring cache 为我们提供了这层的抽象, 我们仅仅在方法上加@Cacheable就可以将返回值进行缓存, 被标有@CacheEvict 就会执行根据key进行缓存删除。 并且我们想要换一个缓存框架比如从ehcache -> redis, 只需编写相应的CacheManager, 将存储和删除的逻辑实现即可。

    对于spring cache 我在这里做个简单的介绍:

    
        最主要的就是 @Cacheable 注解了
    
    以下是spring4.1 以上的版本,对于4.1以下的版本, 不支持cacheManager, cacheResolver. 
    public @interface Cacheable {
        String[] value() default {};   // 类似于数据库级别
        String key() default "";       // key, 可以不指定
        String keyGenerator() default "";       // 生成key的策略
        String cacheManager() default "";      //
        String cacheResolver() default "";
        String condition() default "";
        String unless() default "";
    }
    
    spring 还有一个强大的地方就是支持spEl, 这样使得key的生成是动态的,可定制的。
    比如
    @Data
    class M{
        String name;
    }
    
    @Cacheable(key="#m.name")
    public M method(M m){
        
    }
    
    

    spring cache 非常简单, 但是缓存和抽象存在很多隐患, 想要集成到系统中, 需要进行仔细的设计。

    首先我们做以下分析:

    1. 什么数据是需要缓存的?
    2. 缓存的失效策略 --- 缓存存多久
    3. 缓存会存在什么问题

    springCache 的 不足

    1. 我们无法通过注解设置缓存的生命周期。

    springCache 的强大

    其强大

    多种应用场景分析

    1. 牺牲缓存清除(过期自动清除)

    有时候, 我们对某个表或实体基本不会执行更新操作,只会执行新增操作, 同时, 我们是根据id 查询到该实体类, 那么我认为, 我们可以使用keyGenerator 来对特定的方法自动生成一个特定的key。 一般还需要与unlessunless= '#result ne null'连用, 如果返回的结果为空, 我们不会进行缓存。

    使用keyGenerator,我们不必担心缓存key的生成, 只要写一套就可以了。下面是示例

    public class CommonKeyGenerator implements KeyGenerator {
        public static final String CACHE_HEADER = "cache";
        
        /**
         * the prefix of the key
         * @return
         */
        protected String prefix(){
            return CACHE_HEADER;
        }
    
        @Override
        public Object generate(Object target, Method method, Object... params) {
            String key = prefix() + connectSymbol()+ key(target, method, params);
            return key;
        }
    
        /**
         * default :
         * @return
         */
        protected String connectSymbol(){
            return ":";
        }
    
        public String key(Object target, Method method, Object... params){
            String key =  new BaseCacheKey(target, method, params).toString();
            return key;
        }
    }
    // 通过方法名,参数列表, 类名生成一个唯一的key
    public class BaseCacheKey implements Serializable {
    
        private static final long serialVersionUID = -1651889717223143579L;
    
        private static final Logger logger = LoggerFactory.getLogger(BaseCacheKey.class);
    
        private final Object[] params;
        private final int hashCode;
        private final String className;
        private final String methodName;
    
        public BaseCacheKey(Object target, Method method, Object[] elements){
            this.className=target.getClass().getName();
            this.methodName=getMethodName(method);
            this.params = new Object[elements.length];
            System.arraycopy(elements, 0, this.params, 0, elements.length);
            this.hashCode=generatorHashCode();
        }
    
        private String getMethodName(Method method){
            StringBuilder builder = new StringBuilder(method.getName());
            Class<?>[] types = method.getParameterTypes();
            if(types.length!=0){
                builder.append("(");
                for(Class<?> type:types){
                    String name = type.getName();
                    builder.append(name+",");
                }
                builder.append(")");
            }
            return builder.toString();
        }
    
        @Override
        public boolean equals(Object obj){
            if(this==obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            BaseCacheKey o=(BaseCacheKey) obj;
            if(this.hashCode!=o.hashCode())
                return false;
            if(!Optional.of(o.className).or("").equals(this.className))
                return false;
            if(!Optional.of(o.methodName).or("").equals(this.methodName))
                return false;
            if (!Arrays.equals(params, o.params))
                return false;
            return true;
        }
    
        @Override
        public final int hashCode() {
            return hashCode;
        }
    
        private int generatorHashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + hashCode;
            result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());
            result = prime * result + Arrays.deepHashCode(params);
            result = prime * result + ((className == null) ? 0 : className.hashCode());
            return result;
        }
    
        @Override
        public String toString() {
            logger.debug(Arrays.toString(params));
            logger.debug(Arrays.deepToString(params));
            return "BaseCacheKey [params=" + Arrays.deepToString(params) + ", className=" + className + ", methodName="
                    + methodName + "]";
        }
    
    }
    以上实现仅供参考。
    

    注意一点,方法被代理的话className不要作为key的元素之一,因为名称中会有其他@xxx等信息。在分布式中同一份数据会被存多份。

    1. 要想在执行增删改时清空或put新的缓存值

    我们知道缓存可以说是一个有过期策略的ConcurrentHashMap
    要想更新缓存或删除对应的缓存, key 必须要匹配, 那么我们想要的自动生成的keyGenerator 似乎已经达不到要求了, 因为即使再怎么实现KeyGenerator, 貌似无法实现两个方法key能够统一。

    这时候我们可能就要进行匹配key的设计
    示例如下

    @Cacheable(key = "#keyManager.getModuleKey('role',#roleId)", unless = "#result ne null")
    public Role getRoleById(String roleId) throws Exception {
        return daoImpl.findRollerById(roleId);
    }
    @CachePut(key = "#keyManager.getModuleKey('role',#role.roleId)")
    public Role updateRoleById(Role role) throws Exception {
        role.setRoleName("xxx");
        return role;
    }
    @CacheEvict(key = "#keyManager.getModuleKey('role',#role.roleId)")
    public void deleteRoleById(String roleId) throws Exception {
        daoImpl.deleteRoleId(roleId);
    }
    
    @Component
    public class KeyManager {
        /**
        * module 模块名称, args 多个参数, 基本类型, 非数组
        **/
        public String getModuleKey(String module, Object... args){
            return module +":" + Joiner.on(":").join(args);
        }
    }
    使用方法的好处就是一个地方改了, 跟着其他的key也会变动。
    以上是我的简单实现, 大家可以根据不同的业务写不同的方法, 我建议最好放在一起集中管理。
    

    当然以上并不是解决所有对于增删改差的问题, 因为假如说, 某个方法内是关联查询,返回的对象包含另外一个实体类, 那么像这种更新并只会更新局部。 所以要非常小心使用。

    以上最好是针对单个实体的增删改。

    学会面向缓存设计

    有时候, 我们查询某个查询的业务比较复杂, 其中包含多次数据库查询, 我可能并不建议直接在该方法上执行缓存, 而是对于里面的小方法进行缓存。

    有时候我们可以将一个查询进行拆分, 将变化的部分单独查询。 比如说某个实体类有个状态字段, 会经常变化, 而其他信息基本上不会变化。
    固定的部分可能内容很多, 并且过程较多, 而易变的东西可能很容易获取。

    比如如下

    public Result getTarget(...){
        Result res = findFixedValue(...);  // findFixedValue 方法进行缓存
        Integer a = findVariateValue(...); // findVaiable  不进行缓存。
        result.setA(a);
        retrun res;
    }
    

    对于spring cache 并没有提供对于过期策略, 而是根据缓存框架的支持。比如ehcache可以通过xml配置
    <cache name = ""
    timeToIdleSeconds="86400"
    timeToLiveSeconds="86400"
    />,

    guava 缓存提供多种过期策略。
    redis 通过设置过期时间。

    当然在oschina和github中还有很多开源的框架, 还是要根据自己的业务需求选择合适的框架, 比如考虑分布式, 考虑缓存策略等方面。

    缓存作用于service层还是dao层

    之前,我一直认为缓存应该放到service中, 今天仔细考虑了这个问题, 发现各有利弊

    1. 放在service接口上
    • 问题:
      service 层主要是业务的逻辑,
      在service中可能会调用其他service层,service层也可能直接调用dao层, service层其实是相对复杂的, 所以要考虑, 该缓存的内容适不适合缓存, 比如被调用的service的返回发生了改变, 这时候就会存在不一致性。
    • 好处:
      在远程rpc调用的情况下, 由于调用的是service, 如果该service加了缓存, 并且是分布式缓存, 那么在调用方可以避免远程调用, 从而从缓存中获取。
    1. 放在dao层
      dao层往往直接是数据的获取, 往往是获取的是某个model, 我更认为在dao层缓存是本地缓存。
    • 好处
      由于dao层往往不会涉及业务代码, 其往往被多个service调用, 而service 的公用性相对dao层更少,
      dao层相对service层对key的生成更容易控制。

    采用缓存在很大程度上会丧失一致性, 开过期策略所能容忍的范围内采用缓存, 不要一味追求性能而滥用缓存, 否则必然会带来惨重的代价。

    相关文章

      网友评论

          本文标题:缓存牺牲一致性

          本文链接:https://www.haomeiwen.com/subject/metcfftx.html