缓存
# 缓存雪崩
原因: 同一时间点,大量key过期,导致请求打到DB上,数据库过载宕机。
解决办法: 设置过期时间时采用固定时间+随机时间,防止同时过期。
# 缓存穿透
原因: 数据库和缓存中都没有相应数据,导致每次请求都会打到DB。
解决办法: 数据不存在时,缓存中保存空数据标识。如果客户端伪造大量不同的不存在的key同时请求,还是会有大量请求打到DB,还需要结合业务逻辑校验、限流、布隆过滤器等方法。
# 缓存击穿
原因: 对于一个热点key,在过期的瞬间,大量请求打到DB上。
解决办法: 当缓存中不存在,需要查询数据库时,通过加锁,只允许获得锁的请求查询数据库并更新缓存。其他请求在获得锁后再次检查缓存已存在,不用再请求数据库。
# 数据库与缓存数据一致性
# 先更新缓存,再更新数据库
这种方式会导致还未落库的数据被写到缓存中,被其他请求读取到,导致脏数据。不可取。
# 先更新数据库,再更新缓存
线程A更新数据库
线程B更新数据库
线程B更新缓存
线程A更新缓存
这种并发场景下,缓存中数据与数据库数据不一致。
且某些冷数据可能并不需要在更新时就缓存。
# 先删除缓存,再更新数据库
线程A删除缓存
线程B请求数据,未命中缓存,查数据库,并加载到缓存
线程A更新数据库
这种并发场景下,仍然存在缓存中数据与数据库数据不一致的情况。而且读请求一般数量较大,先删除缓存很可能立即被后续请求重新加载缓存, 所以这一步其实是无效的。
# 先更新数据库,再删除缓存
场景1:
缓存刚好过期
线程A查询数据库, 得到旧数据
线程B更新数据库
线程B删除缓存
线程A更新缓存, 写入旧数据
场景2:
线程A更新数据库
线程B查询数据,从缓存中获得旧数据
线程A删除缓存
场景3: 当使用了读写分离时
线程A更新主库数据,删除缓存
从库还未更新, 线程B读取从库得到旧数据, 更新缓存
这些场景下还是会存在缓存与数据库不一致的情况。 但是发生概率已经大大降低。
可以再加一个延时更新缓存的操作, 例如引入消息、监控binlog的canal等, 异步更新缓存。
# 强一致性保障
从上面四种处理方式可以看出, 先更新数据库, 再删除缓存是较为理想的方式。
但是在更新数据库与删除缓存之间, 读请求仍然有短暂的时间获取到旧数据。
读请求查询数据库与写入缓存之间, 写请求可能会更新数据, 导致读请求写入旧数据。
所以可以引入分布式读写锁将读请求与写请求隔离。 不过这种保障强一致性势必会提升系统的复杂性、而且会降低性能。