无题/MySQL和Redis如何保持数据一致性

Created Fri, 07 Jul 2023 14:42:35 +0800 Modified Wed, 13 Dec 2023 07:03:38 +0000
1415 Words

引言

今年找实习的时候,都遇到了这个问题,简单记录一下比较好的说法

问题现状

在如今的后端标准业务中,通常都包含了一个持久话的关系型数据库如MySQL,当面对一些要求性能的场景中,为了优化查询往往会引入一个中间缓存层

在现有使用最为频繁的中间缓存中间件是Redis,这里就遇到了我们的问题,如何保证数据库中的数据和缓存中的数据是一致的呢?

如何解决

先更新数据库然后更新缓存

最直觉的解决方法就是先更新数据库再更新缓存,这非常正常,因为我们必须要保证数据库中的数据是正确的,随后再更新缓存。这种方式看上去是有效的,但是面对并发场景时就会遇到问题了

假设我们数据库和缓存中的都有一个字段A,其值都为1。当我们有一个请求R1需要更改字段A=2时,我们首先更新了数据库中的A=2,但此时又收到一个读取A的请求R2,由于我们在R1中还未更新缓存,此时R2会直接读取缓存中的A=1值返回,这时就出现了数据不一致的情况

先更新缓存再更新数据库

那如果我们选择先更新缓存,再更新数据库呢?其实也会出现并发的问题,假如两个请求同时修改一个数据,如R1修改A=1,R2修改A=2,此时缓存中A的值为2,但由于某种原因导致R1在更新数据库时比R2慢,这时数据库中A的值为1,这就出现了数据不一致的情况

旁路缓存策略

从上面两个案例中我们可以看到,出现数据不一致的问题究其原因还是并发导致的,我们需要从并发的角度来解决这个问题

由于缓存和数据库是两个不同的程序,无法使用事物来进行,因此我们可以在读的时候需要进行判断

  • 如何有缓存,则直接返回
  • 如果没有缓存则查询数据库,然后保存到缓存中后返回

通过这个形式可以保证我们读到的数据有非常强的一致性,那写缓存呢?其实也有两种形式不过需要根据业务场景来选择

首先是第一种先删除缓存,再更新数据库,假设我们还是需要对A进行操作,此时数据库和缓存中的值均为1。这时有一个请求R1需要更改A的值为2,此时刚刚删除完缓存中的数据,同一时间另一个请求R2读取A的值为1,根据上述读策略,请求R2在读取后会进行缓存的更新,此时缓存中A=1,而R1在此时对数据库进行了更新。这样数据库中A=2,而缓存中A=1,出现了数据不一致的情况

第二种方式则为更新数据库,然后删除缓存,假设我们还是需要对A进行操作,请求R1需要更改A的值为2,此时对数据库更新后需要对缓存中的值进行删除,此时请求R2需要读取A的值,直接从缓存中读取后返回A=1,而这时R1更改了缓存为2。尽管R2读取的值和R1保存的值不一样,但这种情况出现的概率是非常低的,这是因为通常缓存的更新速度非常快速

虽然第二种方法能够保证很强的一致性,但也有一个问题那就是缓存的更新可能会失效,这时缓存中的数据在下一次正常更新之前一直是旧数据。解决这种方式也非常简单,只需要对缓存加一个过期时间,让其自动消失,随后再通过读策略的形式更新缓存,而过期时间的设定需要根据业务需求自行测试。

那如何保证更新缓存中,数据库更新和删除更新缓存是一致的呢?这里有两种方式

  • 依靠消息队列重试机制
    将需要删除的缓存保存在消息队列中,启动一个消费者线程持续消费该消息队列的值,如果删除失败则重新放回消息队列中,如果删除失败次数超过阈值则需要上报错误

  • 订阅Binlog同步数据库
    我们可以启动一个伪MysQL进程来订阅Binlog,这个log包含了MySQL对于数据的操作日志,对于更新操作日志进行同步删除操作,也需要添加重试机制保证正确删除