数据库与Redis缓存一致性

本文探讨了Redis作为缓存解决方案时如何处理数据一致性问题。常见的策略包括缓存过期、暂存模式及其变体、通读、直写、后写和双删。每个策略都有其优缺点,例如缓存过期可能导致脏数据,暂存模式在高并发下可能不一致,后写模式通过消息队列保证最终一致性但可能延迟,而双删模式通过延迟删除提高了一致性。实际应用中,99.9%的一致性往往已经足够,关键在于选择适合场景的策略。
摘要由CSDN通过智能技术生成

如今,Redis已成为最浏览的缓存解决方案之一,尽管关系型数据库带了许多很棒的功能,如ACID。但是,为了使用这些功能,数据库的性能在高负载的情况下也会有所下降。

为了解决这个问题,许多公司和网站在应用层和数据访问层之间都会增加一个缓存层。通常使用内存中缓存来实现这个缓存层。正如我们所知,传统的关系型数据库的性能瓶颈通常是存储I/O。由于科技的发展和进步,主存储器的价格一直在下降,增加内存已经不是什么难事了,因此现在可以在内存中缓存一部分热点数据来提供性能。

背景

虽然我们可以把热点数据存储在内存中,但是这种方法已经会让人头疼,因为我们失去了对数据单一源的控制,相同的数据存储在数据库和内存中。如何避免阻塞情况下确保Redis中的数据和数据库中的数据一致?

下面,我们将了解一些常用的解决方案,这些方案大部分情况下几乎是正确的,因为它们可以保证99.9%的情况下Reids和数据库中的数据一致。但是高并发情况下就可能出现脏数据。

缓存过期

通常我们常用的方案可能就是缓存过期,但是不得不承认这是保证一致性的糟糕方案。

例如:我们设置缓存过期时间为30分钟,你就要确保这30分钟内不会读取到脏数据。如果将过期时间设置得更短会不会好点?

如果你的网站具有巨大的流量和高并发服务,这样你确实缩短了不一致的可能,但是已经违背了使用缓存的初衷,可能会有很多缓存违背命中。

暂存

暂存模式通常是这样的:

对于不可变的操作(读取):

  • 缓存命中:直接从Redis中返回数据,无需查询MySql
  • 缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端

对于可变操作(创建、更新、删除):

  • 创建、更新、删除MySql中的数据
  • 删除Redis中的数据,总是删除而不是更新缓存,下一个缓存未命中将插入新值

这种方法通常被我们使用,实际上,它是MySql和Redis之间实现缓存一致性的标准。但是,这种方法也存在一些问题:

  • 正常情况下,假设写入MySql / Redis绝对不会失败,它通常可以保证最终一致性。假设我们有个热门服务,做了负载分别放在A、B两个服务器上,在某个时刻A已经成功更新了MySql中的数据。在删除Reids中的数据之前,B尝试读取这个数据,然后B将命中缓存,因为辞职A还没有来得及删除Redis中的数据。因此B就读取到了脏数据,但是Redis中的数据最终还是会被删除,其他服务最终将读取到更新后的数据。
  • 在极端情况下,它也不能保证最终的一致性。同样的情况,如果A在尝试删除Redis中的数据前刚好被kill掉,这样Redis中的数据将无法被删除。这样其他服务器都会读取到脏数据。
  • 即使在正常情况下,也存在极低的可能性,最终一致性得不到保障。假设A尝试读取数据,缓存未命中,然后从MySql中读取数据。此时,由于高并发和巨大的流量导致A的服务器突然卡了。这是B尝试更新相同的数据,D更新MySql, 并删除了Redis中的数据。之后A恢复并将其查询结果保存到了Redis。这样后面其他服务都会读出到脏数据,虽然这种可能性非常低。

暂存 - 变体1

暂存模式 - 变体1为:

对于不可变操作(读取):

  • 缓存命中:直接从Redis中返回数据,无需查询MySql
  • 缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端

对于可变的操作(创建、更新、删除):

  • 删除Redis中的数据
  • 创建、更新、删除MySql数据

这种方案也是非常糟糕的。假设A尝试更新数据,在某个时刻,A已经成功删除了Reids中的数据。在A更新MySql中的数据之前,B尝试读取相同的数据,且缓存未命中。然后B查询MySql并将数据保存到Redis中。注意,此时MySql中的数据尚未更新。由于A后面不会再删除Redis中的数据,因此旧的数据依旧保存在Redis中了。

暂存 - 变体2

暂存模式 - 变体1为:

对于不可变操作(读取):

  • 缓存命中:直接从Redis中返回数据,无需查询MySql
  • 缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端

对于可变的操作(创建、更新、删除):

  • 创建、更新、删除MySql数据
  • 在Redis中创建、更新、删除数据

这也是一个不好的解决方案。假设,A、B都试图更新数据,A在B之前更新了MySql。但是B会在A之前更新Redis。最终,Mysq中的数据会由B更新。但是Redis中的数据则由A更新,这将导致不一致。

通读

通读模式为:

对于不可变的操作(读取):

  • 客户端始终从Redis中读取。缓存未命中,这Redis应具有自动从数据库中读取的功能。

对于可变的操作(创建、更新、删除):

  • 此策略不处理可变操作。它与只写模式结合使用

直写

只写模式为:

对于不可变的操作(读取):

  • 此策略不处理不变的操作。它与通读模式结合使用

对于可变的操作(创建、更新、删除):

  • 客户端仅在Redis中创建、更新、删除数据。Redis必须原子地把数据同步到MySql

直接模式的缺点非常明显,首先大部分的缓存中间件并不支持此功能。其次,Redis是缓存而不是RDBMS。Redis的主要目的并不是弹性扩展,因此在更改复制到MySql之前,很可能会丢失。即使Reids现在支持RDB和AOF之类的持久化技术,但是仍然不建议使用。

后写

对于不可变的操作(读取):

  • 此策略不处理不变的操作。它与通读模式结合使用

对于可变的操作(创建、更新、删除):

  • 客户端需要在Redis中创建、更新、删除数据。Redis将更改放到消息队列中,然后返回成功给客户端。更改被异步复制到MySql中。

后写模式与直接模式不同的时,它异步地将更改复制到MySql,这样客户端就不必等待。所以提供了吞吐量。Redis从5.0开始支持Redis流,这可能是一个不错的方式,为了提供性能,可以合并更改并批量更新到MySql中。后写模式的缺点同样也是许多缓存中间件并不支持此功能。其次,使用消息队列必须是FIFO,保证最终结果。而且还要保证消息队列的并发等,如MQ。

双删

双删模式为:

对于不可变的操作(读取):

    • 缓存命中:直接从Redis中返回数据,无需查询MySql
    • 缓存未命中:查询MySql(可以使用只读来提高性能),将返回的数据放到Redis中,然后返回结果给客户端

对于可变的操作(创建、更新、删除):

  • 删除Redis中的数据
  • 创建、更新、删除MySql中的数据
  • sleep一段时间如500ms
  • 再次删除Redis中的数据

这种模式目前是最被接受的方案。它结合了暂存 - 变体1,由于它是基于暂存 - 变体1改进的,因为在大多数情况下它可以保证最终一致性。它也通过sleep来确保删除了脏数据。尽管仍然存在极端的情况会破坏最终一致性,但是这个可能性很小。

后写 - 变体1

来自阿里巴巴canal项目的一种新颖模式,它是通过另外一种方式执行复制。它没有直接把Redis中的更改复制到MySql,而是通过MySql的binlog将数据复制到Redis。与后写模式相比,这更好地保证了持久性和一致性。由于binlog是RDMS技术的一部分,因此它非常具有弹性,而且这种技术,早就被用在MySql之间做主从同步。

结论

对于实际情况而言,99.9%的正确性已经足够了,我们应该谨记使用Redis的最初目的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值