大家好!我是楼下的小黑哥,我又来了~本文分享一个之前加入公司后在公司第一个发布项目中遇到的一个问题,数据库读写分离的一个坑。前言事情是这样的。刚入职的时候接到过这样一个业务需求:每个支付通道在支付失败的时候都会返回一个特定的错误码。业务需要将通道特定的错误代码转换为内部错误代码。这样我们就可以统一向外界返回自己的错误码了。这个要求其实并不难。当时设计的系统架构如下:添加规则的过程简单分为三步:业务人员通过管理后台添加映射规则;在数据库中添加和修改这条映射规则;删除缓存。这里之所以加缓存,是因为这个场景下每次支付都需要用到缓存。使用缓存可以避免每次都从数据库中读取,提高读取速度。后续的支付请求业务流程如下:数据库读写分离-用户操作当缓存中的映射规则不存在时,会查询数据库,然后加载到缓存中。如果缓存中的映射规则已经存在,则直接使用缓存中的映射规则。这个业务流程其实比较简单。当时在测试环境测试没有问题,但是在后续发布线上环境的时候遇到了奇怪的问题。“添加新规则后,映射规则有一段时间没有生效,查看日志,发现查询数据库时,没有数据。”这很奇怪。日志显示添加成功,但是查询没有数据。但是过了一会儿,查询又有数据了。检查代码后发现没有问题。第二天上班的时候问了同事才知道问题的原因:原来线上数据库采用主从架构,数据读写分离,数据查询使用从库.数据写入直接在主库上操作,然后同步到从库。“由于数据库同步有延迟,这样会导致在数据同步期间,数据不同步,主从数据不一致,从库查询不到最新的数据。”如果你之前的数据库系统架构是单库或者主备结构,当你第一次切换到数据读写分离架构时,大概率会踩到这个坑。一、数据库系统架构的发展我们先来了解一下数据库系统架构,最后是如何解决主从同步延迟导致的数据不一致。1、主备架构业务发展初期,数据访问量小。这时候我们可以直接采用单库架构。但是,我们一般不会使用上面的架构,因为存在单点问题。如果数据库出现故障,这段时间业务将不可用。除了等待重启,我们没有其他解决办法。所以我们会添加一个备库来实时同步主库的数据。主备架构一旦“主库”出现故障,手动将“主机”踢下线,将“备”改为“主机”继续提供服务。这种架构易于部署和维护,业务开发不需要任何修改。但是,缺点也很明显。备库只有在主库出现问题时才会启用,存在一定的资源浪费。2.主从架构随着业务的发展,请求量和数据量不断增加,业务变得更加复杂,数据很快就会达到瓶颈。由于大部分业务读多写少,数据库读取最容易成为系统瓶颈。这时候,我们可以提高阅读的表现。这时候我们可以采用的方案是增加slave实例,主从同步,数据读写分离。可以看出这种架构和主从架构没有区别。主要区别在于,在主从架构下,从库和主库一样,需要一直工作。主库提供写服务,从库只提供读服务。如果后续的阅读压力还是太大,我们也可以通过增加从库的数量来横向扩展阅读能力。主从架构虽然帮助我们解决了读取的瓶颈,但是由于主从之间需要进行数据同步,自然存在一定的延迟。在这个延迟窗口期间,从库中读取只能读取到一个旧数据,这就是上述案例中问题的真正原因。接下来,让我们看看如何优化这种情况。二、主从延时解决方案1、忍大法第一个解决方案很简单,没有别人,不管你是谁,不看也没关系。此时业务不需要做任何改造,hello,me,orher~如果业务对数据一致性要求不高,可以采用这种方案。2、数据同步写入方案主从数据同步方案一般采用异步的方式同步到备库。我们可以修改成同步方案,主从同步完成,主库上的写就可以返回了。业务系统发起写操作,数据写入主库;写请求需要等待主从同步完成后返回;从从库读取数据,主从同步完成后可以读取最新的数据。在这个方案中,我们只需要修改数据库之间的同步配置,业务层不需要修改,比较简单。“但是,由于主库需要等待主从完成,写请求的延迟会增加,吞吐量会下降。”这对于目前的在线业务来说可能是无法接受的。3、选择性强制读master对于需要强一致性的场景,我们可以对所有的读请求都操作master库,这样“读写都在主库”,不会出现不一致的情况。这类方案的业务层需要改造,而且是强制读master,改造难度相对较小。但是这种方案浪费了另外一个数据库,增加了主库的压力。4.中间件选择路由方式该方案需要使用一个中间件,所有的数据库操作都是先发送给中间件,再由中间件分发给相应的数据库。此时流程如下:写请求,中间件会发给主库,记录此时写请求的key(操作表加主键等);读请求,如果此时key存在,则路由到主库;经过一定时间(经验值),中间件认为主从同步完成,删除这个key,后续读取将从从库读取。该方案可以保持数据读写的一致性。但是在系统架构中增加了一个中间件,整体复杂度变高,业务开发变得更加复杂,学习成本也比较高。5.缓存路由大法这个方案和中间件方案的过程类似,但是改造成本比较低,不需要增加中间件。此时流程如下:向主库发送写请求,同时缓存操作的key,缓存过期时间设置为主从延时;读请求首先判断缓存是否存在:如果存在,说明刚刚发生了写操作,读请求去操作主库;如果不存在,说明最近没有发生写操作,读请求是从库中操作的。这种方案比中间件方案成本更低,但是我们此时引入了一个缓存组件,所有的读写之间多了一个缓存操作。3.总结我们引入了主从架构和数据读写分离。目的是为了解决业务快速发展、请求量增大、并发量增大带来的数据库读取瓶颈。然而,当引入一种新的架构来解决问题时,必然会带来另一个问题。数据库读写分离后,主从延迟会导致数据不一致。为了解决主从延迟和数据不一致的情况,我们可以采用以下方案:耐心法;数据库同步写入方案;选择性强制读硕士;中间件选择路由方式;缓存路由方法。上述方案各有优点,当然也有相应的缺点。我们需要根据自己的业务情况选择相应的解决方案。
