缓存牺牲一致性

缓存-牺牲一致性

在企业中, 为了性能的提升, 常常会使用缓存技术, 在业界中已经有很多非常优质的缓存框架,如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的生成更容易控制。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,654评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,807评论 6 342
  • 灯亮了 开始散场 那首《remember me》 却还在耳畔回响...... 梦想 人生再艰难, 我还有我的吉他,...
    童晓晓的简书阅读 435评论 0 0
  • 感恩海仙姐姐给我介绍yjwd,让我体验到分享的快乐 感恩z录制第13章内容,让我们没有7阶课件的小伙伴也可以听到 ...
    骞卉阅读 194评论 0 0
  • 我就是走一走,看一看,仅仅吃了一个晚饭而已 如若涉及任何不能公开之处,请联系作者,立马删除。 文如其名,写的就是在...
    待嫁猪格格阅读 562评论 0 0