30分钟带你吃透冷热分离一期实现思路:冷热数据都用MySQL

冷热分离一期实现思路:冷热数据都用MySQL

当决定用冷热分离之后,项目组就开始考虑使用一个性价比最高的冷热分离方案。因为资源有限、工期又短,冷热分离一期有一个主导原则,即热数据跟冷数据使用一样的存储(MySQL)和数据结构,这样工作量最少,等到以后有时间再做冷热分离二期。

如图1-1所示,在冷热分离一期的实际操作过程中,需要考虑以下问题。

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

2)如何触发冷热数据分离?

3)如何实现冷热数据分离?

4)如何使用冷热数据?

5)历史数据如何迁移?

图1-1 冷热分离需考虑问题示意图

接下来针对以上5个问题进行详细讲解。

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

一般而言,在判断一个数据到底是冷数据还是热数据时,主要采用主表里一个字段或多个字段的组合作为区分标识。

这个字段可以是时间维度,比如“下单时间”,可以把3个月前的订单数据当作冷数据,3个月内的订单数据当作热数据。

当然,这个字段也可以是状态维度,比如根据“订单状态”字段来区分,将已完结的订单当作冷数据,未完结的订单当作热数据。

还可以采用组合字段的方式来区分,比如把下单时间小于3个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。

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

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

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

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

回到本章项目场景,这里就把lastProcessTime大于1个月,并且status为“关闭”的工单数据标识为冷数据。

如何触发冷热数据分离

了解冷热数据的判断逻辑后,就要开始考虑如何触发冷热数据分离了。一般来说,冷热数据分离的触发逻辑分为3种。

1)直接修改业务代码,使得每次修改数据时触发冷热分离(比如每次更新订单的状态时,就去触发这个逻辑),如图1-2所示。

这个逻辑在该业务场景中就表现为:工单表每做一次变更(其实就是客服对工单做处理操作),就要对变更后的工单数据触发一次冷热数据的分离。

• 图1-2 修改业务代码触发冷热分离示意图

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

• 图1-3 监听日志触发冷热分离示意图

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

• 图1-4 定时触发冷热分离示意图

以上3种触发逻辑到底选哪种比较好?下面给出它们各自的优缺点,见表1-2。

表1-2 3种触发逻辑的优缺点

根据以上对比,可以得出每种触发逻辑的建议场景。

1.修改写操作的业务代码

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

场景示例:假设是根据订单的状态来区分冷热数据,订单的状态不会随着时间自动变化,必须有人去修改才会变化,并且很容易找出所有修改订单状态的业务代码,这种情况下可以用这种触发逻辑。

2.监听数据库变更日志

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

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

3.定时扫描数据库

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

所以这一次,项目组就选用了定时扫描数据库的触发方式。但是对于不同项目自身的场景,到底选择哪种触发方式,还是需要根据具体业务需求来决定。

当决定了冷热分离的触发方式后,就进入下一个决策点:如何分离冷热数据。整个方案最复杂的环节就是这里。

如何分离冷热数据

在讲解如何分离冷热数据之前,先来了解一下分离冷热数据的基本逻辑,只有掌握了基本原理,才能真正理解事物的本质。

分离冷热数据的基本逻辑如图1-5所示,细节如下。

• 图1-5 分离冷热数据基本逻辑示意图

1)判断数据是冷是热。

2)将要分离的数据插入冷数据库中。

3)从热数据库中删除分离的数据。

这个逻辑看起来简单,而实际做方案时,以下3点都要考虑在内。

1.一致性:同时修改多个数据库,如何保证数据的一致性?

这里提到的一致性要求是指如何保证任何一步出错后数据最终还是一致的。任何一个程序都要考虑在运行过程中突然出错中断时,应该怎么办。业务逻辑如下。

1)找出符合冷数据的工单。

2)将这些工单添加到冷数据库。

3)将这些工单从热数据库中删除。

举几个例子。

例1:假设执行到步骤2)的时候失败了,那么,要确保这些工单数据最终还是会被移到冷数据库。

例2:假设执行到步骤3)的时候失败了,那么,要确保这些工单数据最终还是会从热数据库中删除。

这称为“最终一致性”,即最终数据和业务实际情况是一致的。

这里的解决方案为,保证每一步都可以重试且操作都有幂等性,具体逻辑分为4步。

1 ) 在 热 数 据 库 中 给 需 要 迁 移 的 数 据 加 标 识 :

ColdFlag=WaittingForMove(实际处理中标识字段的值用数字就可以,这里是为了方便理解),从而将冷热数据标识的计算结果进行持久化,后面可以使用。

2)找出所有待迁移的数据(ColdFlag=WaittingForMove)。这一步是为了确保前面有些线程因为部分原因运行失败,出现有些待迁移的数据没有迁移的情况时,可以通过这个标识找到这些遗留在热数据库中的工单数据。也就是上述例1中的情况。

3)在冷数据库中保存一份数据,但在保存逻辑中需要加个判断来保证幂等性(关于幂等性,后续还有详细的介绍),通俗来说就是假如保存的数据在冷数据库已经存在了,也要确保这个逻辑可以继续进行。这样可以防止上述例2中的情况,因为可能会出现有一些工单其实已经保存到冷数据库中了,但是在将它们从热数据库删除时的逻辑出错了,它们仍然保留在热数据库中,等下次冷热分离的时候,又要将这些工单重复插入冷数据库中。这里面就要通过幂等性来确保冷数据库中没有重复数据。

4)从热数据库中删除对应的数据。

上面就是最终一致性要考虑的几点。

接着,还要考虑数据量的问题。

2.数据量:假设数据量大,一次处理不完,该怎么办?是否需要使用批量处理?

前面讲了3种冷热分离的触发逻辑,前2种基本不会出现数据量大的问题,因为每次只需要操作那一瞬间变更的数据,但如果采用定时扫描的逻辑就需要考虑数据量这个问题了。

回到业务场景中,假设每天做一次冷热分离,根据前面的估算,每天有10万的工单数据和几十万的工单历史记录数据要迁移,但是程序不可能一次性插入几十万条记录,这时就要考虑批量处理了。

这个实现逻辑也很简单,在迁移数据的地方加个批量处理逻辑就可以了。为方便理解,来看一个示例。

假设每次可以迁移1000条数据。

1)在热数据库中给需要的数据添加标识:ColdFlag=WaittingForMove。

这个过程使用Update语句就可以完成,每次更新大概10万条记录。

2)找出前1000条待迁移的数据(ColdFlag=WaittingForMove)。

3)在冷数据库中保存一份数据。

4)从热数据库中删除对应的数据。

5)循环执行2)~4)。

以上就是批量处理的逻辑。接下来讲第3点:并发性。

3.并发性:假设数据量大到要分到多个地方并行处理,该怎么办?

在定时迁移冷热数据的场景里(比如每天),假设每天处理的数据量大到连单线程批量处理都应对不了,该怎么办?这时可以使用多个线程进行并发处理。回到场景中,假设已经有3000万的数据,第一次运行冷热分离的逻辑时,这些数据如果通过单线程来迁移,一个晚上可能无法完成,会影响第二天的客服工作,所以要考虑并发,采用多个线程来迁移。

Tips

虽然大部分情况下多线程较快,但笔者在其他项目中也曾碰到过这种情况:单线程的batchsize达到一定数值时效率特别高,比任何batchsize的多线程还要快。因此,是否采用多线程要在测试环境中实际测试一下。

当采用多线程同时迁移冷热数据时,需要考虑如下实现逻辑。

(1)如何启动多线程?

本项目采用的是定时器触发逻辑,性价比最高的方式是设置多个定时器,并让每个定时器之间的间隔短一些,然后每次定时启动一个线程后开始迁移数据。

还有一个比较合适的方式是自建一个线程池,然后定时触发后面的操作:

先计算待迁移的热数据数量,再计算要同时启动的线程数,如果大于线程池的数量就取线程池的线程数,假设这个要启动的线程数量为N,最后循环N次启动线程池的线程来迁移数据。

本项目使用了第二种方式,设置一个size为10的线程池,每次迁移500条记录,如果标识出的待迁移记录超过5000条,那么最多启动10个线程。

考虑了如何启动多线程的问题,接下来就是考虑锁了。

(2)某线程宣布正在操作某个数据,其他线程不能操作它(锁)因为是多线程并发迁移数据,所以要确保每个线程迁移的数据都是独立分开的,不能出现多个线程迁移同一条记录的情况。其实这就是锁的一个场景。关于这个逻辑,需要考虑3个特性。

1)获取锁的原子性:当一个线程发现某个待处理的数据没有加锁时就给它加锁,这两步操作必须是原子性的,即要么一起成功,要么一起失败。实现这个逻辑时是要防止以下这种情况:

“我是当前正在运行的线程,我发现一条工单没有锁,结果在要给它加锁的瞬间,它已经被别人加锁了。”

可采用的解决方案是在表中加上LockThread字段,用来判断加锁的线程,每个线程只能处理被自己加锁成功的数据。然后使用一条Update…Where…语句,Where条件用来描述待迁移的未加锁或锁超时的数据,Update操作是使LockThread=当前线程ID,它利用MySQL的更新锁机制来实现原子性。

Tips

LockThread可以直接放在业务表中,也可以放在一个扩展表中。放在业务表中会对原来的表结构有一些侵入,放在扩展表中会增加一张表。最终,项目组选择将其放在业务表中,因为这种情况下编写的Update语句相对更简单,能缩短工期。

2)获取锁必须与处理开始保证一致性:当前线程开始处理这条数据时,需要再次检查操作的数据是否由当前线程锁定成功,实际操作为再次查询一下LockThread=当前线程ID的数据,再处理查询出来的数据。为什么要多此一举?因为当前面的Update…Where…语句执行完以后,程序并不知道哪些数据被Update语句更新了,也就是说被当前线程加锁了,所以还需要通过另一条SQL语句来查出这些被当前线程加锁成功的数据。这样就确保了当前线程处理的数据确实是被当前线程成功锁定的数据。

3)释放锁必须与处理完成保证一致性:当前线程处理完数据后,必须保证锁被释放。线程正常处理完后,数据不在热数据库,而是直接到了冷数据库,后续的线程不会再去迁移它,所以也就没有锁有没有及时释放的顾虑了。

(3)若某线程失败退出,但锁没释放,该怎么办(锁超时)?

如果锁定某数据的线程异常退出了且来不及释放锁,导致其他线程无法处理这个数据,此时该怎么办?解决方案为给锁设置一个合理的超时时间,如果锁超时了还未释放,其他线程可正常处理该数据。

所以添加一个新的字段LockTime,在更新数据的LockThread时,也将LockTime更新为当前时间。加锁的SQL语句则变成类似这样:

Update Set LockThread= 当 前 线 程 ID , LockTime= 当 前 时 间 …WhereLockThread为空Or LockTime<N秒这样的话,即使加锁的线程出现异常,后续的线程也可以去处理它,保证数据没有遗漏。

那么超时时间设为多长才是合理的?这一时间可以通过在测试环境中测试几次批量数据来得出。

设置超时时间时,还应考虑如果正在处理的线程并未退出、还在处理数据而导致了超时,又该怎么办。

假设超时时间为10秒。

如图1-6所示,上述场景顺序如下。

• 图1-6 锁超时重复处理场景示意图

1)10:00:00,线程甲锁住A1数据,开始处理。

2)10:00:10,线程甲还没处理结束,线程乙认为A1原来的锁已经超时,将A1的锁变成线程乙的线程ID,也开始处理A1。

这样就变成了两个线程重复处理A1数据。

对于这种场景,除了将超时的时间设置成处理数据的合理时间外,处理冷热数据的代码必须保证是幂等性的。

在编程中,一个幂等操作的特点是多次执行某个操作与执行一次操作的影响相同。

这句话什么意思?就是当多个线程先后对同一条数据进行迁移处理时,要让迁移线程的每一步都去判断:这条数据的当前步骤是否已经执行过了?如果是的话,直接进入下一步,或者忽略它。总之,需要达到的效果就是,不管只是线程甲处理A1数据一次,还是线程甲、乙各处理A1一次,甚至多个线程分别处理A1,都要确保最终的数据是一样的。

那么如何实现幂等操作?使用MySQL的Insert…On Duplicate Key Update语句即可。使用这样的操作后,当前线程的处理就不会破坏数据的一致性。

考虑到前面的逻辑比较复杂,这里专门总结了一个分离冷热数据的流程图,如图1-7所示。

• 图1-7 冷热分离流程图

介绍到这里,冷热分离的5个问题已经解决了3个。接下来要解决如何使用冷热数据。

 如何使用冷热数据

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

Tips

在判断是冷数据还是热数据时,必须确保用户没有同时读取冷热数据的需求。


图1-8 区分使用冷热数据简单示意图

回到真实场景,在工单列表页面的搜索区域增加一个checkBox:查询归档。这个checkBox默认不勾选,这种情况下客服每次查询的都是非归档的工单,也就是未关闭或者关闭未超过1个月的工单。如果客服要查询归档工单,则勾选这个checkBox,这种情况下,客服只能查询归档的工单,查询速度还是很慢。

历史数据如何迁移

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

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

 整体方案

把所有的逻辑汇总、梳理一下,就形成了一个整体解决方案,如图1-9所示。

图1-9 冷热分离整体方案示意图

总结一下,实现思路分为5个部分:冷热数据判断逻辑、冷热数据的触发逻辑、冷热数据分离实现思路、冷热数据库使用、历史数据迁移。以上就是整个冷热分离一期的方案。

这个项目的完成花费了10天,上线以后,在客服的常规工单处理页面中,查询基本可以在1秒左右完成,大大提升了客服的工作效率,甚至比业务变动之前还快,所以客户非常满意。

既然这里称为“一期”,那么很明显,这个方案还有二期。

为什么要有二期?下一节会讲到。

本文给大家讲解的内容是冷热分离一期实现思路:冷热数据都用MySQL

  1. 下篇文章给大家讲解的内容是冷热分离二期实现思路:冷数据存放到HBase
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
#MySQL##数据库##程序员##架构师##Java#
java架构师技术分享 文章被收录于专栏

java架构师技术分享

全部评论
清晰的思路可以加深我们的记忆
点赞 回复 分享
发布于 2022-08-28 17:36 河南

相关推荐

10-11 17:45
门头沟学院 Java
走吗:别怕 我以前也是这么认为 虽然一面就挂 但是颇有收获!
点赞 评论 收藏
分享
点赞 2 评论
分享
牛客网
牛客企业服务