文档参考:书名:《从程序员到架构师:大数据量、缓存、高并发、微服务、多团队协同等核心场景实战》-王伟杰
前文如下:
上会讲到如何判断一个数据到底是冷数据还是热数据 以及如何触发冷热数据分离。今天继续学习:如何把冷热数据的分离具体落地。
1.3.4 如何分离冷热数据
在讲解如何分离冷热数据之前,先来了解一下分离冷热数据的基本逻辑,只有掌握了基本原理,才能真正理解事物的本质。
分离冷热数据的基本逻辑如图所示,细节如下。
- 判断数据是冷是热。
- 将要分离的数据插入冷数据库中。
- 从热数据库中删除分离的数据。
这个逻辑看起来简单,而实际做方案时,以下3点都要考虑在内。
- 一致性:同时修改多个数据库,如何保证数据的一致性?这里提到的一致性要求是指如何保证任何一步出错后数据最终还是一致的。任何一个程序都要考虑在运行过程中突然出错中断时,应该怎么办。
业务逻辑如下。 1)找出符合冷数据的工单。
2)将这些工单添加到冷数据库。
3)将这些工单从热数据库中删除。\
举几个例子。
例1:假设执行到步骤2)的时候失败了,那么,要确保这些工单数据最终还是会被移到冷数据库。
例2:假设执行到步骤3)的时候失败了,那么,要确保这些工单数据最终还是会从热数据库中删除。\
这称为“最终一致性”,即最终数据和业务实际情况是一致的。
这里的解决方案为,保证每一步都可以重试且操作都有幂等性,具体逻辑分为4步。
1)在热数据库中给需要迁移的数据加标识:ColdFlag=WaittingForMove(实际处理中标识字段的值用数字就可以,这里是为了方便理解),从而将冷热数据标识的计算结果进行持久化,后面可以使用。
2)找出所有待迁移的数据(ColdFlag=WaittingForMove)。这一步是为了确保前面有些线程因为部分原因运行失败,出现有些待迁移的数据没有迁移的情况时,可以通过这个标识找到这些遗留在热数据库中的工单数据。也就是上述例1中的情况。
3)在冷数据库中保存一份数据,但在保存逻辑中需要加个判断来保证幂等性(关于幂等性,后续还有详细的介绍),通俗来说就是假如保存的数据在冷数据库已经存在了,也要确保这个逻辑可以继续进行。这样可以防止上述例2中的情况,因为可能会出现有一些工单其实已经保存到冷数据库中了,但是在将它们从热数据库删除时的逻辑出错了,它们仍然保留在热数据库中,等下次冷热分离的时候,又要将这些工单重复插入冷数据库中。这里面就要通过幂等性来确保冷数据库中没有重复数据。
4)从热数据库中删除对应的数据。上面就是最终一致性要考虑的几点。
数据量:假设数据量大,一次处理不完,该怎么办?是否需要使用批量处理?
前面讲了3种冷热分离的触发逻辑,前2种基本不会出现数据量大的问题,因为每次只需要操作那一瞬间变更的数据,但如果采用定时扫描的逻辑就需要考虑数据量这个问题了。
回到业务场景中,假设每天做一次冷热分离,根据前面的估算,每天有10万的工单数据和几十万的工单历史记录数据要迁移,但是程序不可能一次性插入几十万条记录,这时就要考虑批量处理了。
这个实现逻辑也很简单,在迁移数据的地方加个批量处理逻辑就可以了。为方便理解,来看一个示例。
假设每次可以迁移1000条数据。
1)在热数据库中给需要的数据添加标识:ColdFlag=WaittingForMove。这个过程使用Update语句就可以完成,每次更新大概10万条记录。
2)找出前1000条待迁移的数据(ColdFlag=WaittingForMove)。
3)在冷数据库中保存一份数据。
4)从热数据库中删除对应的数据。
5)循环执行2)~4)。
以上就是批量处理的逻辑。
3.并发性:假设数据量大到要分到多个地方并行处理,该怎么办?
在定时迁移冷热数据的场景里(比如每天),假设每天处理的数据量大到连单线程批量处理都应对不了,该怎么办?这时可以使用多个线程进行并发处理。回到场景中,假设已经有3000万的数据,第一次运行冷热分离的逻辑时,这些数据如果通过单线程来迁移,一个晚上可能无法完成,会影响第二天的客服工作,所以要考虑并发,采用多个线程来迁移。
Tips
虽然大部分情况下多线程较快,但笔者在其他项目中也曾碰到过这种情况:单线程的batchsize达到一定数值时效率特别高,比任何batchsize的多线程还要快。因此,是否采用多线程要在测试环境中实际测试一下。
当采用多线程同时迁移冷热数据时,需要考虑如下实现逻辑。
(1)如何启动多线程?
本项目采用的是定时器触发逻辑,性价比最高的方式是设置多个定时器,并让每个定时器之间的间隔短一些,然后每次定时启动一个线程后开始迁移数据。还有一个比较合适的方式是自建一个线程池,然后定时触发后面的操作:先计算待迁移的热数据数量,再计算要同时启动的线程数,如果大于线程池的数量就取线程池的线程数,假设这个要启动的线程数量为N,最后循环N次启动线程池的线程来迁移数据。本项目使用了第二种方式,设置一个size为10的线程池,每次迁移500条记录,如果标识出的待迁移记录超过5000条,那么最多启动10个线程。 考虑了如何启动多线程的问题,接下来就是考虑锁了。
(2)某线程宣布正在操作某个数据,其他线程不能操作它(锁)
因为是多线程并发迁移数据,所以要确保每个线程迁移的数据都是独立分开的,不能出现多个线程迁移同一条记录的情况。其实这就是锁的一个场景. 关于这个逻辑,需要考虑3个特性。
获取锁的原子性:当一个线程发现某个待处理的数据没有加锁时就给它加锁,这两步操作必须是原子性的,即要么一起成功,要么一起失败。实现这个逻辑时是要防止以下这种情况:
“我是当前正在运行的线程,我发现一条工单没有锁,结果在要给它加锁的瞬间,它已经被别人加锁了。” 可采用的解决方案是在表中加上LockThread字段(有点类似于以前文章里面写的通过数据库实现分布式锁),用来判断加锁的线程,每个线程只能处理被自己加锁成功的数据。然后使用一条Update…Where…语句,Where条件用来描述待迁移的未加锁或锁超时的数据,Update操作是使LockThread=当前线程ID,它利用MySQL的更新锁机制来实现原子性。
Tips
LockThread可以直接放在业务表中,也可以放在一个扩展表中。放在业务表中会对原来的表结构有一些侵入,放在扩展表中会增加一张表。最终,项目组选择将其放在业务表中,因为这种情况下编写的Update语句相对更简单,能缩短工期。
获取锁必须与处理开始保证一致性:当前线程开始处理这条数据时,需要再次检查操作的数据是否由当前线程锁定成功,实际操作为再次查询一下LockThread=当前线程ID的数据,再处理查询出来的数据。为什么要多此一举?因为当前面的Update…Where…语句执行完以后,程序并不知道哪些数据被Update语句更新了,也就是说被当前线程加锁了,所以还需要通过另一条SQL语句来查出这些被当前线程加锁成功的数据。这样就确保了当前线程处理的数据确实是被当前线程成功锁定的数据。
释放锁必须与处理完成保证一致性:当前线程处理完数据后,必须保证锁被释放。线程正常处理完后,数据不在热数据库,而是直接到了冷数据库,后续的线程不会再去迁移它,所以也就没有锁有没有及时释放的顾虑了。
(3)若某线程失败退出,但锁没释放,该怎么办(锁超时)?
如果锁定某数据的线程异常退出了且来不及释放锁,导致其他线程无法处理这个数据,此时该怎么办?解决方案为给锁设置一个合理的超时时间,如果锁超时了还未释放,其他线程可正常处理该数据。
所以添加一个新的字段LockTime,在更新数据的LockThread时,也将Lock Time更新为当前时间。加锁的SQL语句则变成类似这样:Update Set LockThread=当前线程ID,LockTime=当前时间…Where LockThread为空Or LockTime<N秒这样的话,即使加锁的线程出现异常,后续的线程也可以去处理它,保证数据没有遗漏。
那么超时时间设为多长才是合理的?这一时间可以通过在测试环境中测试几次批量数据来得出。设置超时时间时,还应考虑如果正在处理的线程并未退出、还在处理数据而导致了超时,又该怎么办。假设超时时间为10秒。
假设超时时间为10秒。如图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所示。
介绍到这里,冷热分离的5个问题已经解决了3个。接下来要解决如何使用冷热数据。
1.3.5 如何使用冷热数据
在功能设计的查询界面上,一般都会有一个选项用来选择需要查询冷数据还是热数据,如果界面上没有提供,则可以直接在业务代码里区分,如图所示。
Tips
在判断是冷数据还是热数据时,必须确保用户没有同时读取冷热数据的需求。
回到真实场景,在工单列表页面的搜索区域增加一个checkBox:查询归档。这个checkBox默认不勾选,这种情况下客服每次查询的都是非归档的工单,也就是未关闭或者关闭未超过1个月的工单。如果客服要查询归档工单,则勾选这个checkBox,这种情况下,客服只能查询归档的工单,查询速度还是很慢。
1.3.6 历史数据如何迁移--加个标识
一般而言,只要与持久化层有关的架构方案都需要考虑历史数据的迁移问题,即如何让旧架构的历史数据适用于新的架构。
因为前面的分离逻辑在考虑失败重试的场景时刚好覆盖了这个问题,所以其解决方案很简单,只需要批量给所有符合冷数据条件的历史数据加上标识ColdFlag=WaittingForMove,程序就会自动迁移了。
1.3.7 整体方案
把所有的逻辑汇总、梳理一下,就形成了一个整体解决方案,如图所示。
总结一下,实现思路分为5个部分:冷热数据判断逻辑、冷热数据的触发逻辑、冷热数据分离实现思路、冷热数据库使用、历史数据迁移。