当前位置: 首页 > 科技观察

终于明白为什么要“库表分离”了!

时间:2023-03-18 01:22:27 科技观察

[.com原稿]随着互联网行业的蓬勃发展,互联网应用产生的数据也与日俱增。大量的交易记录和行为记录产生,它们的存储和分析是我们需要面对的问题。图片来自Pexels。比如:一张表有几百万甚至几千万条数据。“分表分库”成为解决上述问题的有效工具。今天就和大家一起来看看,如何分表分库以及期间遇到的问题。为什么分表分库的数据库数据会随着业务的发展不断增加,那么数据操作的增删改查成本也会增加。加上物理服务器的资源有限(CPU、磁盘、内存、IO等)。最终,数据库所能承载的数据量和数据处理能力都会遇到瓶颈。也就是说,需要一个合理的数据库架构来存储不断增长的数据,这就是分库分表的设计初衷。目的是减轻数据库的压力,最大限度地提高数据操作的效率。数据分表如果单表的数据量太大,比如几千万或者更多,那么在操作表的时候会增加系统的开销。每个查询都会消耗数据库中的大量资源。如果需要多表联合查询,这个缺点就更加明显了。以MySQL为例,在插入数据时,对表进行锁定,分为表锁定和行锁定。无论使用哪种加锁方式,都意味着前一条数据在操作表或行时,后面的请求都是排队的。当访问次数增加时,会影响数据库的效率。那么既然要分表,那么每张表有多少数据才合适呢?这里建议根据业务场景和实际情况来分析。一般来说,MySQL数据库的单表记录最好控制在500万条左右(这是一个经验数字)。既然需要将数据从一个表存储到多个表中,那么我们来看看下面两种分表的方式。垂直分表是将一个表中的字段(Field)按照业务划分到不同的表中。划分的数据通常是根据业务需要,比如划分出一些不常用的字段,一些长度较长的字段。一般拆分表的字段数较多。主要目的是为了避免查询时数据量大导致的“跨页”问题。一般在数据库设计之初就会考虑这种拆分,在系统上线前就考虑调整。对于已经上线的项目,做这种操作要慎重考虑。横向分表取一个表中的数据,根据关键字(例如:ID)对一个特定的数取模(或者取Hash后),得到的余数就是新表要存放的位置。使用ID取模的分表方法分配4条记录,记录ID分别为01-04。如果分配到3张表,那么取模3得到的余数为:ID:01取3取余1,存入“表1”。ID:023的模余数为2,存入“表2”。ID:033的模余数为3,存放在“表3”中。ID:043的模余数为1,存入“表1”。当然,这只是一个例子,实际情况需要对ID进行哈希处理后计算。同时,也可以根据不同表所在的不同数据库的资源来设置存储数据量。为每个表所在图书馆的资源设置权重。这样存储数据后,在访问具体的数据时,需要通过一个MappingTable获取对应的响应数据来自哪个数据表。目前流行的数据库中间件已经帮我们实现了这部分功能。也就是说,您不需要自己构建映射表。中间件帮助你在查询时实现MappingTable的功能。因此,我们这里只需要了解它的实现原理即可。MappingTable辅助子表的水平拆分。另一种情况是按照数据产生的先后顺序拆分存储。比如主表只存储最近2个月的信息,其他较旧的信息拆分到其他表中。数据按时间区分。更重要的是,数据是按服务区域区分的。数据按时间分表时,需要注意分表带来的一系列记录级问题,如Join和ID的生成、事务处理等。同时,这些表有可能需要跨数据库:Join:需要进行两次查询,在应用层合并两次查询的结果。这种方式最简单,需要在设计应用层时考虑。ID:可以用UUID,也可以用一张表来存放生成的Sequence,但是效率不高。UUID实现起来比较方便,但是比较占空间。Sequence表节省空间,但所有ID都依赖于单个表。这里介绍一个大厂使用的Snowflake方法。Snowflake是Twitter开源的分布式ID生成算法,结果是长ID。核心思想是:用41bit作为毫秒数,10bit作为机器的ID(5bit为数据中心,5bit为机器ID),12bit作为毫秒内的序号(意思是每个节点可以生成4096个ID),末尾有一个符号位,永远为0。SnowflakeSchematicSorting/Paging:当数据分配到几个水平表时,不好做排序分页或者一些集合操作。根据经验,这里有两种方法。首先对子表中的数据进行排序/分页/聚合,然后合并它们。子表中的数据先合并,再排序/分页/聚合。Transactions:存在分布式事务的可能,需要考虑补偿事务或者使用TCC(TryConfirmCancel)辅助完成。下面我们将介绍这部分的内容。数据分库说完分表,再来说说分库。每个物理数据库支持的数据是有限的,每个数据库请求都会产生一个数据库链接。当一个库不能支持更多的访问时,我们会把原来单一的数据库拆分成多个来帮助分担压力。下面介绍几种分库的原则,可以根据具体场景选择:根据不同的业务分库,这种情况下,主业务会和其他功能分开。比如可以分为订单库、账务库、评论库。数据库以冷热数据切分,按数据访问频率划分。比如近一个月的交易数据属于高频数据,2-6个月的交易数据属于中频数据,6个月以上的数据属于低频数据。根据访问数据的区域/时间范围对数据库进行分段。单个表将被划分到不同的数据库中。通常,数据分库后,每个数据库包含多个数据表,多个数据库会组成一个Cluster/Group,提高了数据库的可用性,可以实现读写分离。Master库主要负责写操作,Slave库主要负责读操作。当应用程序访问数据库时,会使用一个负载均衡代理,通过判断读写操作,将请求路由到相应的数据库。如果是读操作,请求也会按照数据库设置的权重或者平均分配。另外还有数据库健康监测机制,定时发送心跳检测数据库的健康状态。如果Slave出现问题,将启动熔断机制,停止对其的访问;如果Master出现问题,会通过选举机制选出新的Master来代替。主从数据库示意图分库扩容后的数据库会遇到数据扩容或者数据迁移。这里推荐两种数据库扩容方案。主从数据库扩展我们这里假设有两个数据库集群,每个集群有M1S1和M2S2互为主备。两个数据库集群示意图由于M1和S1互为主备,所以数据是一样的,M2和S2也是一样的。将原来的ID%2模式切换为ID%4模式,即将2个数据集群扩展为4个数据库集群。负载均衡器直接将数据路由到原来的两个S1和S2,同时S1和S2会停止与M1和M2同步数据,作为主库独立存在(写操作)。这些修改不需要重启数据库服务,只需要修改代理配置即可。由于M1M2S1S2中会存在一些冗余数据,可以在不影响数据使用的情况下,启动后台服务删除这些冗余数据。两个集群中的两个master和slave分别扩展为四个集群中的四个host。此时考虑到数据库的可用性,扩充的4个主库用于主备操作,并为每个主库建立一个对应的主库。从库上看,前者负责写操作,后者负责读操作。如果下次需要扩容,可以按照类似的操作进行。从两个集群扩展到四个集群。无需数据库主从配置的双写数据库扩容。假设有如下图所示的数据库M1和M2:扩容前的两个master数据库需要对当前的两个数据库进行扩容。展开后有如下图所示的4个库。新加入的库为M3,M4的路由方法分别为ID%2=0和ID%2=1。添加两个主库。此时新数据会同时进入M1、M2、M3、M4这四个库,使用旧数据还是会从M1和M2中获取。同时,后台服务对M1、M3、M2、M4进行数据同步。建议先进行全量同步,再进行数据校验。旧数据库为新数据库进行数据同步。数据同步完成后,四个数据库的数据是一致的。修改负载均衡代理的配置为ID%4的模式。至此扩容完成,由原来的2个数据库扩容到4个数据库。当然会存在一些数据冗余,需要像上述方案一样通过后台服务删除,删除过程不会影响业务。数据同步后Hash分段分布式事务原理架构设计结果分表分库。我们不得不考虑分布式事务。今天我们就来看看分布式事务需要记住哪两条原则。大多数CAPInternet应用程序都使用分表分库的操作。这时候业务代码很可能会同时访问两个不同的数据库,进行不同的操作。同时,这两个操作可能在同一个事务中处理。分布式系统的CAP理论由此而来,它包括以下三个属性:一致性:分布式系统中的所有数据在同一时间具有相同的值。业务代码将A记录写入01数据库的节点,01数据库将A记录同步到02数据库,业务代码从02数据库读取的记录也是A,那么两个数据库存储的数据是一致的。一致性图可用性(Availability):分布式系统中的某些节点发生故障,分布式系统仍然可以响应用户请求。假设01和02数据库同时存储A记录,由于01数据库宕机,业务代码无法从中获取数据。那么业务代码就可以从02号数据库中取出记录A了。也就是说,当节点出现问题时,数据的可用性也得到了保证。可用性图分区容忍度(Partitiontolerance):假设两个数据库节点在两个区域,两个区域之间的通信出现了问题。数据一致性达不到,就是分区的情况,我需要在C和A中选择,是的选择Availability(A)获取其中一个区域的数据。或者选择一致性(C),等待两个区域的数据同步后再获取数据。这种情况的前提是两个节点之间的通信失败。在写入01库记录时,需要锁定02库记录,防止其他业务代码被修改,直到01库记录被修改。所以此时C和A是矛盾的。你不能两者兼得。分区容错图BASEBase的原理广泛应用于大数据量、高并发的互联网场景。我们来看看包含了什么:基本可用:不会因为某个节点出现问题而影响用户请求。即使在流量激增的情况下,也会考虑限流降级的方法,保证用户的请求可用。比如,当电商系统的流量激增时,资源会转移到核心业务上,其他业务就会降级。SoftState:如果一份数据有多个副本,副本之间的同步延迟是允许的,短时间内不一致是可以容忍的。这种正在同步且尚未完成同步的状态称为软状态。最终一致性:最终一致性是相对于强一致性而言的。强一致性是保证所有数据实时一致和同步。最终一致性会容忍短时间内数据不一致,但过了这段时间数据就会一致。它包括以下几种“一致性”:①因果一致性(CausalConsistency)如果有两个进程1和2都对变量X进行操作,则“进程1”写入变量X,“进程2”需要读取变量X,然后用这个X来计算X+2。这里“进程1”和“进程2”的操作之间存在因果关系。“进程2”的计算依赖于进程1写入的X,没有X的值,“进程2”无法计算。两个进程对同一个变量进行操作②ReadYourWrites(读取你的写入)“进程1”写入变量X后,进程可以获得自己写入的值。进程写入值的同时获取值③会话一致性(SessionConsistency)如果一个会话实现读取它写入的内容。一旦数据更新,客户端只要在同一个会话中就可以看到这个更新后的值。多个进程需要在同一个会话中看到相同的值④单调写一致性(MonotonicWriteConsistency)“进程1”如果有3个操作分别是1、2、3。“进程2”分别有两个操作1和2。当进程向系统发出请求时,系统会确保进程中的操作按照执行的顺序执行。多进程、多操作通过队列方式执行分布式事务方案。说完分布式的原理,再来说说分布式的方案。由于场景不同,方案也不同。这里介绍两种比较流行的方案,two-stage和TCC(Try,Confirm,Cancel)。顾名思义,两阶段提交涉及对一个事务进行两次提交。这里需要引入两个概念,一个是事务协调器,也叫事务管理器。它用于协调事务。当所有的事务都准备好,可以提交的时候,由它来协调管理。另一个是参与者,也叫资源管理者。主要负责处理具体事务,管理人员需要处理的资源。例如:订票业务、扣款业务。第一阶段(准备阶段):事务协调器(事务管理器)向每个参与者(资源管理器)发送Prepare消息。发送此消息的目的是询问“每个人都准备好了吗?执行业务”。参与者将对照自己的业务和资源进行检查,然后给出反馈。此检查过程因业务内容而异。例如:订票业务,需要查询是否还有余票。扣款业务会检查余额是否足够。一旦检查通过,就可以返回就绪(Ready)信息。否则,事务将终止并等待下一次查询。由于这些检查需要一些操作,后面回滚的时候可能会用到这些操作,所以需要写redo和undo日志,在事务重试失败或者事务回滚失败的时候用到。第二阶段(commitphase):如果协调器收到参与者失败或超时的消息,会向参与者发送回滚(rollback)消息;否则,它会发送一个提交(commit)消息。两种情况的处理如下:情况1,当所有参与者都反馈yes时,提交事务:协调者向所有参与者发出正式的提交事务的请求(即commitrequest)。参与者执行提交请求并释放整个事务期间占用的资源。每个参与者向协调器反馈ack(响应)完成的消息。协调器收到所有参与者反馈的ack消息后,事务提交完成。情况2,当一个参与者反馈no时,事务被回滚:协调者向所有参与者发送回滚请求(rollbackrequest)。参与者利用第一阶段的undo信息进行回滚操作,释放整个事务占用的资源。每个参与者将ack完成的消息反馈给协调器。协调器收到所有参与者的ack消息后,交易完成。TCC(Try,Confirm,Cancel)对于一些对一致性要求高的分布式事务,比如支付系统,交易系统,我们都会使用TCC。包括,Try尝试,Confirm确认,Cancel取消。看看下面的例子能不能帮助你理解。假设我们有一个转账服务,需要将“A银行”和“A账户”中的钱分别转账到“B银行”、“B账户”和“C银行”、“C账户”。假设这三个银行都有自己的转账服务,那么这个转账交易就形成了一个分布式交易。下面看看用TCC是怎么解决的:转账业务示意图首先是Try阶段,主要检查资源是否可用,比如检查账户余额是否充足,缓存、数据库、队列是否可用等等。不实现特定逻辑。如上图,从“A账户”转出前,检查账户总金额是否大于100,并记录转出金额和剩余金额。对于“B账户”和“C账户”,需要知道账户的原始总额和转账金额,这样才能计算出转账金额。除了金额字段,这里的交易数据库设计还有一个转出金额或转入金额的字段,在回滚Cancel时用到。Try阶段示意图如果Try阶段成功,则进入Confirm阶段,执行具体的业务逻辑。到这里,从“A账户”转账100元成功,剩余总金额=220-100=120。将此剩余金额写入总额并保存,并设置交易状态为“转账成功”。“B账户”和“C账户”分别设置总金额为80=50+30和130=60+70,同时设置交易状态为“转账成功”。那么整个交易就完成了。Confirm阶段示意图如果Try阶段不成功,那么服务A、B、C都需要回滚。对于“A账户”,扣除的100元需要补回来,所以总金额为220=120+100。那么“服务B”和“服务C”需要从总金额中减去入账金额,即50=80-30,60=130-70。Cancel阶段TCC接口实现示意图这里需要注意的是,Try、Confirm、Cancel三个阶段的代码需要针对每个服务分别实现。比如上面提到的检查资源、执行服务、回滚服务等操作。目前有很多开源架构如ByteTCC、TCC-transaction可以借鉴。TCC实现接口示意图TCC可靠性TCC通过记录事务日志来保证可靠性。一旦服务挂了或者在Try、Confirm、Cancel操作过程中出现异常,TCC会提供重试机制。另外,如果服务是异步的,可以使用消息队列进行通信,保持事务的一致性。SchematicDiagramofRetryMechanismSchematicDiagramofRetryMechanismSchematicdiagramof分表分库介绍如果觉得分表分库后需要考虑的问题很多,可以使用市面上现成的中间件来帮助我们实现。下面介绍几个常用的中间件:MySQLProxy和Amoeba都是基于代理方式的。HibernateShards基于Hibernate框架。YoudangSharding-基于JDBC的JDBC。MushroomStreetTSharding,一个基于MyBatis的类Maven插件。此外,重点介绍了Sharding-JDBC的架构,与“服务注册中心”非常相似。Sharding-JDBC将提供一个Sharding-Proxy作为代理,它将连接到一个注册中心。一旦数据库节点连接到系统,它就会向这个中心注册,同时也会监控数据库的健康状况,进行心跳检测。而Sharding-Proxy本身可以在业务代码(BusinessCode)请求数据库时辅助负载均衡和路由。同时,Sharding-Proxy本身也可以通过MySQLCli和MySQLWorkbench查看。其实如果理解了分表分库的原理,实现起来并不难,很多大厂都提供了产品。Sharding-Proxy实现示意图总结由于数据量的增加,为了提高性能,系统会分为表和数据库。在分表方面,有水平分表和垂直分表两种方式。数据库可以按照业务、冷热数据等进行划分,数据库划分后,可以通过主从数据库实现读写分离。如果分库后扩容,有两种方式,主从扩容和双写扩容。分表分库会带来分布式事务,我们需要掌握CAP和BASE的原理,介绍两阶段提交和TCC两种分布式事务方案。最后介绍了目前流行的分表分库中间件及其实现原理。作者:崔浩简介:十六年开发架构经验。曾在惠普武汉交付中心担任技术专家、需求分析师、项目经理,后在一家初创公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构和研发管理。【原创稿件,合作网站转载请注明原作者和出处为.com】