前两周,公司某个服务准备的数据脏了,导致线上出问题。那么事后就要反思讨论,由于数据存放在 Redis 中,有同学提出了双 Buffer 解决方案:
1. 两套 Redis 集群,Online 提供线上访问,Offline 用来灌数据
2. Offline 数据准备好后,修改线上配置,切换 Online/Offline 访问
首先,这个解决方案很好,很多公司在用,比如”Golang在京东列表页实战“这个分享也提到过,据传 Baidu 也大量使用双 Buffer. 但是对于小公司,Redis 本身就浪费内存,再搞双 Buffer 肯定不易接受。那么,我们要思考,这次事故的本质其实是多版本控制问题。
广告服务的版本控制
最近在做广告系统,其中涉及到了关于个人画像的数据准备,我们就使用了一个版本控制的概念,以防数据出错,可以快速回退。
1. 数据库 version 表,存放当前线上使用的版本号,采用 unixtimestamp
2. 数据提供方,将数据带上版本号(当天0点时间戳), 灌到数据库中
3. 在灌的过程中,由于 version 表还是老的时间戳,所以线上还是访问老的数据
4. 提供方灌完全量数据,较验新数据无误后,将 version 版本设为当天0点时间戳
5. 提供方清除数据库过期数据,比如 7 天前的历史数据
这是一个最简单的版本控制,数据并发读串行写,用一条 SQL 总结:
SELECT * from profile join version where profile.ver = version.ver and profile.id=xxx and profile.ver=xxxx
这个策略实际上是 CAS 的一个变种。
CAS
CAS 意为 Compare And Swap 来自 Memcache 术语,对于 MC 同一条 Key X 的并发方问两个客户端 A和B 同时取得 X, 各自处理不同逻辑后,再写回 MC 时,数据最终是 AX 还是 BX不得而知,此时 CAS 就派上用场了
1. Get Key 时,Memcache 返回数据和一个 uint64 版本号
2. 客户端处理逻辑,回写 MC 时带上该版本号
3. MC 检查当前 Key 版本号,如果和传来的不一致,认为这是脏数据,返回失败
4. 客户端检查返回状态,如果失败可以选择再从 1 开始重试
我们线上的缓存系统采用 Tewmproxy + Redis 实现,虽然 Redis 是单线程工作,天然保证了操作的原子性,但是在并发面前依然存在这个问题,显然 Redis 实现的 Cache 集群是不具有 CAS 功能。至今没有 Bug 报出也是神奇了。
另外即然提到了,线上 Redis Cache 集群还有个大的问题,Twemproxy 开启 Auto_eject_host 功能,踢出的瞬间部份数据重新分布,扔然会出现脏数据。
其它 DB 的版本控制
做为 DBA 肯定想到了其它数据库系统的多版本控制
BeansDB:来自 Dynamo 模型,典型的最终一致系统 R + W > N, 比较暴力,对于图片文本一致性不高的场景比较高效。为了解决并发提交的一致性问题,Beansdb 采用版本号 + 修改的时间戳来标识数据。提交的策略是:高版本号覆盖低版本号,新数据覆盖旧数据。最终数据同步由官方提供的 Sync.py 脚本完成。
MySQL&PG: MVCC 一般都是在行上存在多个隐藏字段,这些字段不外乎代表本行的事务号和回滚指针,事务号就可以理解为版本号,通过与自已的版本号做比较,来决定对数据的访问权限。MySQL 将事务修改前的值存放到 Undo Log 中,将事务内容写到 Redo Log,其它人想读老版本数据就去 Undo,而 Redo Log 用于崩溃恢复,所谓的前滚。PG 实现的不太一样,每次事务都是追加,回滚也不删除无效数据,不存在 Redo Log 的概念,至于事务的提交状态,则保存到了 commit log中,过期的事务或数据由 vaccum 进程清理。