缓存一致性

本文探讨了缓存系统中常见的问题,如缓存穿透、并发和失效问题,并提出了解决方案,包括通过设置随机过期时间来避免集体失效,以及采用CacheAsidePattern模式确保数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

一般应用而言,追求的都是缓存的最终一致性。

缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

缓存并发

缓存失效问题

引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。

那如何解决这些问题呢?
其中的一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

几种方案

下面的几种方案中,都要设置缓存过期时间。以避免一旦出现脏数据后产生无法保证最终一致性。

先删除缓存,然后再更新数据库

明显是错误的。两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

但是这种方法是在是简单到让人喜欢,所以,有人将其利用队列完全改造了成了串形模式,当出现更新请求时,先扔进队列,然后异步依次执行即可。

Cache Aside Pattern

先把数据存到数据库中,成功后,再让缓存失效。
这是最常用的套路。

一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。但是实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

在这里插入图片描述
我们可以再让缓存失效后立刻模拟一次调用来刷新缓存。

更高水平的缓存一致性

由于缓存长期不一致会引起客户投诉。所以我们可以采取更高水平的缓存保证。

  • 比如在修改不多的场景下,我们可以小时级别轮训上一个小时的修改内容,查询是否有不一致的情况。但是这种情况下需要引入数据版本的概念【缓存版本小于数据库版本即认为缓存实际上已经过期】

  • 如果有数据版本的概念,其实就先改数据库,再改缓存。只要保证改缓存的时候版本一定要大于等于现在的版本就可以了

发布时的缓存手动失效

  • 一般而言,发布时需要让全部缓存失效。如果修改了下面的【业务逻辑2】的部分,需要让缓存全部失效。所以,需要在业务逻辑1中加入版本校验。比如业务逻辑2向缓存放入V1版本的数据,此时修改业务逻辑2,调整逻辑2放入V2版本的数据,同时要求逻辑1校验V2版本。这样,就相当于将缓存中的数据全部失效了。

在这里插入图片描述

预热缓存

一个简单的例子

public abstract class CacheAside<M, C extends CacheManager> {

    @Autowired
    M mapper;//DAO对象
    @Autowired
    C cacheManager;//Cache对象

    private ReentrantLock lock = new ReentrantLock();

    protected String partition = "default";

    protected abstract Object lookupFromDB(Object key);

    protected abstract int updateDB(Object o);

    protected abstract int deleteDB(Object key);

    protected abstract void setPartition();
    /**
     * 查询方法,优选查询cache,
     *          hit --> 返回
     *          miss --> 查询DB,保存到cache,返回
     * @param key 查询key
     * @return 缓存数据
     */
    public Object get(Object key) {
        Object val = cacheManager.get(partition, key);
        if (val != null) {
            return val;
        } else {
            lock.lock();
            try {
                val = lookupFromDB(key);
                cacheManager.put(partition, key, val);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            return val;
        }
    }

    /**
     * 更新方法,更新数据库后再使对应条目的cache条目失效
     * @param o 需要更新的数据,pojo形式
     * @param key 该条目存在cache中的key
     * @return 是否成功
     */
    public boolean put(Object o, Object key) {
        boolean ret = true;
        lock.lock();
        try {
            updateDB(o);
            cacheManager.del(partition, key);
        } catch (Exception e) {
            e.printStackTrace();
            ret = false;
        } finally {
            lock.unlock();
        }
        return ret;
    }

    /**
     * 删除方法,删除数据库后再使对应条目的cache条目失效
     * @param key 数据存在于cache中的key,也用于该条数据查询数据库的条件(i.e. ID)
     * @return 是否成功
     */
    public boolean del(Object key) {
        boolean ret = true;
        lock.lock();
        try {
            deleteDB(key);
            cacheManager.del(partition, key);
        } catch (Exception e) {
            e.printStackTrace();
            ret = false;
        } finally {
            lock.unlock();
        }
        return ret;
    }

    protected M getMapper() {
        return mapper;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值