缓存与数据库的一致性

什么是缓存?

缓存就是数据交换的缓冲区,针对服务对象的不同(本质就是不同的硬件)都可以构建缓存。
目的是,把读写速度慢的介质的数据保存在读写速度快的介质中,从而提高读写速度,减少时间消耗。 例如:

  • CPU 高速缓存 :高速缓存的读写速度远高于内存。

    • CPU 读数据时,如果在高速缓存中找到所需数据,就不需要读内存
    • CPU 写数据时,先写到高速缓存,再回写到内存。
  • 磁盘缓存:磁盘缓存其实就把常用的磁盘数据保存在内存中,内存读写速度也是远高于磁盘的。

读数据时,从内存读取。
写数据时,可先写到内存,定时或定量回写到磁盘,或者是同步回写。

为什么要用缓存?

使用缓存的目的,就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,带来更好的性能,更高的并发量。
日常业务中,我们使用比较多的数据库是 MySQL,缓存是 Redis 。Redis 比 MySQL 的读写性能好很多。那么,我们将 MySQL 的热点数据,缓存到 Redis 中,提升读取性能,也减小 MySQL 的读取压力。例如说:

  • 论坛帖子的访问频率比较高,且要实时更新阅读量,使用 Redis 记录帖子的阅读量,可以提升性能和并发。
  • 商品信息,数据更新的频率不高,但是读取的频率很高,特别是热门商品。

一致性问题是分布式常见的问题,分为最终一致性和强一致性。如果对数据有强一致性要求,不能放缓存,我们只能保证最终一致性。

缓存与数据库的一致性问题?

问题的产生

并发场景下,读取旧的 DB 数据并更新到缓存中。
缓存和 DB 的操作不在一个事务中,可能只有一个操作成功,另一个操作失败,导致不一致。

解决方案

1.先淘汰缓存,再更新数据库

先淘汰缓存,即使写数据库发生异常,也就是下次缓存读取时,多读取一次数据库,这样理论上来说保证了数据的
一致性。但是实际在并发环境下仍然会出现数据不一致的情况。
首先来说写流程:先淘汰缓存,再写 DB;
然后是读流程:先读缓存,如果未命中再读 DB,然后将 DB 中读出来的数据更新到缓存中。并发环境下,在数据库层面并发的读写并不能保证完全顺序,也就是说后发出的读请求可能先完成。举例来说

  • 线程 T1 发出了写请求,淘汰了缓存;然后写数据库,发出修改请求。
  • 线程 T2 发出了读请求,先读取缓存,未命中,再去读取数据库,发出读取请求,这时候,线程 T1 的写数据还未完成,导致 T2 读取了一个脏数据放入缓存,这样就导致了数据不一致。

这种情况下,可以引入分布式锁实现“串行化”来解决。

  • 在写请求时,先获取分布式锁,再淘汰缓存,更新完数据库后再释放锁。
  • 在读请求时,如果缓存未命中,则先获取分布式锁,加锁失败说明写请求还未完成,继续等待;加锁成功则说明写请求已完成,再去缓存查询一次,如果未命中,则再去 DB 查询并即使更新到缓存。

2.先写数据库,再更新缓存

由于操作缓存和操作数据库不是原子的,则第一步写数据库操作成功,第二步淘汰缓存失败,就会出现 DB 中是新数据,缓存中是旧数据,数据不一致的情况。在这种逻辑下,只有保证写数据库和更新缓存在同一个事务中,才能保证最终一致性。

a.基于定时任务来实现

  • 先写入数据库,然后在写入数据库所在的事务中,插入一条记录到任务表,该记录会存储需要更新的缓存 key 和 value。
  • 定时任务每秒扫描任务表,更新到缓存中,之后删除该记录。

b.基于消息队列实现

  • 首先写入数据库,然后发送带有缓存 key 和 value 的事务消息,此时需要有支持事务消息特性的消息队列。
  • 消费者消费该消息,更新到缓存中。
基于数据库的 binlog 日志

缓存与数据库的一致性

  • 应用直接写数据到数据库中。
  • 数据库更新 binlog 日志。
  • 利用 Canal 中间件读取 binlog 日志。
  • Canal 借助于限流组件按频率将数据发到 MQ 中。
  • 应用监控 MQ 通道,将 MQ 的数据更新到 Redis 缓存中。

可以看到这种方案对研发人员来说比较轻量,不用关心缓存层面,而且这个方案虽然比较重,但是却容易形成统一的解决方案。
备注说明: 上述的订阅 binlog 程序在 mysql 中有现成的中间件叫 canal,可以完成订阅 binlog 日志的功能。

链接:https://juejin.im/post/5ca1f4a251882543bf704316