从程序员到架构师——数据持久化层场景

全文摘自:从程序员到架构师(王伟杰著)
购买链接:https://item.jd.com/13626926.html

程序员之间的能力差异在哪里?如果是学技术,大家可以阅读同样的书籍和网络文章,为什么还会造成最终专业能力的差异?

有三点:

  1. 经历的场景不同

    同样大学毕业的程序员,学习能力的差别并不会很大,可是为什么行业头部公司的程序员更受欢迎?原因就是他们经历的场景不一样,头部公司就职的程序员会碰到更多在其他公司没有机会碰到的业务场景。

  2. 在同一个场景中思考的角度不同

    同样一个场景中,可以看到全局、从业务问题推导到最终技术细节的人,和基于别人的设计开始开发的人,其收获并不一样。

  3. 解决问题的方法论不同

    程序员是不可能掌握所有技术的,这就要求他们用20%的技术知识解决80%的问题。所以当碰到一个新的业务场景时,关于如何从0到1设计出方案并最终落地,每个人的方法论是有差异的。

如果想要学好软件架构,基于场景的学习方式最有效。因为一旦理解了业务场景,就能很容易地看懂某个解决方案,并理解解决方案背后的实现原理

如果你想晋升为一名软件架构师,则需要同时具备架构思维架构经历

  • 那些优秀的程序员则更清楚整个架构如何运作,以及个人负责的代码会在架构全局中起到什么关键作用
  • 对业务场景的理解、解决问题的思路、考虑问题的全面性及对解决方案的熟悉度

数据持久化层场景实战

主要方案为:

  1. 冷热分离;
  2. 数据库分区;
  3. 读写分离;
  4. 查询分离;
  5. 分表分库;

冷热分离

冷热分离就是在处理数据时将数据库分成冷库和热库,冷库存放那些走到终态、不常使用的数据,热库存放还需要修改、经常使用的数据。

主要考虑的是锁的机制批量处理以及失败重试的数据一致性问题

什么情况下使用冷热分离

假设业务需求出现了以下情况,就可以考虑使用冷热分离的解决方案。

  1. 数据走到终态后只有读没有写的需求,比如订单完结状态。
  2. 用户能接受新旧数据分开查询,比如有些电商网站默认只让查询3个月内的订单,如果要查询3个月前的订单,还需要访问其他的页面。

场景

一个邮件客服系统,他会对接客户的邮件服务器,自动收取发到几个特定客服邮箱的邮件,每收到一封客服邮件,就自动生成一个工单。之后系统就会根据一些规则将工单分派给不同的客服专员处理。

后来增加了几个客服邮箱,工单数量急剧增长,工单列表打开的速度越来越慢,后来客服的负责人发了封邮件,言辞急切,要求尽快改善性能。

项目组收到需求之后,详细分析了一下当时的数据状况,情况如下:

  1. 工单表已经达到3000万条数据。
  2. 工单表的处理记录表达到1.5亿条数据。
  3. 工单表每日以10万的数据量在增长。
  4. 一般工单被关闭以后,客服查询的概率就很低了。对于那些关闭超过一个月的工单,基本上一年都打开不了几次。

这里需要注意一点,一定要深刻理解需求以及场景!第4点是经过与客服沟通之后才获取到的信息!!!!

实现思路

  1. 如何判断一个数据是冷数据还是热数据?
  2. 如何触发冷热数据分离?
  3. 如何实现冷热数据分离?
  4. 如何使用冷热数据?
  5. 历史数据如何迁移?

如何判断一个数据是冷数据还是热数据?

在实际工作中,最终使用哪种字段来判断,还是需要根据实际业务来决定的。

关于判断冷热数据的逻辑,这里还有两个要点必须说明:

  1. 如果一个数据被标识为冷数据,业务代码不会再对它进行写操作。

  2. 不会同时存在读取冷、热数据的需求。

如何触发冷热数据分离?

有三种方案:

  1. 直接修改业务代码,使得每次修改数据时触发冷热分离;

    建议在业务代码比较简单,并且不按照时间区分冷热数据时使用。

    场景示例:假设是根据订单的状态来区分冷热数据,订单的状态不会随着时间自动变化,必须有人去修改才会变化,并且很容易找出所有修改订单状态的业务代码,这种情况下可以用这种触发逻辑。
    从程序员到架构师——数据持久化层场景_第1张图片

  2. 如果不想修改原来的业务代码,可以通过监听数据库变更日志binlog的方式来触发。具体方法就是另外创建一个服务,这个服务专门用来监控数据库的binlog,一旦发现ticket表有变动,就将变动的工单数据发送到一个队列,这个队列的订阅者将会取出变动的工单,触发冷热分离逻辑

    建议在业务代码比较复杂,不能随意变更,并且不按时间区分冷热数据时使用。

    示例场景跟上一场景类似:假设是根据订单的状态来区分冷热数据,订单的状态不会随着时间自动变化,必须有人去修改才会变化。其不一样的地方在于,业务代码很复杂,特别是有些用了很多年的系统中,修改订单状态的代码分布在多个位置,甚至多个服务中,不可能都找到,并且因为难以评估影响面,所以修改起来风险很大。这种情况下就适合使用监听数据库变更日志的方式。

从程序员到架构师——数据持久化层场景_第2张图片

  1. 通过定时扫描数据库的方式来触发。这个方式就是通过quartz配置一个本地定时任务,或者通过类似于xxl-job的分布式调度平台配置一个定时任务。这个定时任务每隔一段时间就扫描一次热数据库里面的工单表,找出符合冷数据标准的工单数据,进行冷热分离

    建议在按照时间区分冷热数据时使用。

    示例场景就是这个项目中的业务场景。这里的业务需求是已经关闭超过1个月的工单视为冷数据,这种场景下,工单变更的那一瞬间,即使工单已经关闭了,也不能将其视为冷数据,而必须再等待1个月。这样的情况非常适合使用定时扫描。

从程序员到架构师——数据持久化层场景_第3张图片

三种方案的优缺点:

从程序员到架构师——数据持久化层场景_第4张图片

如何分离冷热数据?

分离冷热数据的步骤为:

  1. 判断数据是冷是热;
  2. 将要分离的数据插入冷数据库中;
  3. 从热数据库中删除分离的数据。

在这些步骤中,我们需要考虑的点为:

  1. 数据的最终一致性(同时修改多个数据库);
  2. 数据量:假设数据量大,一次处理不完,该怎么办?是否需要使用批量处理?
  3. 并发性:假设数据量大到要分到多个地方并行处理,该怎么办?

如何使用冷热数据?

在功能设计的查询界面上,一般都会有一个选项用来选择需要查询冷数据还是热数据,如果界面上没有提供,则可以直接在业务代码里区分

从程序员到架构师——数据持久化层场景_第5张图片

历史数据如何迁移

一般而言,只要与持久化层有关的架构方案都需要考虑历史数据的迁移问题,即如何让旧架构的历史数据适用于新的架构。

因为前面的分离逻辑在考虑失败重试的场景时刚好覆盖了这个问题,所以其解决方案很简单,只需要批量给所有符合冷数据条件的历史数据加上标识ColdFlag=WaittingForMove,程序就会自动迁移了。

整体方案

最终,项目组选择了定时任务,整体方案如下:

从程序员到架构师——数据持久化层场景_第6张图片

优化点

在上述方案中,使用的存储是mysql,其实对于归档的数据,使用HBase数据库会更好一些。

这样,冷热分离这个方案就完成了。

冷热分离方案只是刚好适用于这个场景,它其实有很多不足。后来笔者又反思了一下,如果不是因为做一期方案的时候只有1周的时间,那么是不是还会使用冷热分离的方案?会不会有更好的方案?

这个方案有一些明显的不足,如果碰到下面的任何一个场景,这个方案就不适用了。

  1. 工单没有“归档”这一特点,经常需要修改。
  2. 所有的工单数据都需要支持复杂的查询,并且需要非常快的响应速度。
  3. 需要实时地对工单数据进行各种统计。

笔者后来做其他项目的时候就碰到了上面列举的场景,那么,这些情况下,又该用什么方案来解决?

查询分离

查询分离即每次写数据时保存一份数据到其他的存储系统里,用户查询数据时直接从中获取数据

从程序员到架构师——数据持久化层场景_第7张图片

什么情况下使用查询分离

当在实际业务中遇到以下情形时,就可以考虑使用查询分离。

  1. 数据量大:比如单个表的行数有上千万,当然,如果几百万就出现查询慢的问题,也可以考虑使用。
  2. 查询数据的响应效率很低:因为表数据量大,或者关联查询太过复杂,导致查询很慢的情况。
  3. 所有写数据请求的响应效率尚可:虽然查询慢,但是写操作的响应速度还可以接受的情况。
  4. 所有数据任何时候都可能被修改和查询:这一点是针对冷热分离的,因为如果有些数据走入终态就不再用到,就可以归档到冷数据库了,不一定要用查询分离这个方案。

很多人对查询分离这个概念特别熟悉,但是对于查询分离的使用场景不太理解,这是不够的。只有了解了查询分离的真正使用场景,才能在遇到实际问题时采取最正确的解决方案。

场景

客服系统承接的是集团的所有业务,每条业务线的客服又分为多个渠道,有电话、在线聊天、微信、微博等。

当客户接线进来以后,不管是通过什么渠道,客服都会登记一个客服工单,而后再根据业务线、工单的类型来登记不同的信息;工单创建后,会按需创建其他的单据,比如退款单、投诉单、充值单等,针对每个该工单的处理动作或工单关联单据的处理动作,也会自动添加工单处理记录和更新处理时间。

系统已经运作了5年左右,已有数据量大,而且随着集团业务的扩大,业务线增加,客服增多,工单数量的增长也越来越快,在系统中查询工单,以及打开工单详情的时候,就会出现响应速度很慢的情况。

项目组调研了查询慢和工单详情打开慢的问题,具体情况如下。

  1. 查询慢。当时工单数据库里面有1000万左右的客服工单时,每次查询时需要关联其他近10个表,一次查询平均花费13秒左右。
  2. 打开工单慢。工单打开以后需要调用多个接口,分别将用户信息、订单信息以及其他客服创建的单据信息列出来(如退款、赔偿、充值、投诉等)。打开工单详情页需要近5秒。

这次的需求的数据没有走向终态的场景,数据可能随时发生变化。

除了查询分离这种思路外,还有一个读写分离思路

这个思路是这样的:MySQL有个主从架构,可以将所有对工单表的写操作转入MySQL的主库,所有对工单表的查询操作连接到MySQL的从库。读写分离的好处就是,读的请求和写的请求针对不同的数据库,彼此不会抢占数据库资源。而且,主库用InnoDB的存储引擎,从库用MyISM,MyISM不支持事务,但是性能更好。

但是,使用这个方案得到的工单查询速度提升有限,所以最终没有采用。它主要还是用在数据库高并发的场景中。

实现思路

  1. 如何触发查询分离?
  2. 如何实现查询分离?
  3. 查询数据如何存储?
  4. 查询数据如何使用?
  5. 历史数据如何迁移?

如何触发查询分离

这个问题是说应该在什么时候保存一份数据到查询数据库,即什么时候触发查询分离这个动作。

有三种方案:

  1. 修改业务代码,在写入常规数据后同步更新查询数据。

    从程序员到架构师——数据持久化层场景_第8张图片

  2. 修改业务代码,在写入常规数据后,异步更新查询数据
    从程序员到架构师——数据持久化层场景_第9张图片

  3. 监控数据库日志,如有数据变更,则更新查询数据。

    从程序员到架构师——数据持久化层场景_第10张图片

以上3种触发逻辑的优缺点:

从程序员到架构师——数据持久化层场景_第11张图片

(1)业务逻辑灵活可控
一般来说,写业务代码的人能从业务逻辑中快速判断出何种情况下更新查询数据,而监控数据库日志的人并不能将全部的数据库变更分支穷举出来,再把所有的可能性关联到对应的查询数据更新逻辑中,最终导致任何数据的变更都需要重新建立查询数据。

(2)减缓写操作速度
更新查询数据的一个动作能减缓多少写操作速度?答案是很多。举个例子:当只是简单更新了订单的一个标识时,本来更新这个字段的时间只需要2毫秒,但是去更新订单的查询数据时,可能会涉及索引重建(比如使用Elasticsearch查询数据库时,会涉及索引、分片、主从备份,其中每个动作又细分为很多子动作,这些内容后面的场景会讲到),这时更新查询数据的过程可能就需要1秒了。

(3)查询数据更新前,用户可能查询到过时数据
这里结合第2种触发逻辑来讲。比如某个操作正处于订单更新状态,状态更新时会异步更新查询数据,更新完后订单才从“待审核”状态变为“已审核”状态。假设查询数据的更新时间需要1秒,这1秒中如果用户正在查询订单状态,这时主数据虽然已变为“已审核”状态,但最终查询的结果还是显示为“待审核”状态。

可总结出3种触发逻辑的适用场景

从程序员到架构师——数据持久化层场景_第12张图片

如何实现查询分离

项目组选择的是第2种触发方案:修改业务代码异步更新查询数据。最基本的实现方式是单独启动一个线程来创建查询数据,不过使用这种做法要考虑以下情况。

  1. 写操作较多且线程太多时,就需要加以控制,否则太多的线程最终会拖垮JVM。
  2. 创建查询数据的线程出错时,如何自动重试?如果要自动重试,是不是要有个地方标识更新失败的数据?
  3. 多线程并发时,很多并发场景需要解决。面对以上3种情况,该如何处理?

此时就可以考虑使用MQ(Message Queue,消息队列)来解决这些问题了。MQ的具体操作思路为,每次程序处理主数据写操作请求时,都会发一个通知给MQ,MQ收到通知后唤醒一个线程来更新查询数据

从程序员到架构师——数据持久化层场景_第13张图片

了解MQ的具体操作思路后,还应该考虑以下5个问题。

  1. MQ如何选型?
  2. MQ宕机了怎么办?
  3. 更新查询数据的线程失败了怎么办?
  4. 消息的幂等消费。
  5. 消息的时序性问题。

此处读者心中可能有个疑问:MQ在这里的作用只是一个触发信号的工具,如果不用MQ似乎也可以。其实不然,这里MQ的作用如下。

1)服务的解耦:这样主业务逻辑就不会依赖更新查询数据这个服务了。

2)控制更新查询数据服务的并发量:如果直接调用更新查询数据服务,因写操作速度快,更新查询数据速度慢,写操作一旦并发量高,就会造成更新查询数据服务的超载。如果通过消息触发更新查询数据服务,就可以通过控制消息消费者的线程数来控制负载。

查询数据如何存储

应该使用什么技术来存储查询数据呢?目前开发者们主要使用Elasticsearch实现大数据量的搜索查询,当然还可能用到MongoDB、HBase这些技术,这就需要开发者对各种技术的特性了如指掌后再进行技术选型。

前面已经介绍了HBase。它可以存储海量数据,但是其设计初衷并不是用来做复杂查询,即使可以做到,效率也不高。而此处的工单查询复杂度很高,所以项目组最后锁定的两个选项是MongoDB和Elasticsearch。

关于技术选型这个问题,很多时候不能只考虑业务功能的需求,还需要考虑人员的技术结构。比如在这个项目中,设计架构方案时选用了Elasticsearch,之所以这样,除Elasticsearch对查询的扩展性支持外,最关键的一点是团队对Elasticsearch很熟悉,但是没有人熟悉MongoDB。运维人员也没有MongoDB的运维经验。

现在查询分离中的写部分已经完成了,接下来考虑读的部分。

查询数据如何使用

数据存到Elasticsearch以后,就要查询了。那查询的时候要注意什么呢?

因Elasticsearch自带API,所以使用查询数据时,在查询业务代码中直接调用Elasticsearch的API即可。至于Elasticsearch的API怎么用,这里就不讲了。

不过要考虑一个场景:**数据查询更新完前,查询数据不一致怎么办?**举一个例子:假设更新工单的操作可以在100毫秒内完成,但是将新的工单同步到Elasticsearch需要2秒,那么在这2秒内,如果用户去查询,就可能查询到旧的工单数据。

这里分享两种解决思路。

1)在查询数据更新到最新前,不允许用户查询。笔者团队没用过这种方案,但在其他实际项目中见到过。

2)给用户提示:“您目前查询到的数据可能是2秒前的数据,如果发现数据不准确,可以尝试刷新一下。”这种提示用户一般都能接受。

历史数据迁移

新的架构方案上线后,旧的数据如何适应新的架构方案?这是实际业务中需要考虑的问题。

在这个方案里,只需要把所有的历史数据加上标识NeedUpdateQueryData=true,程序就会自动处理了。

MQ+Elasticsearch的整体方案

整个方案的要点如下。

1)使用异步方式触发查询数据的同步。当工单修改后,会异步启动一个线程来同步工单数据到查询数据库。

2)通过MQ来实现异步的效果。MQ还做了两件事:①服务的解耦,将工单主业务系统和查询系统的服务解耦;②削峰,当修改工单的并发请求太多时,通过MQ控制同步查询数据库的线程数,防止查询数据库的同步请求太大。

3)将工单的查询数据存储在Elasticsearch中。因为Elasticsearch是一个分布式索引系统,天然就是用来做大数据的复杂查询的。

4)因为查询数据同步到Elasticsearch会有一定的延时,所以用户可能会查询到旧的工单数据,所以要给用户一些提示。

5)关于历史数据的迁移,因为是用字段NeedUpdateQueryData来标识工单是否需要同步,所以只要把所有历史数据的标识改成true,系统就会自动批量将历史数据同步到Elasticsearch。

从程序员到架构师——数据持久化层场景_第14张图片

查询分离这个解决方案虽然能解决一些问题,但也要认识到它的不足:

  1. 使用Elasticsearch存储查询数据时,就要接受上面列出的一些局限性:有一定延时,深度分页不能自由跳页,会有丢数据的可能性。
  2. 主数据量越来越大后,写操作还是慢,到时还是会出问题。比如这里的工单数据,虽然已经去掉了所有外键,但是当数据量上亿的时候,插入还是会有问题。
  3. 主数据和查询数据不一致时,如果业务逻辑需要查询数据保持一致性呢?这里的查询数据同步到最新数据会有一定的延时,大约为2秒。某些业务场景下用户可能无法接受这个延时,特别是跟钱有关的场景。

分库分表

场景

电商系统中大数据量的实体有两个,用户和订单,其中用户400w数据,订单1200w数据。

某天,领导召集IT部门人员开会,说:“根据市场推广的趋势,我们的订单很快就会上亿,每天会有100万的新订单。不要问我这个数据怎么出来的,总之,领导交代,让IT部门提前做好技术准备,以防到时候系统撑不住”。

做这个规划之前,存储订单的数据库表是一个单库单表。可以预见,在不久的将来数据库的I/O和CPU就可能支撑不住,因为订单系统原来就不是很快。

然后项目组做了简单的功能,插入一些测试数据,订单量到2000万的时候,响应时长就不可接受了。

为了使系统能承受这种日百万级新订单的压力,项目组探讨过很多解决方案,最终决定使用分表分库:先将订单表拆分,再进行分布存储。

拆分存储的技术选型

拆分存储常用的技术解决方案目前主要分为4种:MySQL的分区技术、NoSQL、NewSQL、基于MySQL的分表分库。

MySQL的分区技术

图3-1所示为MySQL官方文档中的架构图。MySQL的分区技术主要体现在图3-1中的文件存储层File System,它可以将一张表的不同行存放在不同的存储文件中,这对使用者来说比较透明。

从程序员到架构师——数据持久化层场景_第15张图片

在以往的项目中,项目组不使用它的原因主要有4点:

  1. MySQL的实例只有一个,它仅仅分摊了存储,无法分摊请求负载。
  2. 正是因为MySQL的分区对用户透明,所以用户在实际操作时往往不太注意,如果SQL跨了分区,那么操作就会严重影响系统性能。
  3. MySQL还有一些其他限制,比如不支持query cache、位操作表达式等。
  4. MySQL的分区还有个限制,即分区字段必须是唯一索引(主键也是唯一索引)的一部分,也就是说接下来无论使用什么当分区字段,都必须把它加到主键当中,形成复合主键

NoSQL

比较典型的NoSQL数据库就是MongoDB。MongoDB的分片功能从并发性和数据量这两个角度已经能满足一般大数据量的需求,但是还需要注意下面3点。

  1. 约束考量:MongoDB不是关系型数据库而是文档型数据库,它的每一行记录都是一个结构灵活可变的JSON,比如存储非常重要的订单数据时,就不能使用MongoDB,因为订单数据必须使用强约束的关系型数据库进行存储。举个例子,订单里面有金额相关的字段,这是系统里面的核心数据,所以必须保证每个订单数据都有这些金额相关的字段,并且不管是怎样的业务逻辑修改,这些字段都要保存好,这时可以通过数据库的能力加一层校验,这样即使业务代码出了问题,导致这些字段存储不正确,也可以在数据库这一层面阻隔问题。

    当然,MongoDB 3.2版以后也支持Schema Validation(模式验证),可以制订一些约束规则。不过项目组使用MongoDB的原因之一就是看重它灵活的Schema(模式)。

  2. 业务功能考量:订单这种跟交易相关的数据肯定要支持事务和并发控制,而这些并不是MongoDB的强项。而且除了这些功能以外,多年来,事务、锁、SQL、表达式等各种各样的操作都在MySQL身上一一实践过,MySQL可以说是久经考验,因此在功能上MySQL能满足项目所有的业务需求,MongoDB却不一定能,且大部分的NoSQL也存在类似复杂功能支持的问题。

  3. 稳定性考量:人们对MySQL的运维已经很熟悉了,它的稳定性没有问题,然而MongoDB的稳定性无法保证,毕竟很多人不熟悉。

NewSQL

NewSQL技术还比较新,笔者曾经想在一些不重要的数据中使用NewSQL(比如TiDB),但从稳定性和功能扩展性两方面考量后,最终没有使用,具体原因与MongoDB类似。

基于MySQL的分表分库

最后说一下基于MySQL的分表分库:分表是将一份大的表数据进行拆分后存放至多个结构一样的拆分表中;分库就是将一个大的数据库拆分成类似于多个结构的小数据库。

原来的订单表就是一个sale数据库里面的一张order表,之后就会创建多个order数据库order1,order2,order3,order4,……,每个数据库里面又有多张订单表t_order_1,t_order_2,t_order_3,……。当然,订单子表也是多张:t_order_item_1,t_order_item_2,t_order_item_3,……。

订单数据根据一定的规律分布存储在不同order库里的不同order表中。

项目组没有选用前面介绍的3种拆分存储技术,而是选择了基于MySQL的分表分库,其中有一个重要考量:分表分库对于第三方依赖较少,业务逻辑灵活可控,它本身并不需要非常复杂的底层处理,也不需要重新做数据库,只是根据不同逻辑使用不同SQL语句和数据源而已,因此,之后出问题的时候也能够较快地找出根源。

如果使用分表分库,有3个通用技术需求需要实现。

  1. SQL组合:因为关联的表名是动态的,所以需要根据逻辑组装动态的SQL。比如,要根据一个订单的ID获取订单的相关数据,Select语句应该针对(From)哪一张表?
  2. 数据库路由:因为数据库名也是动态的,所以需要通过不同的逻辑使用不同的数据库。比如,如果要根据订单ID获取数据,怎么知道要连接哪一个数据库?
  3. 执行结果合并:有些需求需要通过多个分库执行后再合并归集起来。假设需要查询的数据分布在多个数据库的多个表中(比如在order1里面的t_order_1,order2里面的t_order_9中),那么需要将针对这些表的查询结果合并成一个数据集。

而目前能解决以上问题的中间件分为两类:Proxy模式、Client模式。

Proxy模式:图3-2所示为ShardingSphere官方文档中的Proxy模式图,重点看中间的Sharding-Proxy层。

这种设计模式将SQL组合、数据库路由、执行结果合并等功能全部放在了一个代理服务中,而与分表分库相关的处理逻辑全部放在了其他服务中,其优点是对业务代码无侵入,业务只需要关注自身业务逻辑即可。

Client模式:ShardingSphere官方文档中的Client模式如图3-3所示。这种设计模式将分表分库相关逻辑放在客户端,一般客户端的应用会引用一个jar,然后在jar中处理SQL组合、数据库路由、执行结果合并等相关功能

从程序员到架构师——数据持久化层场景_第16张图片

从程序员到架构师——数据持久化层场景_第17张图片

这两种模式的中间件见表3-2

从程序员到架构师——数据持久化层场景_第18张图片

这两种开源中间件的设计模式该如何选择呢?先简单对比一下它们的优缺点,见表3-3。

从程序员到架构师——数据持久化层场景_第19张图片

因为看重“代码灵活可控”这个优势,项目组最终选择了Client模式里的Sharding-JDBC来实现分表分库,如图3-3所示。当然,关于拆分存储选择哪种技术合适,在实际工作中需要根据具体情况来定。

实现思路

技术选型这一难题解决后,具体如何落实分表分库方案呢?需要考虑5个要点。

  1. 使用什么字段作为分片主键?
  2. 分片的策略是什么?
  3. 业务代码如何修改?
  4. 历史数据如何迁移?
  5. 未来的扩容方案是什么?

使用什么字段作为分片主键?

根据这些常见业务需求,判断一下优先级,确立字段

选择字段作为分片主键时,一般需要考虑3个要求:

  1. 数据尽量均匀分布在不同的表或库;
  2. 跨库查询操作尽可能少;
  3. 所选字段的值不会变(这点尤为重要)。

分片的策略是什么?

前通用的分片策略分为根据范围分片、根据Hash值分片、根据Hash值及范围混合分片这3种。

根据范围分片:比如user_ID是自增型数字,把user_ID按照每100万份分为一个库,每10万份分为一个表的形式进行分片,见表3-6。

从程序员到架构师——数据持久化层场景_第20张图片

说明:这里只讲分表,分库就是把分表分组存放在一个库即可。

根据Hash值分片:指的是根据user_ID的Hash值mod(取模)一个特定的数进行分片(为了方便后续扩展,一般是2n)。

根据Hash值及范围混合分片:先按照范围分片,再根据Hash值取模分片。比如,表名=order_#user_ID% 10#_#hash(user_ID)%8,即分成了10×8=80个表,如图3-4所示。

以上3种分片策略到底应该选择哪个?只需要考虑一点:假设之后数据量变大了,需要把表分得更细,此时保证迁移的数据尽量少即可。

因此,根据Hash值分片时,一般建议拆分成2n个表。比如分成8张表,数据迁移时把原来的每张表拆一半出来组成新表,这样数据迁移量就小了。

从程序员到架构师——数据持久化层场景_第21张图片

业务代码如何修改

分片策略确定后,就要考虑业务代码如何修改了。因业务代码修改与业务强关联,所以该项目采用的方案不具备通用性,这里就没有列出来。

但是,笔者在这里分享一些经验。近年来,分表分库操作更加容易,不过需要注意几个要点。

  1. 如果使用微服务,对于特定表的分表分库,其影响面只为该表所在的服务,而如果是一个单体架构的应用做分表分库,那会很麻烦。因为单体架构里面会有很多的跨表关联查询,也就是说,很多地方会直接与订单表一起进行Join查询,这种情况下,要想将订单数据拆分到多个库、多个表中,修改的代码就会非常多。
  2. 在互联网架构中,基本不使用外键约束。
  3. 分库分表以后,与订单有关的一些读操作都要考虑对应的数据是在哪个库哪个表。可以的话,尽量避免跨库或跨表查询。

历史数据如何迁移

讲解查询分离时提过一个方案,就是监控数据库变更日志,将数据库变更的事件变成消息,存到消息系统,然后有个消费者订阅消息,再将变动的数据同步到查询数据库,如图3-5所示。

从程序员到架构师——数据持久化层场景_第22张图片

此数据迁移方案的基本思路为:旧架构继续运行,存量数据直接迁移,增量数据监听binlog,然后通过canal通知迁移程序迁移数据,等到新的数据库拥有全量数据且校验通过后再逐步切换流量到新架构。

数据迁移解决方案的详细步骤如下。

  1. 上线canal,通过canal触发增量数据的迁移。
  2. 迁移数据脚本测试通过后,将老数据迁移到新的分表分库中。
  3. 注意迁移增量数据与迁移老数据的时间差,确保全部数据都被迁移过去,无任何遗漏。
  4. 此时新的分表分库中已经拥有全量数据了,可以运行数据验证程序,确保所有数据都存放在新数据库中。

到这里数据迁移就算完成了,之后就是新版本代码上线,至于是灰度上线还是直接上线,需要根据实际情况决定,回滚方案也是一样。

未来的扩容方案是什么

随着业务的发展,如果原来的分片设计已经无法满足日益增长的数据量的需求,就需要考虑扩容了。扩容方案主要依赖以下两点。

  1. 分片策略是否可以让新表数据的迁移源只有一个旧表,而不是多个旧表?这就是前面建议使用2n分表的原因——以后每次扩容都能扩为2倍,都是把原来一张表的数据拆分到两张表中。
  2. 数据迁移。需要把旧分片的数据迁移到新的分片上,这个方案与上面提及的历史数据迁移一样,此处不再赘述。

该方案还有一些不足之处

  1. 复杂查询慢:很多查询需要跨订单数据库进行,然后再组合结果集,这样的查询比较慢。业界的普遍做法是前面提到的查询分离。第2章讲了单独使用Elasticsearch做查询分离的方案,这里分表分库的二期项目也进行了查询分离,只是查询数据存到了Elasticsearch和HBase中。Elasticsearch存放订单ID、用来查询关键字的字段以及查询页面列表里用到的字段,HBase存放订单的全量数据。Elasticsearch先根据用户的查询组合返回查询结果到查询页面。用户点击特定的订单,就能根据订单ID去HBase获取订单的全量数据。
  2. 增量数据迁移的高可用性和一致性:如果是自己编写迁移的代码,那就参考前面冷热分离和查询分离的迁移逻辑;也可以使用开源工具,这个方案在后面数据同步的场景中会单独展开。
  3. 短时订单量大爆发:分表分库可以解决数据量大的问题,但是如果瞬时流量非常大,数据库撑不住怎么办?这一问题会在后面的缓存和秒杀架构等场景中专门展开。

你可能感兴趣的:(书籍笔记,java,场景)