本文主要讨论这么几个问题:
(1)啥时候数据库和缓存中的数据会不一致
(2)不一致优化思路
(3)如何保证数据库与缓存的一致性
一、需求缘起
上一篇《缓存架构设计细节二三事》(点击查看)引起了广泛的讨论,其中有一个结论:当数据发生变化时,“先淘汰缓存,再修改数据库”这个点是大家讨论的最多的。
上篇文章得出这个结论的依据是,由于操作缓存与操作数据库不是原子的,非常有可能出现执行失败。
假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致【如上图:db中是新数据,cache中是旧数据】。
假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss【如上图:cache中无数据,db中是旧数据】。
结论:先淘汰缓存,再写数据库。
引发大家热烈讨论的点是“先操作缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致”,这就是本文要讨论的主题。
二、为什么数据会不一致
回顾一下上一篇文章中对缓存、数据库进行读写操作的流程。
写流程:
(1)先淘汰cache
(2)再写db
读流程:
(1)先读cache,如果数据命中hit则返回
(2)如果数据未命中miss则读db
(3)将db中读取出来的数据入缓存
什么情况下可能出现缓存和数据库中数据不一致呢?
在分布式环境下,数据的读写都是并发的,上游有多个应用,通过一个服务的多个部署(为了保证可用性,一定是部署多份的),对同一个数据进行读写,在数据库层面并发的读写并不能保证完成顺序,也就是说后发出的读请求很可能先完成(读出脏数据):
(a)发生了写请求A,A的第一步淘汰了cache(如上图中的1)
(b)A的第二步写数据库,发出修改请求(如上图中的2)
(c)发生了读请求B,B的第一步读取cache,发现cache中是空的(如上图中的步骤3)
(d)B的第二步读取数据库,发出读取请求,此时A的第二步写数据还没完成,读出了一个脏数据放入cache(如上图中的步骤4)
即在数据库层面,后发出的请求4比先发出的请求2先完成了,读出了脏数据,脏数据又入了缓存,缓存与数据库中的数据不一致出现了
三、不一致优化思路
能否做到先发出的请求一定先执行完成呢?常见的思路是“串行化”,今天将和大家一起探讨“串行化”这个点。
先一起细看一下,在一个服务中,并发的多个读写SQL一般是怎么执行的
上图是一个service服务的上下游及服务内部详细展开,细节如下:
(1)service的上游是多个业务应用,上游发起请求对同一个数据并发的进行读写操作,上例中并发进行了一个uid=1的余额修改(写)操作与uid=1的余额查询(读)操作
(2)service的下游是数据库DB,假设只读写一个DB
(3)中间是服务层service,它又分为了这么几个部分
(3.1)最上层是任务队列
(3.2)中间是工作线程,每个工作线程完成实际的工作任务,典型的工作任务是通过数据库连接池读写数据库
(3.3)最下层是数据库连接池,所有的SQL语句都是通过数据库连接池发往数据库去执行的
工作线程的典型工作流是这样的:
void work_thread_routine(){
Task t = TaskQueue.pop(); // 获取任务
// 任务逻辑处理,生成sql语句
DBConnection c = CPool.GetDBConnection(); // 从DB连接池获取一个DB连接
c.execSQL(sql); // 通过DB连接执行sql语句
CPool.PutDBConnection(c); // 将DB连接放回DB连接池
}
提问:任务队列其实已经做了任务串行化的工作,能否保证任务不并发执行?
答:不行,因为
(1)1个服务有多个工作线程,串行弹出的任务会被并行执行
(2)1个服务有多个数据库连接,每个工作线程获取不同的数据库连接会在DB层面并发执行
提问:假设服务只部署一份,能否保证任务不并发执行?
答:不行,原因同上
提问:假设1个服务只有1条数据库连接,能否保证任务不并发执行?
答:不行,因为
(1)1个服务只有1条数据库连接,只能保证在一个服务器上的请求在数据库层面是串行执行的
(2)因为服务是分布式部署的,多个服务上的请求在数据库层面仍可能是并发执行的
提问:假设服务只部署一份,且1个服务只有1条连接,能否保证任务不并发执行?
答:可以,全局来看请求是串行执行的,吞吐量很低,并且服务无法保证可用性
完了,看似无望了,
1)任务队列不能保证串行化
2)单服务多数据库连接不能保证串行化
3)多服务单数据库连接不能保证串行化
4)单服务单数据库连接可能保证串行化,但吞吐量级低,且不能保证服务的可用性,几乎不可行,那是否还有解?
退一步想,其实不需要让全局的请求串行化,而只需要“让同一个数据的访问能串行化”就行。
在一个服务内,如何做到“让同一个数据的访问串行化”,只需要“让同一个数据的访问通过同一条DB连接执行”就行。
如何做到“让同一个数据的访问通过同一条DB连接执行”,只需要“在DB连接池层面稍微修改,按数据取连接即可”
获取DB连接的CPool.GetDBConnection()【返回任何一个可用DB连接】改为
CPool.GetDBConnection(longid)【返回id取模相关联的DB连接】
这个修改的好处是:
(1)简单,只需要修改DB连接池实现,以及DB连接获取处
(2)连接池的修改不需要关注业务,传入的id是什么含义连接池不关注,直接按照id取模返回DB连接即可
(3)可以适用多种业务场景,取用户数据业务传入user-id取连接,取订单数据业务传入order-id取连接即可
这样的话,就能够保证同一个数据例如uid在数据库层面的执行一定是串行的
稍等稍等,服务可是部署了很多份的,上述方案只能保证同一个数据在一个服务上的访问,在DB层面的执行是串行化的,实际上服务是分布式部署的,在全局范围内的访问仍是并行的,怎么解决呢?能不能做到同一个数据的访问一定落到同一个服务呢?
四、能否做到同一个数据的访问落在同一个服务上?
上面分析了服务层service的上下游及内部结构,再一起看一下应用层上下游及内部结构
上图是一个业务应用的上下游及服务内部详细展开,细节如下:
(1)业务应用的上游不确定是啥,可能是直接是http请求,可能也是一个服务的上游调用
(2)业务应用的下游是多个服务service
(3)中间是业务应用,它又分为了这么几个部分
(3.1)最上层是任务队列【或许web-server例如tomcat帮你干了这个事情了】
(3.2)中间是工作线程【或许web-server的工作线程或者cgi工作线程帮你干了线程分派这个事情了】,每个工作线程完成实际的业务任务,典型的工作任务是通过服务连接池进行RPC调用
(3.3)最下层是服务连接池,所有的RPC调用都是通过服务连接池往下游服务去发包执行的
工作线程的典型工作流是这样的:
voidwork_thread_routine(){
Task t = TaskQueue.pop(); // 获取任务
// 任务逻辑处理,组成一个网络包packet,调用下游RPC接口
ServiceConnection c = CPool.GetServiceConnection(); // 从Service连接池获取一个Service连接
c.Send(packet); // 通过Service连接发送报文执行RPC请求
CPool.PutServiceConnection(c); // 将Service连接放回Service连接池
}
似曾相识吧?没错,只要对服务连接池进行少量改动:
获取Service连接的CPool.GetServiceConnection()【返回任何一个可用Service连接】改为
CPool.GetServiceConnection(longid)【返回id取模相关联的Service连接】
这样的话,就能够保证同一个数据例如uid的请求落到同一个服务Service上。
五、总结
由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了),可能通过两个小的改动解决:
(1)修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
(2)修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的
六、遗留问题
提问:取模访问服务是否会影响服务的可用性?
答:不会,当有下游服务挂掉的时候,服务连接池能够检测到连接的可用性,取模时要把不可用的服务连接排除掉。
提问:取模访问服务与 取模访问DB,是否会影响各连接上请求的负载均衡?
答:不会,只要数据访问id是均衡的,从全局来看,由id取模获取各连接的概率也是均等的,即负载是均衡的。
提问:要是数据库的架构做了主从同步,读写分离:写请求写主库,读请求读从库也有可能导致缓存中进入脏数据呀,这种情况怎么解决呢(读写请求根本不落在同一个DB上,并且读写DB有同步时延)?
答:下一篇文章和大家分享。
http://oicwx.com/detail/830856
- 大小: 8.3 KB
- 大小: 8.6 KB
- 大小: 6.8 KB
- 大小: 13.4 KB
- 大小: 36.7 KB
- 大小: 34.5 KB
分享到:
相关推荐
【Redis缓存与数据库一致性解决方案】 在高并发的业务环境中,数据库常常成为性能瓶颈。因此,引入Redis作为缓存来分担数据库的压力是常见的优化手段。然而,这种架构会带来缓存与数据库之间的一致性问题。主要问题...
后台服务代码架构:项目实际应用中 redis 缓存与数据库一致性问题解决 一、需求起因 在项目中,出现了 redis 缓存与数据库一致性问题。假设先写数据库,再淘汰缓存:如果第一步写数据库操作成功,第二步淘汰缓存...
总的来说,解决缓存和数据库一致性问题并非一蹴而就,需要根据具体的应用场景和业务需求来设计合适的策略。在保证数据一致性的前提下,还需要充分考虑到系统的性能和可用性,以及对业务场景的影响。只有这样才能在...
如何保证缓存和数据库数据的一致性 知识点: 缓存:读、写、更新、删除,这些操作可能失败; 数据库:读、写、更新、删除,这些操作可能失败。 面试题剖析 一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的...
Sql Server 实用操作 - 数据库一致性检测工具 (DBCC) DBCC (Database Consistency Checker) 是一种强大的数据库一致性检测工具,旨在帮助开发者和数据库管理员检测和解决数据库中的问题。.Sql Server 中的 DBCC ...
在分布式系统中,由于网络分区的不可避兔性,通常需要在一致性与可用性之间做出权衡。例如,采用AP(可用性、分区容忍性)策略的系统可能会允许暂时的数据不一致,以保证服务的连续性;而CP(一致性、分区容忍性)...
双写一致性模型在写入数据时同时更新数据库和缓存,读取时先尝试从缓存获取,如果不存在则从数据库读取并填充缓存。为确保写入一致性,可以使用异步消息队列来协调数据库和缓存的更新。 7. **缓存预热**: 应用...
总之,保证分布式缓存与数据库的强一致性是一项挑战,需要通过设计合理的数据同步策略和处理机制来平衡一致性、可用性和分区容忍性。上述方案提供了一种可能的实践路径,但实际应用中还需根据具体业务需求和系统性能...
分布式缓存是现在很多分布式应用中必不可少的组件,但是用到了分布式缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题? Cache Aside Pattern 最...
缓存和数据库的一致性解决方案: 1. 问题背景 2. 缓存更新策略 3. 改进方案 4. Cache Aside Pattern
数据库事务主要用于保证数据库层面的数据一致性,但它无法解决缓存与数据库之间的同步问题,因为事务处理的是数据库内部的事务性操作,而不是缓存。 4. **使用队列**: 通过队列可以控制读写操作的顺序,避免数据...
数据库和缓存双写一致性是分布式系统中的常见挑战,它涉及到如何在更新数据库的同时保持缓存的正确性。本文主要探讨两种常见的策略及其潜在的问题和解决方案。 首先,我们来看第一种策略,即“先删缓存,再更新...
在当代信息技术架构中,缓存与数据库的一致性问题一直是一个热门的研究领域。对于那些需要即时数据反馈的系统来说,缓存机制的引入显著提升了数据读取的速度和系统的响应能力,但同时,如何确保缓存与数据库之间的强...
趣说 | 数据库和缓存如何保证一致性?.doc
具体来说,分布式系统缓存一致性的设计与应用涵盖以下几个重要知识点: 1. 缓存一致性的定义及其在分布式系统中的重要性。 2. 缓存一致性理论模型的分类,包括严格、顺序和因果一致性模型。 3. 缓存数据结构的设计...
六、缓存更新与一致性 为了保持缓存和数据库的一致性,需要处理以下问题: 1. 缓存失效:当数据库中的数据被更新时,需要标记对应的缓存条目为失效,以便下次访问时重新加载。 2. 缓存穿透:查询的数据在数据库中...
3. **缓存更新策略**:写时复制、反向代理、监听数据库变更等方式,保持缓存与数据库的一致性。 4. **复制与备份**:通过数据复制提高容错性,常见的有主从复制、多副本等。 5. **并发控制**:使用锁机制或无锁算法...