数据持久化层场景实战
冷热分离
业务场景:几千万数据量的工单表如何快速优化
这次项目优化的是一个邮件客服系统。它是一个SaaS(通过网络提供软件服务)系统,但是大客户只有两三家,最主要的客户是一家大型媒体集团。
这个系统的主要功能是这样的:它会对接客户的邮件服务器,自动收取发到几个特定客服邮箱的邮件,每收到一封客服邮件,就自动生成一个工单。之后系统就会根据一些规则将工单分派给不同的客服专员处理。
这个系统是支持多租户的,每个租户使用自己的数据库(MySQL)。
这家媒体集团客户两年多产生了近2000万的工单,工单的操作记录近1亿。
平时客服在工单页面操作时,打开或者刷新工单列表需要10秒钟左右。
该客户当时做了一个业务上的变更,增加了几个客服邮箱,然后把原来不进入邮件客服系统的一些客户邮件的接收人改为这几个新增加的客服邮箱,并接入这个系统。
发生这个业务变更以后,工单数量急剧增长,工单列表打开的速度越来越慢,后来客服的负责人发了封邮件,言辞急切,要求尽快改善性能。
项目组收到邮件后,详细分析了一下当时的数据状况,情况如下。
1)工单表已经达到3000万条数据。
2)工单表的处理记录表达到1.5亿条数据。
3)工单表每日以10万的数据量在增长。
当时系统性能已经严重影响了客服的处理效率,需要放在第一优先级解决,客户给的期限是1周。
在客户提出需求之前,项目组已经通过优化表结构、业务代码、索引、SQL语句等办法来提高系统响应速度,系统最终支撑起了3000万数据的表查询。这次只能尝试其他方案。
因为给的时间太少了,所以也不太可能去做一些大的架构变动,项目组的预期是先用改动最小的临时性方案让客服可以正常工作。
如果不想改动架构,那么最简单的方法就是使用数据库分区,这样的话甚至都不需要改代码。
项目组一开始考虑用数据库的分区功能,但是后来放弃了,下面说说为什么。
分区并不是生成新的数据表,而是将表的数据均衡分配到不同的硬盘、系统或不同的服务器存储介质中,实际上还是一张表。
比如,要创建以下数据库表:
[插图]那么,数据库就会把这个t2表的数据根据YEAR(dob)这个表达式的值分布存储在d0~d7这8个分区。
数据库分区有以下优点。
1)比起单个文件系统或硬盘,分区可以存储更多的数据。
2)在清理数据时,可以直接删除废弃数据所在的分区。同样,有新数据时,可以增加更多的分区来存储新数据。3)可以大幅度地优化特定的查询,让这些查询语句只去扫描特定分区的数据。比如,原来有2000万的数据,设计10个分区,每个分区存200万的数据,那么可以优化查询语句,让它只去查询其中两个分区,即只需要扫描400万的数据。
第3个优点正好可以解决此处的项目需求。但是,要怎么设计分区字段?也就是说,要根据什么来分区?
下面具体说一下该业务场景中的数据表。工单表ticket中的关键字段见表1-1。
表1-1 工单表关键字段
工单表最主要的几个查询语句如下。
1)客服查询无处理人的工单:“Where assignedUserID=?”。
2)客服获取分派给自己的工单:“Where status in(…)and assignedUserID=?”。
3)客服组长查看自己组的工单:“Where assignedUserGroupID=?”。
4)客服查询特定客户的工单:“Where consumerEmail=?”。
为了达到只扫描特定分区的效果,必须在Where语句里面加上一个包含分区字段的条件,但是上面这些主要语句并不包含相同的字段。
另外,MySQL的分区还有个限制,即分区字段必须是唯一索引(主键也是唯一索引)的一部分。工单表是用ticketID当主键,也就是说接下来无论使用什么当分区字段,都必须把它加到主键当中,形成复合主键。MySQL官方文档原文如下。
接着深入分析一下业务流程。
1)系统从邮件服务器同步到邮件以后,创建一个工单,createdTime就是工单创建的时间。
2)客服先去查询无处理人的工单,然后把工单分派给自己。
3)客服处理工单,每处理一次,系统自动增加一条处理记录。
4)客服处理完工单以后,将工单状态改为“关闭”。
通过跟客服的交流,项目组发现,一般工单被关闭以后,客服查询的概率就很低了。对于那些关闭超过一个月的工单,基本上一年都打开不了几次。
调研到这里,基本的思路是增加一个状态:归档。首先将关闭超过一个月以上的工单自动转为“归档”状态,然后将数据库分为两个区,所有“归档”状态的工单存放在一个区,所有非“归档”状态的工单存放在另外一个区,最后在所有的查询语句中加一个条件,就是状态不等于“归档”。
简单估算一下:客服频繁操作的工单基本上都是1个月内的工单,按照后期一天10万来算,也就是300万的数据,这样数据库的非归档区基本就没什么压力了。
那么,是否就将status设为分区字段,然后直接使用MySQL的分区功能?不是的。
因为相关的开发人员并没有用过数据库分区的功能,而当时面临的情况是只有1周的时间来解决问题,并且工单表是系统最核心的数据表,不能出问题。
这种情况下,没人敢在生产的核心功能上使用一项没用过的技术,但是项目组评估了一下,要实现一个类似的方案,其实工作量并不大,而且代码可控。因此,项目组放弃了数据库分区,并决定基于同样的分区理念,使用自己熟悉的技术来实现这个功能.
这个思路也很简单:新建一个数据库,然后将1个月前已经完结的工单数据都移动到这个新的数据库。这个数据库就叫冷库,因为里面基本是冷数据(当然,叫作归档数据库也可以),之后极少被访问。当前的数据库保留正常处理的较新的工单数据,这是热库。
这样处理后,因为客服查询的基本是近期常用的数据,大概只有300万条,性能就基本没问题了。即使因为查询频繁,或者几个客服同时查询,也不会再像之前那样出现数据库占满CPU、整个系统几乎宕机的情况了。上面这个方法,其实就是软件系统常用的“冷热分离”。接下来介绍一下冷热分离的方案。
冷热分离简介
什么是冷热分离
冷热分离就是在处理数据时将数据库分成冷库和热库,冷库存放那些走到终态、不常使用的数据,热库存放还需要修改、经常使用的数据。
什么情况下使用冷热分离
假设业务需求出现了以下情况,就可以考虑使用冷热分离的解决方案。
1)数据走到终态后只有读没有写的需求,比如订单完结状态。
sql数据库迁移另一个硬盘
2)用户能接受新旧数据分开查询,比如有些电商网站默认只让查询3个月内的订单,如果要查询3个月前的订单,还需要访问其他的页面。
冷热分离一期实现思路:冷热数据都用MySQL
当决定用冷热分离之后,项目组就开始考虑使用一个性价比最高的冷热分离方案。因为资源有限、工期又短,冷热分离一期有一个主导原则,即热数据跟冷数据使用一样的存储(MySQL)和数据结构,这样工作量最少,等到以后有时间再做冷热分离二期。
在冷热分离一期的实际操作过程中,需要考虑以下问题。
1)如何判断一个数据是冷数据还是热数据?
2)如何触发冷热数据分离?
3)如何实现冷热数据分离?
4)如何使用冷热数据?
5)历史数据如何迁移?
如何判断一个数据到底是冷数据还是热数据
一般而言,在判断一个数据到底是冷数据还是热数据时,主要采用主表里一个字段或多个字段的组合作为区分标识。
这个字段可以是时间维度,比如“下单时间”,可以把3个月前的订单数据当作冷数据,3个月内的订单数据当作热数据。
当然,这个字段也可以是状态维度,比如根据“订单状态”字段来区分,将已完结的订单当作冷数据,未
完结的订单当作热数据。
还可以采用组合字段的方式来区分,比如把下单时间小于3个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。
而在实际工作中,最终使用哪种字段来判断,还是需要根据实际业务来决定的。
关于判断冷热数据的逻辑,这里还有两个要点必须说明。
1)如果一个数据被标识为冷数据,业务代码不会再对它进行写操作。
2)不会同时存在读取冷、热数据的需求。
回到本章项目场景,这里就把lastProcessTime大于1个月,并且status为“关闭”的工单数据标识为冷数据。
如何触发冷热数据分离
了解冷热数据的判断逻辑后,就要开始考虑如何触发冷热数据分离了。一般来说,冷热数据分离的触发逻辑分为3种。
1)直接修改业务代码,使得每次修改数据时触发冷热分离(比如每次更新订单的状态时,就去触发这个逻辑),如图1-2所示。这个逻辑在该业务场景中就表现为:工单表每做一次变更(其实就是客服对工单做处理操作),就要对变更后的工单数据触发一次冷热数据的分离。
2)如果不想修改原来的业务代码,可以通过监听数据库变更日志binlog的方式来触发。具体方法就是另外创建一个服务,这个服务专门用来监控数据库的binlog,一旦发现ticket表有变动,就将变动的工单数据发送到一个队列,这个队列的订阅者将会取出变动的工单,触发冷热分离逻辑,如图1-3所示。
3)通过定时扫描数据库的方式来触发。这个方式就是通过quartz配置一个本地定时任务,或者通过类似于xxl-job 的分布式调度平台配置一个定时任务。这个定时任务每隔一段时间就扫描一次热数据库里面的工单表,出符合冷数据标准的工单数据,进行冷热分离,如图1-4所示。
以上3种触发逻辑到底选哪种比较好?下面给出它们各自的优缺点,见表1-2。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。