一、需求起因
假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致【如下图:db中是新数据,cache中是旧数据】。
假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败【如下图:cache中无数据,db中是旧数据】。即使写数据库失败,数据库中依然是旧值,当应用程序从缓存中取值时,发现没有,就会从数据库中查询,然后存放到缓存中并返回给用户。这时数据库与缓存中的数据是一致的。但是会导致另一个问题:数据库与缓存中的数据都是旧数据。
结论:先淘汰缓存,再写数据库,最后再将数据写入缓存。
二、数据不一致原因
先操作缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致。
写流程:
1)先淘汰cache
2)再写db
读流程:
1)先读cache,如果数据命中则返回
2)如果数据未命中则读db
3)将db中读取出来的数据入缓存
什么情况下可能出现缓存和数据库中数据不一致呢?
在分布式环境下,数据的读写都是并发的,上游有多个应用,通过一个服务的多个部署(为了保证可用性,一般是部署多份的),对同一个数据进行读写,在数据库层面并发的读写并不能保证完成顺序,也就是说后发出的读请求很可能先完成(读出脏数据):
a)发生了写请求A,A的第一步淘汰了cache(如上图中的1)
b)A的第二步写数据库,发出修改请求(如上图中的2)
c)发生了读请求B,B的第一步读取cache,发现cache中是空的(如上图中的步骤3)
d)B的第二步读取数据库,发出读取请求,此时A的第二步写数据还没完成,读出了一个脏数据放入cache(如上图中的步骤4)
即在数据库层面,后发出的请求4比先发出的请求2先完成了,读出了脏数据,脏数据又入了缓存,缓存与数据库中的数据不一致出现了。
三、问题解决思路
能否做到先发出的请求一定先执行完成呢?常见的思路是“串行化”
上图是一个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)1个服务有多个工作线程,串行弹出的任务会被并行执行
(2)1个服务有多个数据库连接,每个工作线程获取不同的数据库连接会在DB层面并发执行
2、提问:假设服务只部署一份,能否保证任务不并发执行? 答:不行,原因同上
3、提问:假设1个服务只有1条数据库连接,能否保证任务不并发执行?
答:不行,因为
(1)1个服务只有1条数据库连接,只能保证在一个服务器上的请求在数据库层面是串行执行的
(2)因为服务是分布式部署的,多个服务上的请求在数据库层面仍可能是并发执行的
4、提问:假设服务只部署一份,且1个服务只有1条连接,能否保证任务不并发执行?
答:可以,全局来看请求是串行执行的,吞吐量很低,并且服务无法保证可用性。
其实第4问也不能保证串行执行。因为在service服务中有多个工作线程,如果修改数据与读取数据分别由两个线程来完成,在修改数据的过程中,有两步工作要做:第一步是先把缓存中的数据清除,第二步是修改数据库中的数据。假设线程1执行修改数据的工作,在执行完第一步后,用完了CPU时间片,CPU开始运行线程2执行读取数据的工作,线程2先查询缓存,发现为空,就去查询数据库(注意:这时数据库中的数据还是旧数据),然后更新到缓存并返回给用户。CPU又开始运行线程1继续执行第二步修改数据库中的数据,这时数据库中是新数据,而缓存中是旧数据,就会导致缓存与数据库中的数据不一致。
除非service服务中只有一个线程,或者修改数据的方法与读取数据的方法使用同一把锁(如果是集群服务,需要使用分布式锁),这样就可以使得修改数据的方法与读取数据的方法是串行执行,要么先修改数据后读取数据,要么先读取数据后修改数据。
结论:对于同一条数据的修改方法和读取方法加上同一把分布式锁(集群服务需要分布式锁),在修改数据的方法中,先从缓存中清除掉该条数据,再将新数据写入数据库,其次将新数据写入缓存,最后释放锁。