互联网的迅猛发展引发了大量数据存储的挑战,尤其是物联网领域,其中每个智能设备每日都在进行数据的收集与报送,日产量可达到数千万乃至上亿级别。在互联网电商领域,亦或是在某些O2O平台上,日订单数据量可达到千万级别,如此庞大的数据量已超出了传统关系型数据库的处理能力。为此,业界提出了分布式存储与计算等应对策略,尤其是NoSql技术生态。在此前我所提及的k-v数据库、文档数据库以及图形数据库等,均为主流的分布式数据库解决方案。
尽管如此,关系型数据库依旧具备其独特的优势,因此它依然作为核心业务的关键数据支撑平台存在。正因如此,关系型数据库不可避免地要面对随着数据量持续攀升而引发的巨大数据处理挑战。
Mysql数据库海量数据带来的性能问题
当前,绝大多数互联网企业普遍使用MySQL这一开源数据库系统。依据阿里巴巴发布的《Java开发手册》中的内容,一旦某个单表中的行数突破500万行,或者该表的数据总量超过2吉字节,便会对查询效率造成显著影响。在这种情况下,我们建议对相关表结构进行相应的优化处理。
实际上,500万条数据只是一个中间的参考数值,具体的数据规模和数据库服务器的配置、MySQL的设置密切相关。由于MySQL为了增强性能,会将表的索引加载至内存中,一旦内存容量充足,MySQL便能够将所有数据加载至内存,从而确保查询操作能够顺利进行。
然而,一旦单表数据库规模触及一定阈值,内存便无法容纳其索引,进而导致后续的SQL查询不得不依赖磁盘IO,进而引发性能的下滑。当然,这种情况也与具体表结构的设计密切相关,所有问题的根本原因都是内存的限制。在此情况下,提升硬件配置或许能够迅速实现性能的显著提升。
ize 包含数据缓存、索引缓存等。
Mysql常见的优化手段
当然,首要任务是针对Mysql数据库本身进行优化,其中常用的方法包括:
这些常用的改进策略在数据规模有限时能显著提升效果,然而,当数据量增长至某个临界点后,常规的改进方法便无法有效应对实际问题,那我们该如何是好呢?
大数据表优化方案
对大数据表进行优化,最直接的方法是降低单个表的数据规模,因此,通常采用的策略包括:
实际上,这些策略主要针对业务层面,并非全然技术导向,因此在执行过程中,必须依据业务的具体特点来挑选恰当的方法。
详解分库分表
分库分表是一种针对数据表规模庞大的常见优化手段,其核心理念是将庞大的数据表分解为若干较小的数据表。这一过程亦被称作数据分片。实际上,其原理与传统数据库中的分区表机制颇为相似,例如MySQL和都具备这样的分区表功能。
分库分表技术是一种实现水平扩展的方法,它将原本整体的数据集划分为若干个分片,每个分片仅包含部分数据。这种将复杂问题分解为多个简单部分解决的思想在信息技术领域颇为常见,例如多核CPU、分布式系统架构、分布式缓存等。以我们之前讨论的redis集群为例,其中slot槽位的分配便体现了数据分片的概念。
如图6-1所示,数据库分库分表一般有两种实现方式:

图6-1
垂直拆分
垂直拆分可划分为两种形式,其一为单一数据库的垂直拆分,其二为涉及多个数据库的垂直拆分。
单库垂直分表
建议对单张表的字段数量进行限制,保持在20至50个左右,之所以提出这样的限制,是因为一旦字段数量加上数据累积的总长度超过了某个特定值,数据便无法存储在同一页上,从而引发分页现象。分页问题会直接影响到查询的效率,导致性能降低。
因此,在遇到某些业务表格字段数量较多的情况,我们通常采取垂直拆分的方法,将单一表格的字段分散到多个表格中,具体操作如图6-2所示,例如,将订单表拆分为订单主表与订单明细表两部分。

图6-2
在引擎中,单表字段最大限制为1017
参考:
多库垂直分表
多库垂直拆分实质上是对同一数据库内多个表格进行划分,依照特定维度将它们分散至不同的数据库中,如图6-3演示的那样。此拆分策略在微服务架构领域同样普遍,通常依据业务维度对数据库进行拆分,而这个维度同样会对微服务的划分产生影响,通常情况下,服务与数据库是相互独立的。

图6-3
多库垂直拆分带来的首要优势在于实现了业务数据的独立存储。此外,它还能有效减轻数据库的请求压力。在所有表都集中在一个数据库中时,所有请求都会集中作用于单一数据库服务器。而通过拆分数据库,请求得以分散,从而在提高数据库处理能力方面发挥了积极作用。
水平拆分
采用垂直分割的策略并未彻底解决单个表格数据量庞大的难题,因此我们仍需借助水平分割的方法,对大规模的表格数据进行有效的数据分片处理。
水平切分也可以分成两种,一种是单库的,一种是多库的。
单库水平分表
如图6-4所示,这张用户表原本包含10000条数据,依据特定规则,已被分割为四部分,每部分对应的数据条目均为2500条。

图6-4
两个案例:
银行的交易记录详单,要求所有资金往来均需在此表中予以记录。鉴于客户大多查询的是当日及一个月内的交易信息,故此,我们依据查询频率,将该详单细分为三份独立表格。
当天表:只存储当天的数据。
在夜间,我们执行一项预定任务,将前一天收集的数据,悉数转移至当月的数据表中,使用的是“转入”这一操作,紧接着。
历史数据的处理方式与之前相同,依赖定时任务进行操作。此类任务负责将那些记录时间超过30天的信息,转移至历史数据库。鉴于历史表数据量庞大,我们采取按月分区的策略,每月创建一个新的分区来存储数据。
合作线下商家后,为客户办理贷款业务,需向商家支付一定的返利,亦即提成,此过程每日都会累积大量费用数据。为便于管理,我们每月都会编制一张详细的费用报表,例如……。
然而,需留意的是,与分区类似,此方法虽能在一定程度上缓解单表查询的效能问题,却无法彻底克服单机存储的局限性。
多库水平分表
多库水平分表的设计理念与分库分表的综合解决方案有相似之处,它通过分表减少了单个表的数据规模,而在分库层面,则有效缓解了单个数据库访问的性能压力,具体如图6-5所示。

图6-5
常见的水平分表策略
分库设计主要侧重于业务之间的相互依赖程度,即确定每个数据库应包含哪些表,这一决策通常在初期领域建模阶段就已经确定,因此并不会造成太大困扰。然而,分库实施后可能引发的其他问题,我们将在后续章节中进行详细探讨。
在分表这一环节,我们需要思考的问题更为繁杂,具体而言,便是我们应采取何种策略以实现表的横向拆分?这便引出了分表策略的讨论,接下来,我将简要介绍几种最为普遍的分片方法。
哈希取模分片
哈希分片技术,实际上是通过选取表中的一个特定字段,运用哈希算法计算出对应的哈希值,进而通过取模运算来决定数据应当存放在哪个分片中,具体操作如图6-6所示。这种方法特别适用于需要进行随机读写的情况,它能够有效地将一个庞大的表中的数据均匀地分散到若干个较小的表中。

图6-6
hash取模的问题
对数据进行hash取模处理时,存在一个显著的问题。假如依照当前数据表的数量及其增长趋势,我们将一个大型表格拆分成了四个较小的表格,看似暂时能满足使用需求。然而,在经过一段时间的实际运行后,我们发现这四个表格已不足以容纳数据,不得不额外增加四个表格以存储更多信息。在这种情形下,我们必须对原有的数据进行全面的迁移,整个过程相当繁琐。
通常情况下,为了降低此类方法在数据迁移过程中造成的影响,我们倾向于运用一致性哈希算法。
一致性hash算法
在之前我们所讨论的hash取模算法中,它实际上是对目标表或数据库执行hash取模操作。若目标表或数据库在数量上发生变动,便需对所有数据进行迁移。为了降低这种大规模数据迁移带来的影响,我们才提出了一致性hash算法。
如图6-7所示,简言之,一致性哈希将整个哈希值域构建为一个虚拟的环形结构。以某哈希函数H的值域为0至2的32次方减1(即32位无符号整数)为例,这究竟意味着什么呢?
通过将0至232减1的数字排列成一个环形结构,其中圆环顶部对应的是0,紧接着0的右侧是1,依次类推,直至232减1,换句话说,0左侧的第一个位置对应的是232减1。这样的环形由2的32次方个点构成,我们将其命名为hash环。

图6-7
一致性哈希算法与之前的虚拟环有何关联呢?让我们重新回顾一下之前所阐述的hash取模的案例。设想目前存在四个表格,分别为表一、表二、表三和表四。在一致性哈希算法的应用中,取模操作并非直接作用于这四个表格,而是针对2的32次方进行。
hash(table编号)%232
利用该公式计算得出的数值必然位于0至232减1的整数范围内,随后需在该数值所指示的位置上标记目标表格,具体可参照图6-8,四个表格在经过hash取模操作后,各自将落在hash环的特定位置。

图6-8
截至目前,我们已将目标表与hash环相连接。接下来,我们要将一条数据存入某个目标表,具体操作如下:参照图6-9,在添加数据时,依然通过hash算法与hash环的取模运算确定一个目标值,随后根据该目标值在hash环中的位置,顺时针寻找邻近的目标表,并将数据存入该目标表中。

图6-9
或许大家已经察觉到一致性哈希的优势,其特点是哈希计算并非直接针对目标表,而是针对哈希环。这一优势在于,当需要移除或添加表格时,对数据整体变化的影响仅限于局部,而非全局范围。以一个实例来说明,若我们察觉到有必要添加一张新的表格,参照图6-10,增补这样一个表格不会对那四个已经录入数据的表格产生任何影响,而之前分片的数据也无需进行任何调整。
若需移除某个节点,此举仅会对该节点自身的资料造成影响,而不会对节点前后的数据产生任何作用。

图6-10
hash环偏斜
该设计存在一个缺陷,从理论上看,我们的目标表本应均匀地分布在hash环的各个部分,然而,实际情况却可能如图6-11所展示的那样,出现了hash环倾斜的情况。这种倾斜现象的直接后果是,大量数据集中存储在同一个表中,从而导致了数据分配的极度不均。

图6-11
要解决这一问题,首先必须确保目标节点在hash环上分布均匀,然而实际可用的节点仅有四个。那么,如何实现均匀分布呢?一个简便的方法是将这四个节点各自复制一份,并将它们分散至hash环内。这些复制的节点被称为虚拟节点。根据实际需求,可以创建多个虚拟节点,如图6-12所示。

图6-12
按照范围分片
对数据进行区域划分,实则依托于数据表的特定属性,依照一定的标准进行分割,这个范围涵盖了众多释义,例如:
如图6-7所示,表示按照数据范围进行拆分。

图6-7
在确定范围分片时,关键在于挑选一个恰当的分片键。其适宜性取决于具体业务需求。以一位从事智能家居业务的学员为例,他们销售硬件设备,这些设备会收集数据并上传至服务器。当全国范围内的数据集中存储于单一表格中时,数据量可达到亿级规模。因此,在这种情境下,按城市和地域进行分片显得尤为适宜。
分库分表实战
为了帮助大家深入领会分库分表的概念和实际操作,我们选取了一个典型案例进行展示。具体代码请参考:-split-table-项目。
假设存在一个用户表,用户表的字段如下。
该表主要提供注册、登录、查询、修改等功能。

图6-8
该表格所涉及的业务细节如下,需特别注意:在实施分表操作前,务必深入了解业务层对该表的使用状况,随后根据实际情况选择适宜的解决方案。若忽视业务需求而单纯从技术角度出发设计方案,则无异于本末倒置。
用户端: 前台访问量较大,主要涉及两类请求:
运营端主要负责后台信息的检索,需实现基于性别、手机号码、注册日期以及用户昵称等条件进行分页式的数据查询功能。鉴于其为内部系统,访问频率不高,因此对系统可用性的稳定性要求并不严格。
根据uid进行水平分表
鉴于绝大多数查询需求都是基于用户ID来检索用户信息,因此我们毫无疑虑地决定采用用户ID来进行数据表的横向拆分。为此,我们选择了基于用户ID的哈希取模算法来执行分表操作,具体操作步骤详见图6-9,通过用户ID进行一致性哈希取模计算,从而确定目标表并执行数据存储。

图6-9
依照图6-9的布局,需对表格进行逐一复制,并将复制品分别命名为01至04,具体操作如图6-10所示。
图6-10
如何生成全局唯一id
动作执行完毕后,便需着手实际执行,在此过程中,需确保数据在添加、更新、移除时准确导向相应目标数据表,同时,还需关注旧有数据的迁移工作。
在处理老数据迁移时,我们通常编写脚本或程序,从旧表中提取数据,依照分表规则将其重新分配至新表。这一过程相对简单,故不再详细阐述。接下来,我们将着重讨论数据新增、修改及删除的路由配置问题。
在着手执行之前,我们必须先深入思考一个至关重要的议题:在单一表中,我们依赖递增的主键来确保数据的独一无二性。然而,若将数据分散至四个不同的表中,每个表各自实施独立的递增主键生成规则,便可能产生重复的ID,换言之,递增主键将无法实现全局唯一性。
我们必须认识到,尽管数据被分散到了多个表格中,但其核心仍应视为一个统一的数据集合。一旦出现ID的重复,数据便丧失了其独特性。鉴于此,我们必须思考并设计出一种方法来创建一个全球范围内的唯一标识符。
如何实现全局唯一ID
全局唯一ID的独特之处在于确保了其唯一性,基于这一特性,我们能够轻松地发现众多可行的解决方案。
分布式ID的特性数据库自增方案
在数据库中,我们特设了一张序列表,借助该表中自动增长的ID,为其他业务数据生成一个独一无二的全球ID。当需要使用ID时,只需从该表中直接提取即可。
CREATE TABLE `uid_table` (
该字段为`id`,属于`bigint(20)`类型,不得为空,且会自动递增。
业务标识符为整数型,长度为11位,不得为空。
设定主键字段为`id`,并采用B树索引进行存储。
UNIQUE (business_type)
)
在应用程序内,每当执行该段代码,便能不断获取一个数值逐次上升的唯一标识符。
begin;
向uid_table表中的business_id列插入数据,具体值为2。
SELECT LAST_INSERT_ID();
commit;
每次操作中,删除原有的数据后,增加一条新记录,这样便确保了每次获取的都是一个递增的唯一标识符。
该方案易于操作,然而也存在不足,主要体现在对数据库的负担较重,且建议单独设立数据库,但这又会提升整体开销。在美团的leaf项目中,针对这一问题,他们实施了一种相当巧妙的设计策略,具体内容将在后续进行介绍。
优点:
缺点:
UUID
UUID的构成如下:由四个部分组成,每部分包含四个字符,整体以短横线分隔,共计36个字符。首先,它是一个32位的二进制数,随后被转换成16进制表示,最终以四个短横线连接形成的字符串形式呈现。
UUID的五种生成方式
Java语言中,支持使用MD5算法生成UUID,同时也支持通过随机数生成UUID。
优点:
缺点:
雪花算法
算法,本质上是一种开源的分布式ID生成技术。其核心理念在于,采用一个64位的long类型数字来生成全局唯一的ID。雪花算法是一种较为普遍的算法,在百度和美团的Leaf系统中,都应用了雪花算法的具体实现。
图6-11展示了雪花算法的构成,它由64位数据组成,而这64位数据则进一步划分为四个不同的部分。

图6-11
分库分表之后的数据DML操作
有序需要用到全局id,所以在表需要添加一个唯一id的字段。

图6-12
配置完成之后,在如下代码中引入方法。
@Slf4j
@RestController
@RequestMapping("/users")
public class 用户信息控制器 {
@Autowired
定义了一个名为userInfoService的IUserInfoService接口类型的变量。
创建了一个SnowFlakeGenerator对象,其参数分别为1、1和1。
@PostMapping("/batch")
public方法中,通过@RequestBody注解接收一个列表参数,该参数类型为List。 userInfos){
记录信息:"UserInfoController.user方法执行开始。"
userInfoService执行了批量保存操作,针对userInfos集合;
}
@PostMapping
公开方法signal,接收一个名为userInfo的用户信息对象,通过@RequestBody注解指定该对象由请求体提供。
获取到业务ID:Long bizId = 雪花生成器生成的新ID();
userInfo.setBizId(bizId);
获取业务标识符的字符串表示,通过一致性哈希算法,调用ConsistentHashing类的getServer方法,进而获取对应的服务器地址,并将结果赋值给table变量。
系统日志记录显示:“UserInfoController中的signal方法执行,相关数据表信息为:”,随后跟上了table的具体内容。
MybatisPlusConfig中的TABLE_NAME属性被设置为table。
userInfoService执行了保存操作,针对传入的userInfo对象。
}
}
同时,必须增设一个拦截装置,专门对表格进行拦截并执行替换操作,以此达到对动态表格进行路由的目的。
@Configuration
public class MybatisPlus配置类 {
public static ThreadLocal TABLE_NAME = new ThreadLocal<>();
@Bean
创建一个MybatisPlus拦截器实例,并返回该实例。
创建了一个MybatisPlusInterceptor对象,并将其赋值给interceptor变量。
PaginationInnerInterceptor类型的paginationInnerInterceptor对象被创建,其构造时指定了数据库类型为MySQL。
拦截器对象添加了分页拦截器,即paginationInnerInterceptor。
创建了一个名为DynamicTableNameInnerInterceptor的实例,该实例属于DynamicTableNameInnerInterceptor类。
Map创建了一个新的HashMap对象,用于存储tableName与Handler之间的映射关系。<>();
tableNameHandlerMap存储了键值对,将"user_info"作为键,将一个lambda表达式作为值,该表达式接受sql和tableName作为参数,并返回TABLE_NAME所获取的值。
dynamicTableNameInnerInterceptor 采用了 tableNameHandlerMap 作为其表名处理器的映射设置;此操作已成功执行。
拦截器对象引入了动态表名内部拦截器;将其设置为内部拦截器。
return interceptor;
}
}
至此,基础的分库分表实践已经告一段落,然而,问题尚未得到彻底的解决。
非分片键查询
我们的表分片操作是依托于某种机制进行的,这也就是说,在查询某张表中的数据之前,我们得先通过路由定位到相应的表,只有这样,才能成功获取所需信息。
那么,问题随之产生,若所查询的字段并非分片键,即非分片键字段,以本次分库分表的实战案例为例,运营端在查询时可能会依据姓名、手机号码、性别等不同字段进行检索,这时我们便难以确定应从哪张表中获取这些信息。
非分片键和分片键建立映射关系
采取的第一种方案是,将非分片键与分片键之间构建起一种对应关系,例如:创建这种对应关系,就如同搭建了一个基础索引。在执行基于查询数据的操作时,首先通过这个映射表找到相应的数据,然后据此锁定目标表。
映射表仅包含两列,能够容纳大量数据。当数据规模增大时,我们还可以对映射表进行横向拆分。实际上,这种映射机制本质上等同于k-v键值对,因此我们可以借助k-v缓存技术来优化存储性能。
这种映射关系的变动频率极低,因此缓存的成功率相当高,同时系统性能表现优异。
用户端数据库和运营端数据库进行分离
运营端的查询并不仅限于对单一字段的匹配查找,还可能包括更为复杂的查询需求,例如分页查询等。这类查询对数据库的性能有着显著影响,甚至可能对用户端操作用户表造成干扰。因此,通常的做法是将数据库进行分离处理。
由于运营端对数据的一致性和可用性要求并不严格,且无需实时查询数据库,因此我们可将C端用户表的信息同步至运营端的用户表中,同时,运营端的用户表无需进行分表处理,直接进行全量查询即可。
自然,若后台操作速度过于迟缓,我们亦能借助搜索引擎来应对后台的繁杂查询需求。
实际应用中会遇到的问题
在实际操作过程中,往往并非一开始便意识到未来需要对这一表格进行拆分,故而,在多数情况下,我们遇到的问题往往是在数据量已达到一定程度的限制时,才着手思考如何解决这一问题。
因此,分库分表面临的最大挑战并非拆分的策略本身,而在于对运行已久的数据库,如何依据实际业务状况挑选恰当的拆分策略,并在拆分前深思熟虑数据迁移的方案。此外,在整个数据迁移与拆分作业期间,系统必须确保其正常运行不中断。
对于运行中的表的分表,一般会分为三个阶段。
阶段一,新老库双写
鉴于旧的数据表在规划时并未充分考虑未来分表的布局,加之业务不断更新迭代,部分模型亦需进行改进,故而需创建一张新表以容纳旧数据。在这一过程中,必须完成以下几项任务:
阶段二,以新的模型为准
到了第二个阶段,历史数据已经导完了,并且校验数据没有问题。
阶段三,结束双写
到了第三个阶段,说明数据已经完全迁移好了,因此。
分库分表后带来的问题
分库分表带来性能提升的好处的同时,也带来了很多的麻烦,
分布式事务问题
在实施分库分表策略后,原先单一数据库内的事务处理转变为涉及多个数据库的操作,如何确保这些跨库数据的一致性,便成为了一个普遍的挑战。例如,在图6-13中,当用户创建订单时,必须同时在订单数据库中存储订单信息,并对库存数据库中的商品库存进行更新,这就触及了跨库事务一致性的问题。换句话说,我需要确保这两个事务要么同时完成,要么同时失败。

图6-13
跨库查询
在检索合同相关资料时,必须将客户资料纳入考量,但鉴于合同资料与客户资料分别存储于不同的数据库系统中,因此我们无法直接通过join操作来执行这种关联性的查询。
我们有几种主要的解决方案:
上述方法都是通过科学的数据布局来规避跨库之间的关联查询,实际上在我们的日常业务操作中,我们同样力求避免采用跨库关联查询。一旦遇到此类情况,我们需对业务流程或数据拆分策略进行深入分析,以确保其合理性。若即便如此,跨库关联查询仍不可避免,那么我们便只能采取最后的解决方案。
从各个数据库节点筛选出满足特定条件的数据,对这些数据进行重新整合,最终将处理后的信息反馈至用户端。
扫一扫在手机端查看
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。


客服1