互联网一致性架构设计 -- DB和Cache一致性
需求分析
下面两种情况会出现脏数据:
单库情况下
服务层的并发读写,缓存与数据库的操作交叉进行,这种情况虽然少见,但理论上是存在的,后发起的请求B在先发起的请求A中间完成了。
1. 请求A发起一个写操作,第一步淘汰了cache,然后这个请求因为各种原因在服务层卡住了(进行大量的业务逻辑计算,例如计算了1秒钟),如上图步骤1
2. 请求B发起一个读操作,读cache,cache miss,如上图步骤2
3. 请求B继续读DB,读出来一个脏数据,然后脏数据入cache,如上图步骤3
4. 请求A卡了很久后终于写数据库了,写入了最新的数据,如上图步骤4
主从同步
读写分离的情况下,读从库读到旧数据,这种情况请求A和请求B的时序是完全没有问题的,是主动同步的时延(假设延时1秒钟)中间有读请求读从库读到脏数据导致的不一致。
1. 请求A发起一个写操作,第一步淘汰了cache,如上图步骤1
2. 请求A写数据库了,写入了最新的数据,如上图步骤2
3. 请求B发起一个读操作,读cache,cache miss,如上图步骤3
4. 请求B继续读DB,读的是从库,此时主从同步还没有完成,读出来一个脏数据,然后脏数据入cache,如上图步4
5. 最后数据库的主从同步完成了,如上图步骤5
不一致的原因
1. 单库情况下,服务层在进行1s的逻辑计算过程中,可能读到旧数据入缓存
2. 主从库+读写分离情况下,在1s钟主从同步延时过程中,可能读到旧数据入缓存
建议:先淘汰缓存,再更新数据库
单库情况的优化
自己重写数据库连接池,例如根据userId取模,得到唯一的数据库连接,如何做到“让同一个数据的访问串行化”,只需要“让同一个数据的访问通过同一条DB连接执行”就行。
如何做到“让同一个数据的访问通过同一条DB连接执行”,只需要“在DB连接池层面稍微修改,按数据取连接即可”?
获取DB连接的
CPool.GetDBConnection()【返回任何一个可用DB连接】
改为
CPool.GetDBConnection(longid)【返回id取模相关联的DB连接】
1. service的上游是多个业务应用,上游发起请求对同一个数据并发的进行读写操作,上例中并发进行了一个uid=1的余额修改(写)操作与uid=1的余额查询(读)操作。
2. service的下游是数据库DB,假设只读写一个DB。
3. 中间是服务层service,它又分为了这么几个部分
(1) 最上层是任务队列
(2) 中间是工作线程,每个工作线程完成实际的工作任务,典型的工作任务是通过数据库连接池读写数据库
(3) 最下层是数据库连接池,所有的SQL语句都是通过数据库连接池发往数据库去执行的
4. 当用uid=1写数据库时,正在使用数据库连接池中的连接1。
5. 此时用uid度数据库,同样要使用连接1,这样读操作就会进行等待,要前面写数据完毕,释放数据库连接后才能读数据。
拓展
能否做到同一个数据的访问落在同一个服务上?
重新修改结构如下:
获取Service连接的
CPool.GetServiceConnection()【返回任何一个可用Service连接】
改为
CPool.GetServiceConnection(longid)【返回id取模相关联的Service连接】
1. 业务应用的上游不确定是啥,可能是直接是http请求,可能也是一个服务的上游调用
2. 业务应用的下游是多个服务service
3. 中间是业务应用,它又分为了这么几个部分
(1)最上层是任务队列【或许web-server例如tomcat帮你干了这个事情了】
(2)中间是工作线程【或许web-server的工作线程或者cgi工作线程帮你干了线程分派这个事情了】,每个工作线程完成实际的业务任务,典型的工作任务是通过服务连接池进行RPC调用
(3)最下层是服务连接池,所有的RPC调用都是通过服务连接池往下游服务去发包执行的
4. 当请求Service层时,根据uid来取模,决定使用哪个Service的连接
主从同步情况下的优化
既然旧数据就是在那1s的间隙中入缓存的,是不是可以在写请求完成后,再休眠1s,再次淘汰缓存,就能将这1s内写入的脏数据再次淘汰掉呢?
方法一
1. 先淘汰缓存
2. 再写数据库(这两步和原来一样)
3. 休眠1秒,再次淘汰缓存
缺点:所有的写请求都阻塞了1秒,大大降低了写请求的吞吐量,增长了处理时间,业务上是接受不了的。
方法二
1. 先淘汰缓存
2. 再写数据库(这两步和原来一样)
3. 不再休眠1s,而是往消息总线esb发送一个消息,发送完成之后马上就能返回
缺点:需要业务线的写操作增加一个步骤,这就是我们所谓的代码入侵
方法三
业务线的代码就不需要动了,新增一个线下的读binlog的异步淘汰模块,读取到binlog中的数据,异步的淘汰缓存。
总结
单库
由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了),可能通过两个小的改动解决:
1. 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
2. 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的
主从数据库
在“异常时序”或者“读从库”导致脏数据入缓存时,可以用二次异步淘汰的“缓存双淘汰”法来解决缓存与数据库中数据不一致的问题,具体实施至少有三种方案:
1. timer异步淘汰(本文没有细讲,本质就是起个线程专门异步二次淘汰缓存)
2. 总线异步淘汰
3. 读binlog异步淘汰