读写分离下的数据一致问题
一、问题描述
在高性能设计方面,一般会引入多线程,提高CPU利用率;异步化,减少IO操作和同步等待;其他的还有:缩短链路、柔性事务等,在数据库层面,则有分库分表、读写分离等。
数据库是很宝贵的资源,对于一般的读多写少的应用,通常会采用用读写分离,主库负责数据的新增、更新、删除等操作,从库负责查询操作,这样能够有效的避免数据库行锁,提高数据库操作性能,规避数据库瓶颈。
一般的读写分离方案中,数据库架构模式会采用一主多从模式,读从库的数据需要从主库同步过来,这时是有一定时间延迟的,会导致主从数据库数据不一致,从而影响到正常业务的读请求数据不一致,出现业务异常。
以下是数据库读写分离的示意图:
二、解决方案
问题是:由于引入数据库读写分离,主从数据库数据同步延迟,引起读请求数据不一致,那可以如何解决呢?
2.1、方案1:强制路由读主库
服务端提供强制查询主库和正常查询接口读库的两个接口,供业务方根据不同场景进行调用。此方案需要梳理现有业务流程,识别出来哪些流程需要强制读主库。
(1)、Sharding-JDBC
基于暗示(Hint)的强制主库路由
T0 - 线程开始
T1 - 线程执行 查询 sql A
T2 - 执行 sql B之前调用:
HintManager.getInstance().setMasterRouteOnly();
T3 - 执行 查询 sql B
T4 - 执行 sql B 之后调用:
HintManager.getInstance().close();
T5 - 执行 查询 sql C
sqlA 和 sqlC会执行查从库,sqlB会查询主库
(2)、Mycat
利用Mycat的应用强制走主库的特性,一个查询SQL语句以/*balance*/注解来确定其是走读节点还是写节点。
//强制走写:
/*#mycat:db_type=master*/ select * from order_record
/*!mycat:db_type=master*/ select * from order_record
这样SQL就会限制读主库
2.2、方案2:“选择性”的强读主库
方案2,使用“选择性”的强制读主库,而这种选择性是由系统自动来选择的,加入Redis,针对这种需要强一致性的情况,进行打标,比如订单号打标,有打标的订单进行查询主库
(1)、有新增、编辑、更新动作时,打标订单号
(2)、数据正常写入主库
(3)、查询时,先请求Redis,查看数据是否有打标
(3.1)、有打标,强制读主库
(3.2)、没有打标,请求正常查询链路
(4)、返回查询信息
此方案复杂度较高,涉及数据打标逻辑,需要收敛打标逻辑,避免散落到各个业务流程中。但是该方案能充分的利用读写分离的机制,最大限度的利用从库的资源。
2.3、方案3:同步写时返回需要信息
存在一些业务流程,执行完新增/编辑/更新等动作之后,需要显性的展示操作结果,比如创建订单后显示订单详情,需要展示订单金额,所以下单后同步返回订单金额,避免立即查询订单详情,又查询不到的情况。具体方案图示如下:
该方案局限性较大,只适用于部分场景,不具备通用性,但是能减少部分读请求,提高客户体验
三、总结
方案1:强制读主库,利用DBProxy的特性,强制读主库。难点是需要梳理业务流程,识别出来那些场景需要强制读主库,然后进行改造,确定是不能充分的利用读写分离的特性,避免数据库瓶颈。
方案2:“选择性”的读主库,对写请求进行打标,查请求根据打标情况,有选择性的进行读主库,难点是需要收敛所有写请求的打标逻辑,优点是系统自动化识别,充分利用读写分离的特性。
方案3:同步写时返回需要信息,针对某些特定场景,在同步写请求时,就同步返回业务所需要的信息,避免读请求,缺点是方案不具备通用性,但是能减少读请求,提高客户体验。
目前设计出来的有这三种方案,具体落地需要结合项目的阶段和资源情况,再选择具体方案进行落地。