[

6月9日
如何解决Redis和MySQL之间的数据一致性问题
Redis和MySQL的数据一致性解决方案
Redis具有高性能的数据读写功能,被广泛应用于缓存场景中。首先,它可以提高业务系统的性能,其次,它可以抵御数据库的高并发流量请求。
那么在使用过程中,我们经常会遇到一些场景,需要解决Redis和Mysql数据库之间的数据一致性问题。
今天我将分享我自己的解决方案。
什么是数据库和缓存的一致性?
数据一致性指的是。
- 缓存中有数据,且缓存中的数据值与数据库中的值相等
- 缓存中没有数据,数据库中的值等于最新的值。
推回的缓存与数据库不一致。
- 缓存的数据值不等于数据库中的值。
- 缓存或数据库中存在旧数据,导致线程读取旧数据。
为什么会出现数据一致性问题?
当使用Redis作为缓存时,我们需要在数据变化时进行双倍写入,以确保缓存与数据库中的数据一致。
数据库和缓存,毕竟是两个系统。如果要保证强一致性,就需要引入分布式一致性协议,如2PC 或Paxos ,或分布式锁等。
这是很难实现的,而且肯定会影响执行。性能也有影响。
如果对数据的一致性要求很高,那么真的有必要引入缓存吗?
缓存的使用策略
在使用缓存时,通常有以下几种缓存使用策略来提高系统性能。
Cache-Aside PatternRead-Through PatternWrite-Through PatternWrite-Behind Pattern
1.缓存靠边
所谓缓存旁,是指读取缓存、读取数据库、更新缓存的操作都在应用系统中完成,是业务系统中最常用的缓存策略。
读取数据的逻辑如下。
- 当应用程序需要从数据库中读取数据时,首先检查缓存的数据是否被击中。
- 如果缓存未命中,则查询数据库获取数据,同时将数据写入缓存,这样后续对相同数据的读取就会命中缓存,最后将数据返回给调用者。
- 如果缓存命中,则直接返回。
优势
- 只有应用程序实际请求的数据被包含在缓存中,有助于保持缓存的成本效益。
- 实现起来很简单,可以得到性能的提高。
实现的伪代码如下。
缺点
由于数据只有在缓存缺失后才被加载到缓存中,由于需要额外的缓存填充和数据库查询时间,第一次调用的数据请求的响应时间会增加一些开销。
更新数据
- 写数据到数据库。
- 使缓存中的数据无效或更新缓存中的数据。
当使用cache-aside时,最常见的写入策略是直接将数据写入数据库,但缓存可能与数据库不一致。
我们应该为缓存设置一个过期时间,这是保证最终一致性的解决方案。
如果过期时间太短,应用程序将不断从数据库中查询数据。同样的,如果过期时间太长,而更新又没有使缓存失效,那么缓存的数据很可能是脏的。
最常见的方法是删除缓存,使缓存数据失效。
为什么不更新缓存呢?
第一:性能问题。
当缓存的更新成本很高,需要访问多个表进行联合计算时,建议直接删除缓存,而不是更新缓存数据,以保证一致性。
第二:安全问题。
在高并发的情况下,查询到的数据可能是旧值。
2.穿透式读取
当缓存失误时,数据也会从数据库加载,写入缓存,并同时返回给应用系统。
尽管read-through 与cache-aside 非常相似,但在cache-aside 中,应用程序负责从数据库中获取数据并填充缓冲区。
Read-Through另一方面,在,将从数据存储中获取数值的责任转移给了缓存提供者。
Read-Through 实现了关注点分离的原则。代码只与缓存进行交互,而缓存组件则管理自身与数据库之间的数据同步。
3.穿透式写法
与Read-Through ,当发生写入请求时,Write-Through ,将写入责任转移给缓存系统,缓存抽象层完成缓存数据和数据库数据的更新。
Write-Through 的主要好处是,应用系统不需要考虑故障处理和重试逻辑,而是交给缓存抽象层来管理实现。
直接使用这个策略是没有意义的,因为这个策略需要先写到缓存,然后再写到数据库,这给写操作带来额外的延迟。
当Write-Through 与Read-Through 配合使用时,可以充分发挥穿透式的优势,同时可以保证数据的一致性,而且不需要考虑如何使缓存的设置失效。
这个策略颠覆了Cache-Aside 填充缓存的顺序。而不是在缓存缺失后懒洋洋的加载到缓存中,而是先把数据写到缓存中,然后由缓存组件把数据写到数据库中。
优势
- 缓存和数据库数据总是最新的。
- 查询性能最好,因为要查询的数据可能已经被写入缓存。
缺点
- 不经常请求的数据也会被写入缓存,导致缓存更大,更昂贵。
4.写在后面
乍一看,这个图和Write-Through ,其实不然,区别在于最后一个箭头上的箭头:从实线变成了线。
这意味着缓存系统将异步更新数据库数据,而应用系统只与缓存系统进行交互。
应用系统不需要等待数据库更新完成,提高了应用系统的性能,因为数据库的更新是最慢的操作。
在这种策略下,缓存和数据库之间的一致性并不强,因此不建议用于具有高一致性的系统。
Cache-Aside中一致性问题的分析
Cache-Aside策略是商业场景中使用最多的。在这种策略下,客户端先从缓存中读取数据,如果命中则返回。将数据写入缓存,所以读操作不会导致缓存和数据库之间的不一致。
重点是写操作。数据库和缓存都需要被修改,两者之间会有一个顺序,这可能会导致数据不再一致。对于写操作,我们需要考虑两个问题。
- 先更新缓存还是先更新数据库?
- 当数据发生变化时,选择修改缓存(更新)还是删除缓存(删除)?
结合这两个问题,出现了四种情况。
- 先更新缓存,再更新数据库。
- 先更新数据库,再更新缓存。
- 先删除缓存,再更新数据库。
- 先更新数据库,再删除缓存。
1.先更新缓存,再更新数据库。
如果先更新缓存而数据库写入失败,那么缓存是最新的数据,数据库是旧的数据,而缓存是脏的数据。
之后,其他的查询马上就会得到这些数据,但这些数据在数据库中并不存在。
不存在于数据库中的数据对于缓存和返回给客户端是没有意义的。
程序直接通过。
2.先更新数据库,再更新缓存。
一切工作正常,如下图。
- 先写数据库,成功。
- 然后更新缓存,成功。
如果更新缓存失败。
这时,我们来推断一下,如果这两个操作的原子性被破坏:如果第一步成功,第二步失败,会发生什么?
会导致数据库是最新的数据,而缓存是旧的数据,导致一致性问题。
我就不画这个图了。和前面的图片类似,只是改变了Redis和MySQL的位置。
在高并发的情况下,如果多个线程同时写数据,再写到缓存,缓存的旧值和数据库的最新值肯定会有不一致的地方。
有哪些一致的解决方案呢?
1.缓存延迟双重删除
- 先删除缓存。
- 写数据库。
- 睡眠500毫秒,然后删除缓存。
这样一来,最多只有500毫秒的脏数据读取时间。关键是如何确定睡眠时间?
延迟时间的目的是为了保证读请求结束后,写请求可以删除由读请求引起的缓存脏数据。
因此,我们需要自己评估项目的数据读取业务逻辑的耗时情况,在读取时间的基础上增加几百毫秒作为延迟时间。
2.移除缓存重试机制
如果缓存删除失败该怎么办?例如,如果延迟双倍删除的第二次删除失败,说明脏数据不能被删除。
使用重试机制,确保缓存删除成功。
例如,如果重试了三次,三次都失败了,就会把日志记录到数据库中,并发出警告,要求人工干预。
在高并发的情况下,最好使用异步方法进行重试,比如向MQ中间件发送消息,实现异步解耦。
步骤(5)如果删除失败且未达到最大重试次数,消息将被重新排队,直到删除成功,否则将被记录在数据库中供人工干预。
这种方案的缺点是会对业务代码造成侵扰,所以有了下一个方案,启动一个专门订阅数据库bin-log的服务,读取要删除的数据,进行缓存删除操作。
3.异步读取bin-log删除
- 更新数据库。
- 数据库会将操作信息记录在bin-log日志中。
- 使用canal订阅bin-log日志,获取目标数据和密钥。
- 缓存删除系统获取canal数据,解析目标key,并尝试删除缓存。
- 如果删除失败,将消息发送到消息队列中。
- 缓存删除系统再次从消息队列中获取数据,再次执行删除操作。
总结
缓存策略的最佳实践是Cache Aside Pattern。它们被分为读缓存最佳实践和写缓存最佳实践。
读缓存的最佳实践:先读缓存,如果命中就返回。如果错过了就查询数据库,然后再写到缓存中。
写缓存的最佳实践。
- 先写数据库,然后操作缓存。
- 直接删除缓存,而不是修改缓存。
因为当缓存的更新成本很高时,需要访问多个表进行联合计算,建议直接删除缓存而不是更新缓存。
另外,删除缓存的操作很简单,副作用也只是增加了一次缓存的丢失。建议你使用这个策略。
在上述最佳实践下,为了尽可能的保证缓存和数据库的一致性,我们可以使用延迟的双重删除。
为了防止删除失败,我们使用异步重试机制来保证删除的正确性。通过异步机制,我们可以向MQ消息中间件发送删除消息,或者使用运河订阅MySQL的bin-log日志,以监测删除相应缓存的写请求。
那么,如果我必须保证绝对的一致性呢,先给出结论。
没有办法实现绝对一致性,这是由CAP理论决定的。缓存系统的适用场景是非强一致性的场景,所以它属于CAP中的AP。
因此,我们必须做出妥协,我们可以实现BASE理论中提到的最终一致性。
其实,一旦在方案中使用了缓存,往往意味着我们放弃了数据的强一致性,但也意味着我们的系统可以在性能上得到一定的改善。
所谓的权衡,正是如此。
感谢你阅读这篇文章。
请继续关注更多内容。