数据一致性问题通常产生于数据在不同的时间点、地点或系统中存在多个副本的情况,
系统只存在一个副本的情况下也完全可能会产生。
设想一下,你在一家连锁咖啡店有一张会员卡这张会员卡可以绑定两个账号(假设是微信号),这张卡记录了你的积分。
你在北京的分店购买了一杯拿铁,积分增加了5分。就是这么巧,同一时间,你的好朋友在上海的分店也买了一杯拿铁,积分也增加了5分。
按理来说用这张卡买了两杯咖啡,系统上应该给你增加10分,但是,如果北京分店的系统更新到中央数据库的操作延迟了,
上海分店的系统可能还没有得到最新的积分信息。
这时,上海分店计算总积分的方式就是:原来的总积分 + 5分,北京的分店的计算方式也是:原来的总积分 + 5分,这张卡总的就加了5分,这就是一种数据一致性问题。
即使在单个系统内部,也可能存在类似的问题。
假设咖啡店的积分记录系统在进行维护,而你在这个时间点进行了消费。
如果维护操作和积分更新操作没有被正确的协调,可能会导致你的消费没有被记录到积分中,引发数据一致性问题。
在上面这个例子中,数据一致性问题是由于信息更新在不同地点之间同步不及时造成的,而在单个系统中,
可能是因为操作的顺序和同步机制没有设计得当导致一致性问题。
解决这类问题通常需要引入事务管理、分布式锁、队列、事件驱动的更新机制等策略来确保数据的准确性和实时性。
在软件架构设计中,遵循的原则是尽量保持简洁和避免不必要的复杂性,
这个原则被称作 “最少知识原则” (Principle of Least Knowledge),也被称为 “迪米特法则” (Law of Demeter)。
当它和架构设计中的简约原则联系起来时,你可能指的是 “简单性原则” 或 “奥卡姆剃刀”(Occam’s Razor) 原则。
奥卡姆剃刀原则通常被解释为在没有明显差别的情况下,应该选择假设较少、假设简单的理论。
在软件架构的上下文中,这意味着在其他条件相同的情况下,应该选择更简单、组件更少的设计方案。
这样做的好处包括减少潜在的错误来源、降低系统的维护成本、提高系统的可理解性,并且有时还能提高性能。
每引入一个新组件,就增加了系统的复杂度和潜在的维护负担。因此,设计时应当权衡新组件带来的好处与其引入的复杂度之间的关系。
MySQL 和 Redis 的数据一致性问题通常来源于它们在系统架构中扮演的不同角色和特性。
MySQL 是一个关系型数据库,通常用于存储持久化数据,而 Redis 是一个内存中的数据结构存储,常用作缓存和消息代理。
它们之间的数据一致性问题主要产生于下面几个方面:
数据更新时机:当数据在 MySQL 中被更新后,如果 Redis 中缓存的同一份数据没有被同时更新,就会出现不一致的情况。用户可能从缓存中获取到了旧的数据。
复制延迟:如果你使用 Redis 的复制功能来增强数据的可用性和冗余,可能会遇到主从复制延迟的问题。在高负载情况下,从节点上的数据可能会落后于主节点,导致数据一致性问题。
缓存穿透:如果请求的数据在 Redis 中没有找到,就会查询 MySQL 数据库。如果大量此类请求发生,而数据实际上并不存在于数据库中,这可能会导致数据库层面的性能问题,进而影响数据的读取和写入一致性。
缓存失效策略:当缓存因为达到了设定的时间限制或空间限制而被清除,如果数据更新策略没有得到很好的处理,那么当下一次读取时,就会从 MySQL 中读取到新的数据,而这段时间内,读取操作可能会得到不一致的结果。
事务处理:MySQL 支持事务处理,可以通过 ACID 属性(原子性、一致性、隔离性、持久性)来保证操作的准确性。而 Redis 对事务的支持是有限的,尽管它有一定的事务操作命令,但没有严格的 ACID 属性保证。
系统故障:系统崩溃或网络故障可能会导致正在同步的数据丢失或不完整,这也会引起数据一致性问题。
确保 MySQL 和 Redis 数据一致性可以通过以下几个更加详细的解决方案来实现:
1、先写数据库后写缓存:
首先更新 MySQL 数据库,确保数据持久化后,再更新 Redis 缓存。
这样即使缓存更新失败,应用也可以从数据库中读取最新数据,然后重试更新缓存。例如:
try {
// 更新数据库
updateDatabase(record);
// 同步更新缓存
updateCache(record);
} catch(Exception e) {
// 如果缓存更新失败,可以记录日志并进行重试或者使用其他补偿机制
log.error("Error updating cache", e);
// 可以选择重新尝试更新缓存或者放入队列稍后处理
retryUpdateCache(record);
}
![ ][nbsp]
2、事务消息:
使用本地事务配合事务消息中间件,比如 RocketMQ 的事务消息功能。
先发送预备消息,然后在本地事务中执行数据库操作,根据操作结果最后提交或回滚消息。
如果消息提交成功,则消费者监听到消息后更新 Redis 缓存。这样就能保证数据库操作和缓存更新的最终一致性。
3、缓存双删策略:
在更新数据库之前和之后都删除缓存,第一次删除是为了防止在更新数据库的过程时间窗口内有新的请求访问到旧的缓存数据,
第二次删除是为了处理在第一次删除之后到数据库更新期间产生的脏数据。
// 第一次删除缓存
deleteCache(key);
// 更新数据库
updateDatabase(record);
// 休眠一段时间,比如100ms,让前面的数据库操作和缓存删除操作完成
Thread.sleep(100);
// 第二次删除缓存
deleteCache(key);
![ ][nbsp 1]
缓存双删策略就是在更新数据库记录之前和之后都执行一次缓存删除操作。
这个策略的目的是为了尽可能减少数据库与缓存间数据不一致的时间窗口,解决缓存数据可能出现的脏读问题。具体的原因如下:
a、预防更新期间的脏读:
当更新数据库记录之前先删除缓存,目的是为了防止在数据库更新期间,如果有新的请求进来,这个请求会因为缓存不命中而去读取数据库中的最新数据,并将其写入到缓存中。
b、处理更新期间的写请求:
即使进行了第一次缓存删除操作,但在数据库更新完成之前,新的读请求可能已经带着旧的数据重新填充了缓存(这是因为删除缓存和更新数据库之间还是有一个很短的时间窗口)。因此,在数据库更新之后再次删除缓存,可以确保如果有旧数据被写入缓存,那么这些旧数据也会被清除。
c、确保数据一致性:
第二次删除操作是为了确保任何在两次删除操作中间由于旧数据写入缓存的情况得到处理。
这样即使发生了这种情况,缓存中的旧数据最终也会被清除,新的读请求会再次触发缓存的重建,这次则会从数据库加载最新的数据。
简单来说,缓存双删策略是一种实践中比较简单有效的方法,用于降低缓存数据不一致的风险。
然而,这种策略也不能完全保证缓存和数据库数据的强一致性,因为在高并发场景下仍然存在极小的窗口期可能会导致脏读。
因此,在一些对数据一致性要求极高的场景中,可能需要其他更复杂的数据同步机制。
![ ][nbsp 2]
4、发布/订阅模式:
在更新数据库后,发布一个事件到消息队列。有一个订阅了这个队列的缓存更新服务,它会监听这些事件,并且负责更新或者删除对应的缓存。
5、定时校对进程:
定期运行一个后台进程,该进程对数据库和缓存数据进行校对。如果发现不一致,将缓存数据更新为数据库中的最新状态。这种方法适用于对一致性要求不是非常实时的场景。
这些方法可以根据实际业务的需求和场景进行选择和调整,通常需要综合使用这些策略来达到较优的一致性保障。
在实践中,还需要对各种边界条件和异常情况进行详细的考虑,以确保系统的健壮性。
最后说一句(求关注,求赞,别白嫖我)
最近无意间获得一份阿里大佬写的刷题笔记和面经,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的, 7701页的阿里大佬写的刷题笔记,让我offer拿到手软
求一键三连:点赞、分享、收藏
点赞对我真的非常重要!在线求赞,加个关注我会非常感激!@小郑说编程
[nbsp]: https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=http%3A%2F%2F%2Fwww.feiz.vip%2Fimages%2Ffenbushi%2FMySQL_Redis%2Fconsistency%2Ffirst_MySQL.png&pos_id=img-fdg0tjWU-1704726201602)
[nbsp 1]: https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=http%3A%2F%2F%2Fwww.feiz.vip%2Fimages%2Ffenbushi%2FMySQL_Redis%2Fconsistency%2Fdouble_del1.png&pos_id=img-xpIJro8b-1704726201892)
[nbsp 2]: https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=http%3A%2F%2F%2Fwww.feiz.vip%2Fimages%2Ffenbushi%2FMySQL_Redis%2Fconsistency%2Fdouble_del2.png&pos_id=img-bicyRUqp-1704726202539)