1文章概述
在业务发展初期单表完全可以满足业务需求,在阿里巴巴开发手册也建议:单表行数超过万行或者单表容量超过2GB才推荐进行分库分表,如果预计三年后数据量根本达不到这个级别,请不要在创建表时就分库分表。
但是随着业务的发展和深入,单表数据量不断增加,逐渐成为业务系统的瓶颈。这是为什么呢?
从宏观层面分析任何物体都必然有其物理极限。年英特尔创始人摩尔预测:集成电路上可容纳的元器件的数目,约每隔24个月增加一倍,性能提升一倍,即计算机性能每两年翻一番。
但是摩尔定律会有终点吗?有些科学家认为摩尔定律是有终点的:半导体芯片单位面积可集成的元件数量是有极限的,因为半导体芯片制程工艺的物理极限为2到3纳米。当然也有科学家不支持这种说法,但是我们可以从中看出物理极限是很难突破的,当单表数据量达到一定规模时必然也达到极限。
从细节层面分析我们将数据保存在数据库,实际上是保存在磁盘中,一次磁盘IO操作需要经历寻道、旋转延时、数据传输三个步骤,那么一次磁盘IO耗时公式如下:
单次IO时间=寻道时间+旋转延迟+传送时间
总体来说上述操作都较为耗时,速度和内存相比有着数量级的差距,当数据量过大磁盘这一瓶颈更加明显。那么应该怎么办?处理单表数据量过大有以下六字口诀:删、换、分、拆、异、热。
删是指删除历史数据并进行归档。换是指不要只使用数据库资源,有些数据可以存储至其它替代资源。分是指读写分离,增加多个读实例应对读多写少的互联网场景。拆是指分库分表,将数据分散至不同的库表中减轻压力。异指数据异构,将一份数据根据不同业务需求保存多份。热是指热点数据,这是一个非常值得注意的问题。
2删
我们分析这样一个场景:消费者会经常查询一年之前的订单记录吗?答案是一般不会,或者说这种查询需求量很小。根据上述分析那么一年前的数据我们就没有必要放在单表这张业务主表,可以将一年前的数据迁移到历史归档表。
在查询历史数据表时,可以限制查询条件如必须选择日期范围,日期范围不能超过X个月等等从而减轻查询压力。
处理历史存量数据比较简单,因为存量数据一般是静态的,此时状态已经不再改变了。数据处理一般分为以下两个步骤:
(1)迁移一年前数据至历史归档表(2)根据主键分批删除主表数据
不能一次性删除所有数据,因为数据量太大可能会引发超时,而是应该根据ID分批删除,例如每次删除条数据。
第一步查询一年前主键最大值和最小值,这是我们需要删除的数据范围:
第二步删除数据时不能一次性全部删掉,因为很可能会超时,我们可以通过代码动态更新endId进行批量删除:
3换
换是指换一个存储介质,当然并不是说完全替换,而是用其它存储介质对数据库做一个补充。例如海量流水记录,这类数据量级是巨量的,根本不适合存储在MySQL数据库中,那么这些数据可以存在哪里呢?
现在互联网公司一般都具备与之规模相对应的大数据服务或者平台,那么作为业务开发者要善于应用公司大数据能力,减轻业务数据库压力。
3.1消息队列
这些海量数据可以存储至Kafka,因为其本质上就是分布式的流数据存储系统。使用Kafka有如下优点:
第一个优点是Kafka社区活跃功能强大,已经成为了一种事实上的工业标准。大数据很多组件都提供了Kafka接入组件,经过生产验证并且对接成本较小,可以为下游业务提供更多选择。
第二个优点是Kafka具有消息队列本身的优点例如解耦、异步和削峰。
假设这些海量数据都已经存储在Kafka,现在我们希望这些数据可以产生业务价值,这涉及到两种数据分析任务:离线任务和实时任务。
离线任务对实时性要求不高,例如每天、每周、每月的数据报表统计分析,我们可以使用基于MapReduce数据仓库工具Hive进行报表统计。
实时任务对实时性要求高,例如根据用户相关行为推荐用户感兴趣的商品,提高用户购买体验和效率,可以使用Flink进行流处理分析。例如运营后台查询分析,可以将数据同步至ES进行检索。
还有一种分类方式是将任务分为批处理任务和流处理任务,我们可以这么理解:离线任务一般使用批处理技术,实时任务一般使用流处理技术。
3.2API
上一个章节我们使用了Kafka进行海量数据存储,由于其强大兼容性和集成度,可以作为数据中介将数据进行中转和解耦。
当然我们并不是必须使用Kafka进行中转,例如我们直接可以使用相关JavaAPI将数据存入Hive、ES、HBASE等。
但是我并不推荐这种做法,因为将保存流水这样操作耦合进业务代码并不合适,违反了高内聚低耦合的原则,尽量不要使用。
3.3缓存
从广义上理解换这个字,我们还可以引入Redis远程缓存,把Redis放在MySQL前面,拦下一些高频读请求,但是要注意缓存穿透和击穿问题。
缓存穿透和击穿从最终结果上来说都是流量绕过缓存打到了数据库,可能会导致数据库挂掉或者系统雪崩,但是仔细区分还是有一些不同,我们分析一张业务读取缓存一般流程图。
我们用文字简要描述这张图:
(1)业务查询数据时首先查询缓存,如果缓存存在数据则返回,流程结束(2)如果缓存不存在数据则查询数据库,如果数据库不存在数据则返回空数据,流程结束(3)如果数据库存在数据则将数据写入缓存并返回数据给业务,流程结束
假设业务方要查询A数据,缓存穿透是指数据库根本不存在A数据,所以根本没有数据可以写入缓存,导致缓存层失去意义,大量请求会频繁访问数据库。
缓存击穿是指请求在查询数据库前,首先查缓存看看是否存在,这是没有问题的。但是并发量太大,导致第一个请求还没有来得及将数据写入缓存,后续大量请求已经开始访问缓存,这是数据在缓存中还是不存在的,所以瞬时大量请求会打到数据库。
我们可以使用分布式锁加上自旋解决这个问题,本文给出一段示例代码,具体原理和代码实现请参看我之前的文章:流程图+源码深入分析:缓存穿透和击穿问题原理以及解决方案(