精华内容
下载资源
问答
  • 缓存更新策略

    2020-01-09 20:23:44
    看到有道题,问 常见的缓存更新策略,当时很懵,不知道啥叫缓存更新策略,只知道自己的项目的缓存更新策略,不知道这叫什么模式,也不知道还有什么其他模式,现记录下来。 常见的缓存更新策略有三种: cache aside ...

    看到有道题,问 常见的缓存更新策略,当时很懵,不知道啥叫缓存更新策略,只知道自己的项目的缓存更新策略,不知道这叫什么模式,也不知道还有什么其他模式,现记录下来。
    常见的缓存更新策略有三种:
    cache aside

    一般用的都是这种方式    
    流程:查询时:如果命中缓存,返回缓存结果,如果未命中缓存,则查数据库,同时更新缓存 
         修改时:直接修改数据库,并设置缓存失效
    
    分析:
        1. 这种方式会不会造成数据不一致或者脏数据的问题?
            会,但是可能性很低。
            出现问题的步骤是:一个读请求过来,没有查到缓存,然后去查数据库,读到a=1,但还没有更新缓存,这时另一个写请求过来,更改了要查的内容,a=2,并删除了缓存,然后
            读请求才更新缓存,把缓存内容更新为1,这是导致了数据不一致。但是这种情况一般会比较少,因为读请求一般都要比写请求快很多,不太可能读请求先来,但是还没读完,后来的写请求已经写完了并删除了缓存。(我的想法:如果缓存更新条件比较复杂,需要很长时间的处理才能更新,是不是也有可能出现这种情况呢?)
        2. 为什么更新完删除缓存,而不是更新缓存?
            1. 数据库并发写请求,有可能造成脏数据。请求1和2都是写请求,更新同一个数据。请求1先完成,请求2后完成,这时数据库的结果是请求2的结果。但请求1更新缓存的操作如果晚于请求2,那缓存就会被更新省请求1的执行结果
            2. 在缓存更新比较复杂的情况下,如果更新的缓存不是热点数据,更新的意义就不大,不如放到请求时更新
    

    read/write through

    流程:查询时: 当缓存失效的时候,由缓存服务自己更新缓存。与cache aside相比,缓存失效后,cache aside需要有查询请求的时候才更新,read through是自己更新
         更新时:如果没有命中缓存,则直接更新数据库,如果命中了缓存,则直接更新缓存,再由缓存更新数据库
    
    优点是对于应用来说,可以认为后端只有一个单一的存储。缺点是数据写入速度较慢(同时需要写入缓存和存储)
    

    writebehind caching

    流程: 更新时只更新缓存,根据一定的策略把缓存的数据刷到数据库。
    优点是数据的I/O操作快,缺点是复杂,数据不是强一致性的,数据可能丢失
    
    展开全文
  • 今天简单聊聊缓存的回收策略跟更新策略。由于笔者自身水平有限,如果有不对或者任何建议欢迎批评和指正~ 缓存回收策略 回收策略 1 基于空间: 即设置缓存的【存储空间】,如设置为10MB,当达到存储空间时,按照...

    ​互联网时代的飞速发展,用户的体验度是判断一个软件好坏的重要原因,所以缓存就是必不可少的一个神器。缓存的种类有很多,需要根据不同的应用场景来需要选择不同的cache,比如分布式缓存如redis跟本地缓存如Caffeine。今天简单聊聊缓存的回收策略跟更新策略。由于笔者自身水平有限,如果有不对或者任何建议欢迎批评和指正~

    缓存回收策略

    回收策略

    1 基于空间:  

    即设置缓存的【存储空间】,如设置为10MB,当达到存储空间时,按照一定的策略移除数据。

    2 基于容量: 

    指缓存设置了最大大小,当缓存的条目超过最大大小时,按照一定的策略移除数据。如Caffeine Cache可以通过 maximumSize 参数设置缓存容量,当超出 maximumSize 时,按照算法进行缓存回收。

     public static void maximumSizeTest() {        Cache<String, String> maximumSizeCaffeineCache = Caffeine.newBuilder()                .maximumSize(1)                .build();        maximumSizeCaffeineCache.put("A", "A");        String value1 = maximumSizeCaffeineCache.getIfPresent("A");        System.out.println("key:key1" + " value:" + value1);        maximumSizeCaffeineCache.put("B", "B");        String value1AfterExpired = maximumSizeCaffeineCache.getIfPresent("A");        //输出null        System.out.println("key:key1" + " value:" + value1AfterExpired);        String value2 = maximumSizeCaffeineCache.getIfPresent("B");        //输出B        System.out.println("key:key2" + " value:" + value2);    }

    3 基于时间

    TTL(Time To Live):存活期,即缓存数据从创建开始直到到期的一个时间段(不管在这个时间段内有没有被访问,缓存数据都将过期)。

    TTI(Time To Idle):空闲期,即缓存数据多久没被访问后移除缓存的时间。

    如Caffeine Cache可以通过 expireAfterWrite跟expireAfterAccess参数设置过期时间。

     public static void ttiTest() {        Cache<String, String> ttiCaffeineCache = Caffeine.newBuilder()                .maximumSize(100).expireAfterAccess(1, TimeUnit.SECONDS)                .build();        ttiCaffeineCache.put("A", "A");        //输出A        System.out.println(ttiCaffeineCache.getIfPresent("A"));        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //输出null        System.out.println(ttiCaffeineCache.getIfPresent("A"));    }

    回收算法

    1 FIFO: 先进先出算法,即先放入缓存的先被移除。

    流程:

    优点: 最简单、最公平的一种数据淘汰算法,逻辑简单清晰,易于实现

    缺点: 这种算法逻辑设计所实现的缓存的命中率是比较低的,因为没有任何额外逻辑能够尽可能的保证常用数据不被淘汰掉

    2 LRU

    流程:

    优点: LRU可以有效的对访问比较频繁的数据进行保护,也就是针对热点数据的命中率提高有明显的效果。

    缺点: 对于周期性、偶发性的访问数据,有大概率可能造成缓存污染,也就是置换出去了热点数据,把这些偶发性数据留下了,从而导致LRU的数据命中率急剧下降。

    3 LFU

    流程:

    优点: LFU也可以有效的保护缓存,相对场景来讲,比LRU有更好的缓存命中率。因为是以次数为基准,所以更加准确,自然能有效的保证和提高命中率

    缺点: 因为LFU需要记录数据的访问频率,因此需要额外的空间;当访问模式改变的时候,算法命中率会急剧下降,这也是他最大弊端。

    4 W-TinyLFU

    流程:

    优点: 使用Count-Min Sketch算法存储访问频率,极大的节省空间;定期衰减操作,应对访问模式变化;并且使用window-lru机制能够尽可能避免缓存污染的发生,在过滤器内部会进行筛选处理,避免低频数据置换高频数据。

    缺点:暂未发现

     

    缓存更新策略

    Cache-Aside

    1 什么是Cache Aside Pattern

    旁路缓存方案的经验实践,这个实践又分读实践,写实践。该模式对缓存的关注点主要在于业务代码,即缓存的更新,删除与数据库的操作,以及他们之间的先后顺序在业务代码中实现。

    读操作:

    1. 先读缓存,缓存命中,则直接返回

    2. 缓存未命中,则回源到数据库获取源数据

    3. 将数据重新放入缓存,下次即可从缓存中获取数据

    写操作:

    1. 淘汰缓存,而不是更新缓存

    2. 先操作数据库,再淘汰缓存

    2 Cache Aside 为什么建议淘汰缓存而不是更新缓存

    如下图所示在1和2两个并发写发生时,由于无法保证时序,此时不管先操作缓存还是先操作数据库,都可能出现:

    (1)请求1先操作数据库,请求2后操作数据库

    (2)请求2先set了缓存,请求1后set了缓存

     

    3 Cache Aside 经典缓存处理切面

    缓存切面抽象类

    /** * @Author: yangjie * @Date: 2019-05-21 16:43 * @Description: */@Componentpublic abstract class RedisCacheAbstractAspect {    protected static final String PREFIX = "AOP:";    protected static final String EL_PATTERN = "#\\{([$?\\w\\.]+?)\\}";    protected static final String EL_REPLACE_PATTERN = "[#\\{\\}]";    @Resource    RedisUtil redisUtil;    /**     * 公用获取锁方法,使用setnx对key进行赋值,过期时间seconds     *     * @return true加锁成功,false锁已存在     */    protected boolean getLock(String key, int seconds) {        return redisUtil.setNx(getLockKey(key), "1", seconds);    }    /**     * 释放key对应额锁     *     * @param keys     */    protected void releaseLock(String... keys) {        redisUtil.del(this.getLockKey(keys));    }    /**     * 获取锁的key,如果以冒号结尾,添加“LOCK”,否则末尾添加“:LOCK”     *     * @param key     * @return     */    protected String getLockKey(String key) {        return key.endsWith(RedisConstants.REDIS_SEPARATE) ? key + "LOCK" : key + ":LOCK";    }    protected String[] getLockKey(String... keys) {        if (keys == null || keys.length < 1) {            return new String[0];        } else {            String[] result = new String[keys.length];            for (int i = 0; i < keys.length; i++) {                result[i] = this.getLockKey(keys[i]);            }            return result;        }    }    /**     * 通过ProceedingJoinPoint获取方法的包名     */    protected String getPackageName(ProceedingJoinPoint pjp) {        return pjp.getTarget().getClass().getName();    }    /**     * 根据包名+ 类名 + 方法名 + 参数(多个) 生成Key(String存储使用)     *     * @Param fullKey 值为true时,不使用md5压缩包名     */    protected String getCacheKey(ProceedingJoinPoint pjp, String cacheField, boolean fullKey) {        // 取到方法名        StringBuffer function = new StringBuffer()                .append(pjp.getTarget().getClass().getName())                .append(".")                .append(pjp.getSignature().getName());        // 取到参数字符串        StringBuffer args = new StringBuffer().append(cacheField);        StringBuffer key = new StringBuffer();        if (!fullKey) {            key.append(DigestUtils.md5DigestAsHex(function.toString().getBytes()));            key.append(":").append(args);        } else {            key.append(function).append(":").append(args);        }        return PREFIX + key.toString();    }    /**     * 根据【包名+ 类名 + 方法名 + 参数(多个) 】生成Key(Hash存储使用)     *     * @param pjp     * @param fullKey     * @return     */    protected String getHashCacheKey(ProceedingJoinPoint pjp, boolean fullKey) {        // 取到方法名        StringBuffer function = new StringBuffer()                .append(pjp.getTarget().getClass().getName())                .append(".")                .append(pjp.getSignature().getName());        StringBuffer key = new StringBuffer();        if (!fullKey) {            key.append(DigestUtils.md5DigestAsHex(function.toString().getBytes()));        } else {            key.append(":").append(JSON.toJSONString(pjp.getArgs()));        }        return PREFIX + key.toString();    }    /**     * 判断字符串是否为EL表达式     */    protected boolean isEl(String elStr) {        return !StringUtils.isEmpty(elStr) && elStr.matches(EL_PATTERN);    }    /**     * 根据EL表达式从obj中取值     *     * @param obj    从该实体中取值     * @param name   #{obj.ooo.ooo}     * @param isRoot 当前是否为顶节点,为true时可以使用“#{param}”取非封装的obj值     * @return     * @throws InvocationTargetException     * @throws IllegalAccessException     */    protected static Object getterMethod(Object obj, String name, boolean isRoot) throws InvocationTargetException, IllegalAccessException {        String thisField;        String nextField = null;        int pointIndex = name.indexOf(".");        // 如果是顶层,切掉第一节 #{obj.oooo.oooo}        if (isRoot && pointIndex < 0) {            return obj;        } else {            name = name.substring(pointIndex + 1);            pointIndex = name.indexOf(".");        }        if (pointIndex <= 0) {            thisField = name;        } else {            // thisField = xxx            thisField = name.substring(0, pointIndex);            // nextField = ooo.ooo            nextField = name.substring(pointIndex + 1);        }        Class clazz = obj.getClass();        Object returnValue = null;        String getMethodName = "get" + thisField.substring(0, 1).toUpperCase() + thisField.substring(1);        Method[] methods = clazz.getDeclaredMethods();        for (Method method : methods) {            if (getMethodName.equals(method.getName())) {                returnValue = method.invoke(obj, null);            }        }        if (StringUtils.isEmpty(nextField)) {            return returnValue;        } else {            return getterMethod(returnValue, nextField, false);        }    }}

    读切面

    /** * @Author: yangjie * @Date: 2019-10-12 15:18 * @Description: */@Slf4j@Aspect@Component@Order(2)public class RedisCacheAspect extends RedisCacheAbstractAspect {    /**     * 配置redis 切面环绕方法     *     * @param point     * @param redisCache     * @return     * @throws Throwable     */    @Around("@annotation(redisCache)")    public Object doAround(ProceedingJoinPoint point, RedisCache redisCache)            throws Throwable {        获得切面当中方法签名        MethodSignature methodSignature = (MethodSignature) point.getSignature();        Method method = methodSignature.getMethod();        // 所有参数的值        Object[] params = point.getArgs();        // 所有参数的名字        String[] paramNames = methodSignature.getParameterNames();        // 缓存个性化字段        String cacheField = redisCache.field();        String cacheKey = redisCache.key();        // 缓存个性化字段的EL值        String fieldName = cacheField.replaceAll(EL_REPLACE_PATTERN, "");        List<String> keyElList = new ArrayList<>();        Pattern p = Pattern.compile(EL_PATTERN);        Matcher m = p.matcher(redisCache.key());        while (m.find()) {            keyElList.add(m.group());        }        // 循环所有的参数名,为EL动态取值的属性赋值        for (int i = 0; i < paramNames.length; i++) {            String name = paramNames[i];            // 替换field里的el表达式的值            if (isEl(cacheField) && name.equals(fieldName.split("\\.")[0])) {                cacheField = String.valueOf(getterMethod(params[i], fieldName, true));            } else {                cacheField = redisCache.field();            }            // 替换key里的el表达式的值            for (String keyEl : keyElList) {                String elStr = keyEl.replaceAll(EL_REPLACE_PATTERN, "");                if (!name.equals(elStr.split("\\.")[0])) {                    continue;                }                cacheKey = cacheKey.replace(keyEl, getterMethod(params[i], elStr, true) + "");            }        }        // 根据需要的类型进行处理        try {            if (RedisDataType.STRING == redisCache.redisDataType()) {                // Redis数据结构是String类型的逻辑                return handleString(point, method, redisCache, cacheField, cacheKey);            } else if (RedisDataType.HASH == redisCache.redisDataType()) {                // Redis数据结构是Hash类型的逻辑                return handleHash(point, method, redisCache, cacheField, cacheKey);            }        } catch (Exception e) {            log.error("【redis aop handler error】", e);            return point.proceed();        }        return point.proceed();    }    /**     * handler string     *     * @param point     * @param method     * @param redisCache     * @param cacheField     * @param cacheKey     * @return     * @throws Throwable     */    private Object handleString(ProceedingJoinPoint point, Method method, RedisCache redisCache,                                String cacheField, String cacheKey) throws Throwable {        Object result;        String key = StringUtils.isEmpty(cacheKey)                ? getCacheKey(point, cacheField, redisCache.fullKey())                : cacheKey + (StringUtils.isEmpty(cacheField) ? "" : RedisConstants.REDIS_SEPARATE + cacheField);        if (getLock(key, redisCache.refreshTime())) {            // 抢到锁更新缓存数据            result = point.proceed();            if (result == null) {                return result;            }            // 放入缓存            String data = encodeObject(result);            log.info(MessageFormat.format("【redis key miss,key->{0},data->{1}】 ", key, data));            // -1 不过期            if (redisCache.expire() == -1) {                redisUtil.set(key, data);            } else if (redisCache.expire() >= 0) {                SetParams setParams = SetParams.setParams().ex(redisCache.expire());                redisUtil.set(key, data, setParams);            }            return result;        } else {            return decodeObject(redisUtil.get(key), method, redisCache);        }    }    private Object handleHash(ProceedingJoinPoint point, Method method, RedisCache redisCache,                              String cacheField, String cacheKey) throws Throwable {        Object result;        String key = StringUtils.isEmpty(cacheKey)                ? getHashCacheKey(point, redisCache.fullKey())                : cacheKey;        String field = cacheField;        String lockTargetKey = key + RedisConstants.REDIS_SEPARATE + field;        if (getLock(lockTargetKey, redisCache.refreshTime())) {            // 抢到锁更新缓存数据            // 后端查询数据            result = point.proceed();            if (result == null) {                return result;            }            // 将list作为Hash存储            String data = encodeObject(result);            redisUtil.hset(key, field, data);            // -1 不过期            if (redisCache.expire() != -1) {                redisUtil.expire(key, redisCache.expire());            }            return result;        } else {            // 加锁失败后            String value = redisUtil.hget(key, field);            return decodeObject(value, method, redisCache);        }    }    private String encodeObject(Object result) {        return JSON.toJSONString(result);    }    private Object decodeObject(String result, Method method, RedisCache redisCache) {        if (result == null) {            return null;        }        if (!(result instanceof String)) {            return result;        } else if (redisCache.clazz() == String.class) {            return result;        }        return decodeObject(result, redisCache.clazz(), method.getGenericReturnType());    }    private Object decodeObject(String result, Class clazz, Type type) {        if (String.valueOf(result).startsWith("[")) {            // "[" 开头为列表,使用JSONArray反序列化            return JSONArray.parseArray(result, clazz);        } else {            // 不以"[" 开头为实体,使用JSONObject反序列化            if (clazz != null) {                return JSONObject.parseObject(result, clazz);            } else {                return JSONObject.parseObject(result, type);            }        }    }}

    更新缓存切面

    @Aspect@Component@Slf4j@Order(1)public class RedisCacheRefreshAspect extends RedisCacheAbstractAspect {    @Around("@annotation(redisCacheRefresh)")    public Object doAround(ProceedingJoinPoint point, RedisCacheRefresh redisCacheRefresh)            throws Throwable {        Object result = null;        try {            result = point.proceed();            // 获取签名和方法            MethodSignature methodSignature = (MethodSignature) point.getSignature();            // 所有参数的值            Object[] params = point.getArgs();            // 所有参数的名字            String[] paramNames = methodSignature.getParameterNames();            // 缓存个性化字段            String[] cacheFields = redisCacheRefresh.fields();            String[] cacheKeys = redisCacheRefresh.keys();            if (redisCacheRefresh.keys().length != cacheFields.length) {                log.error("【切面缓存】清除锁keys与fields长度不一致");                return result;            }            // 循环所有的keys,为EL动态取值的属性赋值            for (int j = 0; j < cacheFields.length; j++) {                String cacheField = cacheFields[j];                String cacheKey = cacheKeys[j];                // 缓存个性化字段的EL值                String fieldName = cacheField.replaceAll(EL_REPLACE_PATTERN, "");                for (int i = 0; i < paramNames.length; i++) {                    String name = paramNames[i];                    if (name.equals(fieldName.split("\\.")[0])) {                        if (super.isEl(cacheField)) {                            cacheFields[j] = getterMethod(params[i], fieldName, true) + "";                        } else {                            cacheFields[j] = redisCacheRefresh.fields()[j];                        }                        // 从keys里筛选El表达式列表                        List<String> keyElList = new ArrayList<>();                        Pattern p = Pattern.compile(EL_PATTERN);                        Matcher m = p.matcher(cacheKey);                        while (m.find()) {                            keyElList.add(m.group());                        }                        // keys中若配置多个EL全部处理                        for (String keyEl : keyElList) {                            String elStr = keyEl.replaceAll(EL_REPLACE_PATTERN, "");                            if (!name.equals(elStr.split("\\.")[0])) {                                continue;                            }                            cacheKeys[j] = cacheKeys[j].replace(keyEl, getterMethod(params[i], elStr, true) + "");                        }                    }                }            }            // 循环所有的fields值,根据keys-fields对应值拼装需要清除的锁,并清除,            for (int i = 0; i < cacheKeys.length; i++) {                String key = cacheKeys[i];                String clearKey = key + (StringUtils.isEmpty(cacheFields[i]) ? "" : RedisConstants.REDIS_SEPARATE + cacheFields[i]);                super.releaseLock(clearKey);            }        } catch (Exception e) {            log.error("【redis aop handler error】", e);            return result;        }        return result;    }}

    Cache-As-SoR

    1 Read-Through:

    Read-Through 也是在查询的时候更新缓存,跟Cache-Aside的区别就是当缓存失效的时候Cache-Aside 是由业务代码负责把数据加载入缓存而 Read-Through 则用缓存服务自己来加载对业务代码是透明的。比如Caffeine Cache中的 CacheLoader

     

    public static void readThroughTest() {        LoadingCache<String, String> loadingCache = Caffeine.newBuilder().expireAfterWrite(2, TimeUnit.SECONDS).build(new CacheLoader<String, String>() {            @Nullable            @Override            public String load(@NonNull String o) throws Exception {                return getFromMysql(o);            }        });        System.out.println(loadingCache.get("A"));        System.out.println(loadingCache.get("A"));}

    2 Write-Through:

    Write-Through 和 Read-Through类似,只不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己同步更新数据库。

    3 Write-Behind:

    Write-Behind 也叫 Write-Back。就是在更新数据的时候,只更新缓存,不同步更新数据库,而是异步地更新数据库。这种模式实现起来技术比较复杂,一般情况下很少使用。

     

     

     

     

     

     

     

    展开全文
  • redis缓存更新策略先删除缓存,后修改数据库先修改数据库,后删除缓存延迟双删内存队列第三方队列 先删除缓存,后修改数据库 这个方案显然是有问题的,不推荐使用。 两个并发的读写操作: 一个写的操作先进来,把...

    先删除缓存,后修改数据库

    这个方案显然是有问题的,不推荐使用。
    两个并发的读写操作:

    1. 一个写的操作先进来,把缓存删除了;
    2. 在写操作还没有更新数据库的时候,一个读的请求又进来了,发现没有命中缓存,就去数据库把老数据取出来了;
    3. 写操作更新了数据库;
    4. 读操作把老数据放在了缓存中。

    这样,数据库中的数据和缓存中的数据就不一致了

    在这里插入图片描述
    这个方案显然不行,在此场景下能保持数据一致?
    让我们设想下这样的场景:一个写的请求进来,删除缓存,这个时候,Redis服务器突然出问题了,或者网络突然出问题了,导致删除缓存失败,抛出了一个异常,导致程序没有继续执行修改数据库的操作。从数据库、缓存一致性的角度来说,这里很好的保证了数据库、缓存的一致性,两者保存的数据是一样的,尽管保存的都是老数据。

    先修改数据库,后删除缓存

    (推荐使用)
    在没有缓存的情况下,两个并发的读写操作:

    1. 读操作先进来,发现没有缓存,去数据库中读数据,这个时候因为某种原因卡了,没有及时把数据放入缓存;
    2. 写的操作进来了,修改了数据库,删除了缓存;
    3. 读操作恢复,把老数据写进了缓存。

    在这里插入图片描述
    这样就造成了数据库、缓存不一致,不过,这个概率出现的非常低,因为这需要在没有缓存的情况下,有读写的并发操作,在一般情况下,写数据库的操作要比读数据库操作慢得多,在这种情况下,还要保证读操作写缓存晚于写操作删除缓存才会出现这个问题,所以这个问题应该可以忽略不计。

    说了这么多,并没有看到先修改数据库,后删除缓存的致命问题啊,别急,让我们继续设想这样的场景:一个写的操作进来,修改了数据库,但是删除缓存的时候 ,由于Redis服务器出现问题了,或者网络出现问题了,导致删除缓存失败,这样数据库保存的是新数据,但是缓存里面的数据还是老数据,妥妥的数据库、缓存不一致啊。

    延迟双删

    可以看到修改数据库,后删除缓存有两个问题,虽然两个问题都是低概率的,所以第三种方案出现了:延迟双删。
    延迟双删就是先删除缓存,后修改数据库,最后延迟一定时间,再次删除缓存。
    在这里插入图片描述
    这么做就可以在一定程度上缓解上述两个问题,第一次删除缓存相当于检测下缓存服务是否可用,网络是否有问题,第二次延迟一定时间,再次删除缓存,是因为要保证读的请求在写的请求之前完成。
    但是这么做,还是有一定问题,比如第一次删除缓存是成功的,第二次删除缓存才失败,又该怎么办?

    内存队列

    上面三种方式,都有一定的问题:

    • 修改数据库、删除缓存这两个操作耦合在了一起,没有很好的做到单一职责;
    • 如果写操作比较频繁,可能会对Redis造成一定的压力;
    • 如果删除缓存失败,该怎么办?

    为了解决上面三个问题,第四种方式出现了:内存队列删除缓存:写操作只是修改数据库,然后把数据的Id放在内存队列里面,后台会有一个线程消费内存队列里面的数据,删除缓存,如果缓存删除失败,可以重试多次。

    在这里插入图片描述

    这样,就把修改数据库和删除缓存两个操作解耦了,如果删除缓存失败,也可以多次尝试。由于后台有一个线程去消费内存队列去删除缓存,不是直接删除缓存,所以修改数据库和删除缓存之间产生了一定的延迟,这延迟应该可以保证读操作已经执行完毕了。
    但是这么做也有不好的地方:

    • 程序复杂度成倍上升,需要维护线程、队列以及消费者;
    • 如果写操作非常频繁,队列的数据比较多,可能消费会比较慢,修改数据库后,间隔了一定的时间,缓存才被删除。

    第三方队列

    如RabbitMQ,Kafka

    方案一:
    在这里插入图片描述
    流程如下所示:
    (1)更新数据库数据;
    (2)缓存因为种种问题删除失败
    (3)将需要删除的key发送至消息队列
    (4)自己消费消息,获得需要删除的key
    (5)继续重试删除操作,直到成功

    该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
    在这里插入图片描述
    流程如下图所示:
    (1)更新数据库数据
    (2)数据库会将操作信息写入binlog日志当中
    (3)订阅程序提取出所需要的数据以及key
    (4)另起一段非业务代码,获得该信息
    (5)尝试删除缓存操作,发现删除失败
    (6)将这些信息发送至消息队列
    (7)重新从消息队列中获得该数据,重试操作。

    备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制,采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可。

    展开全文
  • 读写锁应用到缓存更新一、读取缓存流程二、模拟查询缓存代码三、常见的缓存更新策略四、读写锁应用到缓存更新策略 一、读取缓存流程 我们知道,Redis是一个Nosql数据库,由于其数据都放在内存中,常常用来做缓存。 ...

    一、读取缓存流程

    我们知道,Redis是一个Nosql数据库,由于其数据都放在内存中,常常用来做缓存。Redis用作缓存,肯定要和数据库打交道。当然Redis的应用场景还有很多,不光只用作缓存。

    在读取缓存方面,都是按照下图的流程来进行业务操作。
    在这里插入图片描述
    但兄弟们有没有想过如果数据库中数据修改了,那么缓存的更新策略是什么样呢?我们现在就来讨论这个问题。首先我们看看模拟查询缓存代码。

    二、模拟查询缓存代码

    定义一个Map集合作为缓存,在实际生产中使用Redis来做缓存,实际上Redis的底层结构也是Map集合。

    class GenericDaoCached extends GenericDao {
        private GenericDao dao = new GenericDao();
        private Map<SqlPair, Object> map = new HashMap<>();     //定义Map集合
       
        @Override
        public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
            // 先从缓存中找,找到直接返回
            SqlPair key = new SqlPair(sql, args);
        	T value = (T)map.get(key);
            if(value!=null){
                return value;
            }
            //缓存中没有,查询数据库,然后将查询结果放到缓存中
            dao.queryOne(beanClass, sql, args);
            map.put(key,value);
            return value;   
        }
        
        @Override
        public int update(String sql,Object... args){
            //更新时,先清缓存
            map.clear();
            //再更新数据库
            return dao.update(sql,args);
        }   
    }
    

    首先分析一下,上述代码可能会出现什么问题?

    • 集合是HashMap,它是线程不安全的,在多线程访问下就有可能丢失数据(当多线程同时put值的时候,若发生hash碰撞,可能多个元素都落在链表的头部,从而造成元素覆盖)

    • 清空缓存和更新数据库这两个操作有可能导致查询的值和数据库中的值不一致

    三、常见的缓存更新策略

    首先,我们思考一下更新时,是先清除缓存还是先更新数据库?

    先清缓存,再更新数据库

    结果:造成查询的值和数据库中的值不一致
    在这里插入图片描述
    先更新数据库,再清除缓存

    结果:造成A线程首次查询和后续查询得到不一致的结果,首次查询得到 x=1,后续查询发现已经清空了缓存,需要去数据库中查得 x=2
    在这里插入图片描述

    四、读写锁应用到缓存更新策略

    class GenericCachedDao<T> {
    	// HashMap 作为缓存非线程安全, 需要保护
    	HashMap<SqlPair, T> map = new HashMap<>();
    	//读写锁
    	ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    	public T queryOne(Class<T> beanClass, String sql, Object... params) {
    		SqlPair key = new SqlPair(sql, params);
    		// 加读锁, 防止其它线程对缓存更改
    		lock.readLock().lock();
    		try {
    			T value = map.get(key);
    			if (value != null) {
    				return value;
    			}
    		} finally {
    			lock.readLock().unlock();
    		}
    		// 加写锁, 防止其它线程对缓存读取和更改
    		lock.writeLock().lock();
    		try {
    			// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据
    			// 为防止重复查询数据库, 再次验证
    			T value = map.get(key);
    			if (value == null) {
    				// 如果没有, 查询数据库
    				value = genericDao.queryOne(beanClass, sql, params);
    				map.put(key, value);
    			}
    			return value;
    		} finally {
    			lock.writeLock().unlock();
    		}
    }
    	public int update(String sql, Object... params) {
    		SqlPair key = new SqlPair(sql, params);
    		// 加写锁, 防止其它线程对缓存读取和更改
    		lock.writeLock().lock();
    		try {
    			int rows = genericDao.update(sql, params);
    			map.clear();
    			return rows;
    		} finally {
    			lock.writeLock().unlock();
    		}
    }
    

    当然,实现缓存更新策略还有很多方法,我们下回再述. . . . . .

    展开全文
  • 一般缓存更新策略

    2016-05-24 11:04:26
    我们一般把缓存更新策略归纳为以下几种 FIFO [First In First Out] 最先进入缓存的数据,在缓存空间不足够的情况下,会被首先清理出去 LFU [Less Frequently Used] 最少使用的缓存会被清理,这要求缓存的元素有 hit...
  • 为提高P2P空间矢量数据索引网络的性能,在已有混合结构P2P空间索引网络的基础上,引入缓存机制,并提出了一种新的面向多图层的空间矢量数据缓存更新策略。该策略针对空间矢量数据多图层特性,综合考虑图层优先级以及...
  • 缓存更新策略被动更新设置key过期的时间,让其自动失效。主动更新更新DB的时候,同时更新缓存。一般业务都是主动更新和被动更新结合使用。先更新DB,后更新缓存对于主动更新来说,存在一个问题:你是先更新缓存,后...
  • 【缓存】缓存更新策略

    千次阅读 2019-06-15 15:43:28
    缓存用于缓解后端db的压力,策略指的是更新缓存以及db的方式。 主要可以分为两个大类: 调用方主动更新缓存以及db: 这种是最最常见也是最最容易想到的方式。即调用端需要同时维护db和缓存的调用,调用端逻辑比较...
  • 目录1、缓存的优势2、缓存更新的Pattern3、缓存的有效性 首先声明,我不一定能讲好这个章节,所以本章节仅供你的参考。缓存是一个微服务设计时必须涉及的领域,然而我自己的微服务涉及的缓存封装比较低级,因此造成...
  • 缓存更新策略初探

    2019-07-23 11:42:26
    Cache Aside策略 这是大家经常用到的一...如果应用要更新某个数据,也是先去更新数据库中的数据,更新完成之后,则通过指令让缓存Cache中的数据失效。 这里为什么不让更新操作在写完数据库之后,紧接着去把缓存...
  • 情况1: 写请求处理期间,并发的读请求读取到旧数据,写完成后缓存更新到最新数据;(最终一致性) 情况2: 读请求处理期间,并发的写请求更新数据,导致缓存最终数据为旧数据;(无法实现最终一致性,实际情况中...
  • Mysql 之 缓存更新策略

    2020-04-22 11:25:30
    缓存更新策略:(主要解决数据不一致问题) (一):先更新数据库,再更新缓存(一般不建议使用) 业务角度,对于读操作很少的,造成性能浪费; 线程安全角度,容易产生数据脏读(线程A更新了数据库,线程B更新了数据库,...
  • Redis缓存更新策略

    2020-10-30 23:26:25
    注意,我们的更新是先更新数据库,成功后,让缓存失效。那么,这种方式是否可以没有文章前面提到过的那个问题呢?我们可以脑补一下。 一个是查询操作,一个是更新操作的并发,首先,没有了删除cache
  • redis缓存更新策略

    2020-03-04 18:47:34
    Redis的缓存设计原则 只应将热数据放到缓存中 所有缓存信息都应设置过期时间 缓存过期时间应当分散以避免集中过期 缓存key应具备可读性 应避免不同业务出现同名缓存key 可对key进行适当的缩写以节省内存空间 ...
  • Redis 缓存更新策略

    2020-03-30 18:19:19
    Redis缓存设计 一、缓存的收益与成本 1.1 收益 加速读写:因为缓存通常都是全内存的(例如Redis、Memcache),而存储层通常读写性能不够强悍(例如MySQL),内存读写的速度远远高于磁盘I/O。通过缓存的使用可以...
  • 文章目录1、缓存的收益与成本1.1、受益1.2、成本1.3、使用场景2、缓存更新策略3、双写一致性(redis和mysql数据同步方案)4、缓存粒度控制5、缓存穿透,缓存击穿,缓存雪崩 1、缓存的收益与成本 1.1、受益 1、加速读写...
  • 常见缓存更新策略

    2017-10-02 00:51:28
    看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,964
精华内容 785
关键字:

缓存更新策略