数据库、缓存一致性问题
先说最重要的数据库、缓存一致性问题,关于该问题,有以下几点需要考虑:
当DB数据发生变更时,是删除缓存还是修改缓存?
答案是删除缓存。相比修改缓存,删除缓存是幂等性操作。删除缓存可以避免出现双写并发问题。
另外一点,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值,也就是说update的value是什么我无法简单得知。
懒加载思想: 更新了,但是没有读请求就白白浪费内存了。
先写DB还是先写缓存?
答案是先操作DB。结合case1, 在读多写少的高并发场景下,如果先删缓存再操作DB,有一个很明显的逻辑错误,使得有极高的概率出现读写并发问题。虽然先db后缓存的方式也无法完全避免这类问题,但是出现的概率极低。【详细的问题时序见下文】
出现并发问题的时序如下:
- 如果读的时候,key不存在,回源头读ReadDB
- 写操作,UpdateDB
- 写操作,删除缓存
- 读操作,将第一步读到的数据写入缓存(此时,缓存已经过期)
高并发下,关于缓存的一致性会出现什么问题?
case1, case2中提到了在高并发的情况下,会出现某种并发逻辑错误,导致数据不一致。
是否听说过订阅数据库binlog变更去清理缓存的方法?这个方法的使用场景是啥?
通过binlog变更的方式去清理缓存,有两个好处:第一,无业务侵入型;第二,支持无限重试。
CacheAside模式
CacheAside模式,一定是最佳的吗?不一定,具体场景具体分析:
(1)新用户注册场景,同时数据库主从延迟1s
解决方案:新建数据库 + 新建缓存(避免读取到延迟的数据结果)
(2)写入特别频繁的场景,而我们对命中率有一定的要求
解决方案:
- updateDB + updateCache(With Lock)
- updateDB + updateCache(with TTL)
Write/Read Through模式
两种应对write miss的办法:
(1)write-allocate 写cache,再由cache更新db
(2)no write allocate 直接更新db
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时,额外写一份数据到布隆过滤器,查询时优先访问过滤器
缓存雪崩
最后谈一下缓存雪崩。
同样先搞清楚什么是缓存雪崩。由于某些系统设计不合理,缓存会设置为相同的过期时间或者很接近。这样子在某个时间点,缓存便会近乎同时失效,造成业务请求全部回源db,造成db过载,我们称这种情况为缓存雪崩。一般情况下,我们需要有意的设置key的过期时间,让他们随机过期,从而解决缓存同时过期导致的缓存雪崩问题。
热key重建
高并发多线程的情况下,热Key重键是使用redis比较典型的一个问题:
解决方案:
- 加锁重键(互斥锁):
- 热键不过期:在缓存中创建一个时间戳,先判断时间戳是否过期,如果没有过期返回原数据,过期了则访问数据源