本文讲的第一个场景是冷热分离。简单来说,就是将常用的“热”数据和不常使用的“冷”数据分开存储。
本章要考虑的重点是锁的机制、批量处理以及失败重试的数据一致性问题。这部分内容在实际开发中的“陷阱”还是不少的。
首先介绍一下业务场景。
这次项目优化的是一个邮件客服系统。它是一个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 ( … ) andassignedUserID=?”。
3)客服组长查看自己组的工单:“Where assignedUserGroupID=?”。
4)客服查询特定客户的工单:“Where consumerEmail=?”。
为了达到只扫描特定分区的效果,必须在Where语句里面加上一个包含分区字段的条件,但是上面这些主要语句并不包含相同的字段。
另外,MySQL的分区还有个限制,即分区字段必须是唯一索引(主键也是唯一索引)的一部分。工单表是用ticketID当主键,也就是说接下来无论使用什么当分区字段,都必须把它加到主键当中,形成复合主键。MySQL官方文档原文如下。
All columns used in the partitioning expression for apartitioned table must be part of every unique key that the tablemay have. In other words,every unique key on the table must use everycolumn in the table's partitioning expression(This also includesthe table's primary key, since it is by definition a uniquekey.This particular case is discussed later in this section).
接着深入分析一下业务流程。
1)系统从邮件服务器同步到邮件以后,创建一个工单,createdTime就是工单创建的时间。
2)客服先去查询无处理人的工单,然后把工单分派给自己。
3)客服处理工单,每处理一次,系统自动增加一条处理记录。
4)客服处理完工单以后,将工单状态改为“关闭”。
通过跟客服的交流,项目组发现,一般工单被关闭以后,客服查询的概率就很低了。对于那些关闭超过一个月的工单,基本上一年都打开不了几次。
调研到这里,基本的思路是增加一个状态:归档。首先将关闭超过一个月以上的工单自动转为“归档”状态,然后将数据库分为两个区,所有“归档”状态的工单存放在一个区,所有非“归档”状态的工单存放在另外一个区,最后在所有的查询语句中加一个条件,就是状态不等于“归档”。
简单估算一下:客服频繁操作的工单基本上都是1个月内的工单,按照后期一天10万来算,也就是300万的数据,这样数据库的非归档区基本就没什么压力了。
那么,是否就将status设为分区字段,然后直接使用MySQL的分区功能?
不是的。
因为相关的开发人员并没有用过数据库分区的功能,而当时面临的情况是只有1周的时间来解决问题,并且工单表是系统最核心的数据表,不能出问题。
这种情况下,没人敢在生产的核心功能上使用一项没用过的技术,但是项目组评估了一下,要实现一个类似的方案,其实工作量并不大,而且代码可控。因此,项目组放弃了数据库分区,并决定基于同样的分区理念,使用自己熟悉的技术来实现这个功能。
这个思路也很简单:新建一个数据库,然后将1个月前已经完结的工单数据都移动到这个新的数据库。这个数据库就叫冷库,因为里面基本是冷数据(当然,叫作归档数据库也可以),之后极少被访问。当前的数据库保留正常处理的较新的工单数据,这是热库。
这样处理后,因为客服查询的基本是近期常用的数据,大概只有300万条,性能就基本没问题了。即使因为查询频繁,或者几个客服同时查询,也不会再像之前那样出现数据库占满CPU、整个系统几乎宕机的情况了。
上面这个方法,其实就是软件系统常用的“冷热分离”。接下来介绍一下冷热分离的方案。
冷热分离就是在处理数据时将数据库分成冷库和热库,冷库存放那些走到终态、不常使用的数据,热库存放还需要修改、经常使用的数据。
假设业务需求出现了以下情况,就可以考虑使用冷热分离的解决方案。
1)数据走到终态后只有读没有写的需求,比如订单完结状态。
2)用户能接受新旧数据分开查询,比如有些电商网站默认只让查询3个月内的订单,如果要查询3个月前的订单,还需要访问其他的页面。
来源:
https://www.toutiao.com/article/7117549424783557159/?log_from=0f4ca551ca58e_1657522573009
“IT大咖说”欢迎广大技术人员投稿,投稿邮箱:aliang@itdks.com
来都来了,走啥走,留个言呗~
IT大咖说 | 关于版权
由“IT大咖说(ID:itdakashuo)”原创的文章,转载时请注明作者、出处及微信公众号。投稿、约稿、转载请加微信:ITDKS10(备注:投稿),茉莉小姐姐会及时与您联系!
感谢您对IT大咖说的热心支持!