分布式数据一致性的探讨

很多分布式系统都会用缓存,本地缓存或者NoSQL,亦或者两者都上。在笔者认为,只要用到缓存系统,就必然会存在数据不一致的时间段,这个时间有长有短而已,当然最终数据是要一致的,也就是达成最终一致性(eventually consistency),这里简单聊聊笔者的理解。

单数据库

单数据库的读取问题,包含了脏读,脏写,不可重复读,幻读等等。这和数据库的隔离级别有关,因为这篇文章主要聊的是分布式系统的数据一致性,所以这里先留坑,只要知道单数据库也可能存在数据不一致的情况。

多份副本

这是最常见的解决方案,一般称为主从(master/slave)复制。主从复制的基本概念是正常情况下,主库只接受写操作,从库从主库复制数据,通过拉取预写日志或者复制行等方式复制数据。有单领导者,多领导者,无无领导者几种情况,先说一说单主复制的模式,这里先不考虑NoSQL缓存和分区。
我在抖音上更新头像,向抖音服务器发出请求,服务器收到通知,往主库更新我的新头像,这个时候服务器有两个选择,一个是主库写入数据成功就给我发通知告诉我操作成功,另一个是等待从库从主库复制数据成功了再给我发通知。第一个方案速度快,只要写一个库就返回,肯定比多个快嘛,但是万一这时候主库挂了,我再刷新抖音,发现我刚更新的头像没了。这时候作为用户我就不开心了,抖音你告诉我更新成功了,为什么查看的时候却没有,你这个大骗子。这时候第二种方案的优势就出来了,只有从库复制成功了才会返回通知,如果主库在复制的时候挂掉了,从库没有复制到数据就返回失败,作为用户的我只当网络问题,再更新一次好了。但是这样的问题在于返回结果慢,如果从库很多呢,还遍布全国各地呢,更新一个头像需要几分钟,那这体验太差了。所以正常应该是一个从库同步复制,其余从库异步复制,采取这样的的折中方案来保证效率以及数据安全。恩,似乎很完美,曾经笔者刚学分布式的时候也这样认为,直到接下来的问题出现。
我更新完头像,服务器将我的头像更新到主库A,从库B上,返回结果成功。我随即去查看头像,很正常的对吧,向抖音服务器发请求,服务器收到请求将我的读请求发到了从库C上!靠,抖音这个骗子,又没有。这就是读己之写一致性(read-your-writes consistency)问题。有几个解决方案:

  1. 应用层写逻辑,让这种情形只从主库读取数据。譬如说记录刚才更新的时间戳,如果时间间隔太短就从主库去读,时间间隔长就从从库。亦或者读自己的数据就从主库获取,读取其他用户的数据就在从库获取。但是如果更新的内容很多很复杂,应用层逻辑会变的复杂,主库的读请求也会很多,增加负担
  2. 本地缓存,都不需要和服务器发请求的,效率高。这是笔者遇到这个问题的第一反应。不过这也有问题,如果用户在网页更新头像,随即到手机端查看呢,web端的缓存app可没有
  3. 服务器层增加处理逻辑,譬如对用户ID取模,映射到某个指定从库上,同步复制的从库以及后期读取数据都从这个指定从库上。这也是笔者推荐的方式。因为这样也能避免用户多次读取,请求被分配到不同从库导致新数据有时有有时无的情况,形成单调读(Monotonic reads)。
  4. 但是这几种方式都不能解决一个问题,如果我更新完情侣头像,立刻让女友去看,结果女友读取的是从库C,没有读取到最新数据,那么我完了,全局终。

缓存

为了减少数据库的访问压力,分布式系统很多都会有一份内存数据库做缓存,存放热数据,我们拿微博大V来举例。
笔者猜一下微博的流程,微博大V发了一个微博,服务器写完主库从库,还会存放一份复制到Redis集群(假设)上,因为follow的人太多,这么多的读取请求必须分流(好像记得一些热点数据还会主动push到信息流上,热门话题应该是这样)当然笔者这样的小透明是不需要放到Redis上的。
问题在于数据库更新后,对缓存的处理上。笔者公司的系统的做法是,更新完数据库,再将新数据写入Redis,不关心成功与否,后期当然会在其他地方保证数据一致。那么如果写入Redis失败了,其他用户甚至自己再去查看数据,服务器首先会去Redis拉取数据,因为Redis上有对应的数据,所以服务器读取并返回给用户,这个时候数据库是新数据,Redis是旧数据,数据不一致就发生了。
这个问题笔者看过一个解决方案,就是先删除缓存,再更新数据库,这样即使更新数据库失败了,因为缓存没有数据,其他用户读取缓存没有,去数据库读取的也是旧数据,不会造成数据不一致。但是在高并发的情况下,有可能会发生B用户在A用户删除缓存更新数据库的中间,完成读取数据库旧数据,并在A用户写新数据回缓存后,再次写入旧数据到缓存里的情况,也就是数据库是新数据,Redis是旧数据。
笔者后来想过这个方案还有一个问题,回到微博大V发微博的例子上,这个时候写操作肯定不是高并发,大V更新了微博,服务器如果按照刚才的方案先删除缓存再写数据库, 如果缓存失效了,那么对数据的读取都会压倒数据库上,认为造成了缓存穿透的严重后果,这个时候宁愿不一致,而不要缓存不存在的情况发生。
对于很热门的数据,服务器还会采取本地缓存的方案,可以采用发布订阅版本号的方式。如果大V先发了微博,服务器在本地缓存了一份复制并订阅这条微博。大V编辑微博内容,主服务器在写入数据库成功后发布这条微博的新版本号,副服务器就知道自己本地的内容过期了,需要去Redis或者数据库更新内容。甚至主服务器完全可以发布微博编辑后的内容,这样比其他服务器还要去拿缓存或者读取数据库还要快。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值