冗余表的架构设计就是牺牲空间一份数据存多张表,可以通过不同索引查询提高效率的一种架构思想。
背景
互联网很多业务场景的数据量很大,此时数据库架构要进行水平切分,水平切分会有一个patition key,通过patition key的查询能够直接定位到库,但是非patition key上的查询可能就需要扫描多个库了。
例如订单表,业务上对用户和商家都有订单查询需求,如果用buyer_id来分库,seller_id的查询就需要扫描多库。如果用seller_id来分库,buyer_id的查询就需要扫描多库。这类需求,为了做到高吞吐量低延时的查询,往往使用“数据冗余”的方式来实现,同一个数据,冗余两份,一份以buyer_id来分库,满足买家的查询需求;一份以seller_id来分库,满足卖家的查询需求。
实现
服务同步写
顾名思义,由服务层同步写冗余数据,先后向两个表中同时插入数据。
优点:
- 不复杂,服务层由单次写,变两次写
- 数据一致性相对较高(因为双写成功才返回)
缺点:
- 请求的处理时间增加(要插入次,时间加倍)
- 数据仍可能不一致,例如第二步写入T1完成后服务重启,则数据不会写入T2
服务异步写
数据的双写并不再由服务来完成,服务层异步发出一个消息,通过消息总线发送给一个专门的数据复制服务来写入冗余数据。
优点:
- 请求处理时间短(只插入1次)
缺点:
- 系统的复杂性增加了,多引入了一个组件(消息总线)和一个服务(专用的数据复制服务)
- 因为返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
- 在消息总线丢失消息时,冗余表数据会不一致
线下异步写
数据的双写不再由服务层来完成,而是由线下的一个服务或者任务来读取数据的log来完成。
优点:
- 数据双写与业务完全解耦
- 请求处理时间短(只插入1次)
缺点:
- 返回业务线数据插入成功时,数据还不一定插入到T2中,因此数据有一个不一致时间窗口(这个窗口很短,最终是一致的)
- 数据的一致性依赖于线下服务或者任务的可靠性
先写正表还是反表
上述三种方案各有优缺点,但不管哪种方案,都会面临“究竟先写T1还是先写T2”的问题,对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:
【如果出现不一致】,谁先做对业务的影响较小,就谁先执行,需要根据业务逻辑来做处理。
比如还是对订单的业务,用户下单时,如果“先插入buyer表T1,再插入seller冗余表T2”,当第一步成功、第二步失败时,出现的业务影响是“买家能看到自己的订单,卖家看不到推送的订单”,相反,如果“先插入seller表T2,再插入buyer冗余表T1”,当第一步成功、第二步失败时,出现的业务影响是“卖家能看到推送的订单,卖家看不到自己的订单”,由于这个生成订单的动作是买家发起的,买家如果看不到订单,会觉得非常奇怪,并且无法支付以推动订单状态的流转,此时即使卖家看到有人下单也是没有意义的,因此,在此例中,应该先插入buyer表T1,再插入seller表T2。
保证数据的一致性
不管哪种方案,因为两步操作不能保证原子性,总有出现数据不一致的可能,基本解决方案。
线下扫面正反冗余表全部数据
线下启动一个离线的扫描工具,不停的比对正表T1和反表T2,如果发现数据不一致,就进行补偿修复。这个是我们最容易想到的方法,也是最消耗资源的方法。
优点:
- 比较简单,开发代价小
- 线上服务无需修改,修复工具与线上服务解耦
缺点:
- 扫描效率低,会扫描大量的“已经能够保证一致”的数据
- 由于扫描的数据量大,扫描一轮的时间比较长,即数据如果不一致,不一致的时间窗口比较长
线下扫描增量数据
有没有只扫描“可能存在不一致可能性”的数据,而不是每次扫描全部数据,每次只扫描增量的日志数据,就能够极大提高效率,缩短数据不一致的时间窗口。
当然,我们还是需要一个离线的日志扫描工具,不停的比对日志log1和日志log2,如果发现数据不一致,就进行补偿修复
优点:
- 虽比方法一复杂,但仍然是比较简单的
- 数据扫描效率高,只扫描增量数据
缺点:
- 线上服务略有修改(代价不高,多写了2条日志)
- 虽然比方法一更实时,但时效性还是不高,不一致窗口取决于扫描的周期
实时线上“消息对”检测
有些系统要求比较高,需要实现实时的检测,所以就不能使用日志了,需要使用消息系统。假设正常情况下,msg1和msg2的接收时间应该在3s以内,如果检测服务在收到msg1后没有收到msg2,就尝试检测数据的一致性,不一致时进行补偿修复
优点:
- 效率高
- 实时性高
缺点:
- 方案比较复杂,上线引入了消息总线这个组件
- 线下多了一个订阅总线的检测服务