当引入缓存后,不可避免的会出现缓存一致性的问题。当数据进行了更新操作,如何保证缓存中的数据能够得到正确的更新?下面就来讨论一下这个问题。

# 更新的模式

考虑不同的方式,要同时考虑两步中一个成功另一个失败的情况,同时要考虑并发操作的情况。

  1. 先缓存后数据库 这种方式明显不行,数据库是最终持久化数据的地方,只有再确保数据更新完成后才去更新缓存,避免数据保存失败的问题。
  2. 更新数据库再更新缓存 确保的数据已经更新成功,但在并发情况下,无法保证其顺序。例如 A 更新 X=1,B 更新 X=2,但可能 B 先去写了缓存,而 A 后去又覆盖了缓存,导致缓存变成过期数据。在 1 中也存在这个问题,时序无法保证。
  3. 更新数据库再失效缓存 失效可以避免上述的问题,等到下一次需要时来查询再填充,也可以避免一些无用的缓存填充操作,例如这个缓存数据并不马上需要,且构建复杂。但再极端情况下仍可能出现异常,例如一个查询未命中缓存,去查询到了数据,而在它要填充前,另一个更新操作完成,此时查询的填充操作就把旧数据填入了缓存。不过这种情况发生的概率很低,一般更新都会更加慢。

由此可见,其实很难有一种方式能确保缓存的一致性,要实现强一致性就势必会影响性能,如 2PC 或 Paxos 协议等,实现也更加复杂。所以更常用的,是依靠异步的补偿,去实现最终一致性。

# 异步补偿

同步补偿时间不可接受,也无法确定需要耗时多久,因此选择另开 Job 去进行这种操作。这里又有不同的方式:

# 消息队列

数据产生更新后,往消息队列投递对应的缓存更新消息,由专门的服务去消费并进行缓存的构建。

引入消息队列必然会让整体架构更加复杂,且需要维护消息队列的高可用。

# 订阅 binlog

还有一种即订阅数据库的 binlog,这样不用在应用里投递消息,直接由数据库的变更驱动缓存的更新。实际上,这种方式相当于 MySQL 的主从同步,只不过是同步到缓存中去。

# 优化

但使用异步的方式就高枕无忧了吗?实际上还是可能会有问题。考虑这样的情况:

  • 读线程 A cache miss,进行查询获得 v1
  • 写线程 B 写入数据为 v2,并删除缓存
  • 异步服务订阅到 binlog 进行缓存回填,设置缓存为 v2
  • 读线程 A 填充缓存为 v1

实际上和上面未使用异步服务一样,它们都无法满足 "Happens before",也就是谁先谁后的顺序并不确定。在同样都会写缓存的情况下,就可能会出现覆盖的情况。

因此这里可以这样优化:对于读的线程来说,cache miss 后进行查询回填操作,它的优先级较低,使用 SETNX 操作,如果设置的时候发现缓存已经存在了,那么说明已经被写的操作回填了,因此它不进行任何操作。而对于写线程 / 异步更新的 Job 来说,更新优先级更好,它写缓存则直接覆盖,确保写入最新的数据。

其实就是很小的改动点,为更新缓存增加了优先级,避免了覆盖的情况出现。

# 主从不一致

但还有一个问题,就是主从同步的延迟问题。

现在数据库大都使用主从模式,主负责接受写,从负责读。读写分离的情况下,由于主从之间存在延迟可能导致数据不一致的问题,例如:

  • A 写入数据 v2,并删除缓存
  • B 读取 cache miss,读取到从库 v1
  • 主从同步为 v2
  • B 更新缓存为 v1

如是异步补偿使用消息队列,且在更新后投递更新的消息,那也有可能出现读取了从库旧的数据进行更新。这种可以考虑从主库读取数据确保最新。

如果是订阅 binlog,则订阅到说明已经获取到了,更新的是最新的数据。

# 进程内缓存

当遇到热点缓存的时候,一般会有两种解决方案。一种是将热点 key 分散到多个节点中去,例如对 key 加上编号,避免请求都达到同一个节点上。另一种就是将热点缓存提升为进程内缓存。

通过发现某段时间内高频次请求的内容,将其缓存到进程内不,避免了去向 redis 请求数据。但这又带来了缓存一致性的问题。可以尝试以下的几种方式:

  1. 主动通知
    执行更新的节点通知其他节点,重新从缓存中获取数据。缺点在于,复杂系统中节点众多,关系错乱,要通知其他节点完成更新并不容易。

  2. 消息队列通知
    通过消息队列,发出缓存更新的消息,各节点消费消息并更新缓存。这简化了上述的通知逻辑,但引入消息队列本身就引入了复杂度。

  3. 定时更新
    进程内设置缓存的有效期,并在过期时重新获取。这相对简单一点,但没有彻底解决问题,只是将可能的影响减到最少。且不同节点间更新不一致,可能会读到脏数据。

考虑到提升为进程间缓存主要目的在于缓解高热点时 redis 的压力,也可以设置较短的有效期,在一定程度上保证缓存的延迟不会太高。

同样的,这也是一种取舍,没有完全完美的方案能解决这个问题。因此只能衡量业务中的利弊,做相应选择。要强一致,必然要牺牲性能;要高性能,就只能保证最终一致性。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

HuaLin 微信支付

微信支付

HuaLin 支付宝

支付宝