数据库分库分表
关系型数据库本身容易成为系统瓶颈,单机存储量、连接数、处理能力有限,当单标数据量达到1000万或大于100G时,即使读写分离、优化索引,性能还是明显下降,此时需要考虑数据切分。
数据库分布式的核心就是数据切分以及切分后数据的定位、整合。将数据切分到多个数据库中,通过扩展主机的数量缓解单一数据库瓶颈。
1.数据切分
1.1 垂直切分
垂直分库
根据业务系统拆分,将耦合性低(关联度低)的表存储在不同的数据库中,类似于按照业务将大系统拆分多个子系统,做法与微服务治理相似,每个微服务依赖独立的数据库。例如拆分用户、订单、优惠、支付等
垂直分表
根据表字段拆分,例如将访问频率低、修改频率高的字段单独拆分出来,提高读写操作。大表拆小表可以避免分页、且方便开发维护,Mysql是将叶子节点设置为页的大小一次性加载的,如果大表的字段过多可能会导致分页。
优点
按照业务拆分,系统层级解耦,业务清晰。
方便开发维护,根据业务进行维护、监控、扩展等。
高并发场景下,垂直切分一定程度提升IO性能/连接数,缓解单机硬件瓶颈。
缺点
分布式事务处理复杂
部分表无法join,增加开发难度
依然存在单表数据量大的问题(需要水平切分)。
1.2 水平切分
当一个应用垂直拆分后,单库单表的数据量依然巨大,依然存在读写性能瓶颈,此时可以考虑水平切分。
库内分表
库内分表解决了单一表数据量过大的问题,但没有将表分不到不同机器的库上,无法解决单机硬件瓶颈,如CPU/IO/内存连接数等。
分库分表
既解决了单一表数据量过大的问题,且将表分不到不同机器的库上,有效缓解单机硬件资源瓶颈。
优点
不存在单库单表数据量过大、高并发瓶颈问题
应用端改造小,不需要拆分业务
缺点
分布式事务处理复杂
跨库join关联查询性能较差(开启FEDERATED引擎,且以此引擎建立中间表,CONNECTION=‘mysql://root:root@localhost:3306/b/school’)
数据扩展、维护难度大
1.2.1 切分方式
数值范围
优点
单表大小可控
水平扩展方便,扩容时只需添加节点,无需对原有分片迁移
使用分片字段范围查询时,快速定位分片,有效避免跨分片查寻
缺点
热点数据成为瓶颈。例如按照时间分片,存储最近时间的分片可能会被频繁的读写,成为热点数据,而存储历史数据的分片很少被读写。
不使用分片字段查询,需要跨分片查询
Hash取模
优点
数据分散相对均匀,不易出现热点数据
缺点
扩容复杂,需要迁移原有分片数据(hash取模,模数变了)
范围查询时,需要跨分片查询
不使用分片字段查询,需要跨分片查询
2.分库分表带来的问题
2.1 事务一致性
2.1.1 分布式事务
指事务操作位于不同的节点上,需要保证事务的ACID特性。
例如下单场景,订单和库存位于不同的节点上,涉及分布式事务问题。
本地消息表
消息表与业务表位于同一数据库,能够利用本地事务保证对两个表的操作满足事务特性,并利用消息队里保证最终一致性。
- 分布式事务一方将数据写入业务表后写入本地消息表,本地事务保证一定可以写入本地消息表。
- 写入本地消息表后将小发送到MQ消息队列,发送成功,将本地消息表删除,否则重发。
- 分布式事务另一方监听消息队列,读取消息执行操作。
2PC
两阶段提交(Two Phase Commit,2PC) ,通过引入协调者(Coordinator)来协调参与者,并最终决定这些参与者是否真正提交事务。
执行过程
- 协调者询问参与者,参与者返回事务执行结果
- 如果所有参与者返回执行结果成功,协调者通知所有参与者提交事务,否则回滚
存在问题
同步阻塞:所有参与者需要等待最后参与者的回馈结果
单点问题:协调者出现问题,会有 严重影响,尤其是第二阶段,所有参与者都要等待协调者通知
数据不一致:第二阶段通知提交出现网络问题,部分参与者提交事务
太过保守:一个参与者出现问题,所有参与者都要回滚,没有容错机制
2.1.2 最终一致性
对于性能要求很高,但对实时一致性要求不高的系统,可采用事务补偿的方式。一些常见的做法有:基于日志对比、基于标准数据源同步对比。
2.2 跨节点关联查询join问题
表冗余(全局表)
将系统中所有模块都依赖的一些表,避免跨库join查询,在每个库中都保留一份,一般这些表修改频率较低,不用担心一致性的问题。
字段冗余
一种典型的反范式设计,以空间换时间。例如订单表中存储用户部分信息。
数据组装(业务层)
在业务层分多次查询组装数据。
ER分片
将有关联度额数据分散在同一分片上,如根据用户id分片(订单中用户id外键),同一用户及其下的订单分散到同一分片。
2.3 夸节点的分页、排序、函数等问题
当分片规则是按照数值范围切分、且排序规则按照分片字段排序,可以快速定位分片。
其他情况都比较复杂,为了保证准确性,需要将所有节点的前N页数据排序后汇总,然后再整体排序后丢弃绝大部分数据,只返回第N页数据,如果N的数值很大,则会造成系统资源、IO/CPU/MEM的极大浪费。Elasticsearch等搜索引擎的深度分页问题与此类似。
当使用Max/Min/Sum/Count等函数时,需要在所有节点执行函数,汇总后再次计算返回结果。
2.4 全局主键
UUID
方案简单、无网络开销,但32位16进制的UUID会占用大量存储空间,且因UUID的无序性,建立索引、及根据索引查询性能较差。
数据库唯维护主键表
如利用数据库唯一索引等。
Snowflake分布式自增ID算法
41位时间戳+10位机器ID+12位序列号,MongoDB的主键生成策略与此类似
毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高
强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复
Redis原子操作生成
自定义规则 + INCR key (对存储在指定key的数值执行原子的加1操作)
如年月日时间戳 + 原子操作自增生成主键,此策略有序且易读,例如用户注册、用户下单等场景,年月日表示注册、下单日期,自增数量表示当日注册数量、下单数量。
依赖Reids,当Redis资源不够时,对主键生成有较大影响。
2.4 数据迁移、扩容
当使用数值范围分片时,扩容只需增加节点即可,无需对原有分片进行迁移
当使用数值取模分片时,扩容需要对原有分片数据重新取模分片。
3.何时考虑切分
3.1 能不切分尽量不要切分
3.2 数据量过大,正常运维影响业务访问
3.3 数据快速增长
3.4 业务发展,需要对某些字段垂直拆分
例如用户表里面有最后登录时间(频繁修改)、和用户信息(text类型,占用较大空间且读取频率较低),此时可以考虑将最后登录时间拆分出去、将用户信息拆分出去。