Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

关于缓存与数据库的一致性与过载保护机制

数据库、缓存一致性问题

先说最重要的数据库、缓存一致性问题,关于该问题,有以下几点需要考虑:

  • 当DB数据发生变更时,是删除缓存还是修改缓存?

    1. 答案是删除缓存。相比修改缓存,删除缓存是幂等性操作。删除缓存可以避免出现双写并发问题。

    2. 另外一点,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值,也就是说update的value是什么我无法简单得知。

    3. 懒加载思想: 更新了,但是没有读请求就白白浪费内存了。

  • 先写DB还是先写缓存?

    答案是先操作DB。结合case1, 在读多写少的高并发场景下,如果先删缓存再操作DB,有一个很明显的逻辑错误,使得有极高的概率出现读写并发问题。虽然先db后缓存的方式也无法完全避免这类问题,但是出现的概率极低。【详细的问题时序见下文】

    出现并发问题的时序如下:

    1. 如果读的时候,key不存在,回源头读ReadDB
    2. 写操作,UpdateDB
    3. 写操作,删除缓存
    4. 读操作,将第一步读到的数据写入缓存(此时,缓存已经过期)
  • 高并发下,关于缓存的一致性会出现什么问题?

    case1, case2中提到了在高并发的情况下,会出现某种并发逻辑错误,导致数据不一致。

  • 是否听说过订阅数据库binlog变更去清理缓存的方法?这个方法的使用场景是啥?

    通过binlog变更的方式去清理缓存,有两个好处:第一,无业务侵入型;第二,支持无限重试。

CacheAside模式

CacheAside模式,一定是最佳的吗?不一定,具体场景具体分析:

(1)新用户注册场景,同时数据库主从延迟1s

解决方案:新建数据库 + 新建缓存(避免读取到延迟的数据结果)

(2)写入特别频繁的场景,而我们对命中率有一定的要求

解决方案:

  1. updateDB + updateCache(With Lock)
  2. updateDB + updateCache(with TTL)

Write/Read Through模式

两种应对write miss的办法:

(1)write-allocate 写cache,再由cache更新db
(2)no write allocate 直接更新db

write back模式

write-back模式的读

write-back模式的写

变种:在允许数据丢失的情况下,写入时只写缓存,而异步写入存储

经典问题

缓存穿透

再说缓存穿透问题。

首先明确什么是缓存穿透问题?考虑在很高的读并发下,如果某一个redis key突然过期,会发生什么?如果真的发生这种情况,并且我们没有任何预防措施,按照cache aside模式,程序会read from db,然后set cache。但是由于并发很高,所有的线程同时去请求db,造成db过载不可用。我们称这种现象就缓存穿透。通过分布式锁来控制仅一个线程去read db,而其他线程等待,可以一定程度上避免缓存穿透问题。

缓存并发穿透(狗桩效应):极热点缓存失效,大量请求穿透到DB,造成DB瞬时压力过大

如何解决缓存并发穿透呢?
(1)热点发现,定时续期
(2)对于极热点,启用本地缓存

还有一种情况,如果请求的某一个key在db中也不存在,我们需要设置一个拥有较短TTL的空缓存来避免每次请求都穿透到db。

回种空值—-缺点在于:会占用很大的内存来存储好多无用请求,需要评估内存是否OK

布隆过滤器—-在写入DB时,额外写一份数据到布隆过滤器,查询时优先访问过滤器

image

缓存雪崩

最后谈一下缓存雪崩。

同样先搞清楚什么是缓存雪崩。由于某些系统设计不合理,缓存会设置为相同的过期时间或者很接近。这样子在某个时间点,缓存便会近乎同时失效,造成业务请求全部回源db,造成db过载,我们称这种情况为缓存雪崩。一般情况下,我们需要有意的设置key的过期时间,让他们随机过期,从而解决缓存同时过期导致的缓存雪崩问题。

热key重建

高并发多线程的情况下,热Key重键是使用redis比较典型的一个问题:

解决方案:

  1. 加锁重键(互斥锁):
  2. 热键不过期:在缓存中创建一个时间戳,先判断时间戳是否过期,如果没有过期返回原数据,过期了则访问数据源
你的支持是我坚持的最大动力!