分库分表

分库分表是为了解决数据库读写的性能问题。

分片方案

分库分表的分片方案包括:应用层分片中间件分片

分片方案


应用层分片: 分片逻辑耦合在应用系统中,直连数据库。

优势: 在于直连,性能更好,运维成本低。

劣势: 由于耦合在应用系统中,升级问题较为麻烦,需要每个应用重新升级发布。

常见的方案有Sharding-jdbcTDDL


中间件分片: 分片逻辑作封装为独立中间件,独立部署,作为应用系统与数据库之间的代理。

优势: 对应用系统无入侵,独立部署后也仅需升级中间件。

劣势: 独立部署运维成本高,代理模式增加性能消耗。

常见的方案有AtlasMycatCobar.


数据切分

数据的切分方式分为两种:垂直切分水平切分

垂直切分是基于业务边界拆分的,数据相对独立,拆分几乎无副作用。

水平切分是基于业务逻辑拆分的,是为了解决大表的拆分策略。

在系统演进的过程中一般都会先经历垂直切分然后再经历水平切分

垂直切分

垂直切分是指按照业务将表进行分类,重新分配到不同的数据库上,从而实现请求压力的拆分。

垂直切分

优点:

  • 切分规则清晰(划分数据边界)
  • 切分后数据维护性不变(原有SQL无影响)
  • 切分后请求压力会由不同业务数据库独自承担(业务之间无影响)

缺点:

  • 不同业务之间的事务处理复杂(涉及到分布式事务)
  • 不同业务之间的表无法JOIN,需要做关联查询(HTTP/RPC

在垂直切分情况下,可能存在一张表拆分为多张表的情况,那么原有的直接查询可能就需要修改为关联查询

为了避免关联查询带来的性能损耗,在满足业务的情况下,可以冗余部分字段信息来避免关联查询。

水平切分

水平切分是指按业务逻辑对表中数据行的切分,把单表数据分配到多个数据库及多个表中,从而实现请求压力的拆分。

水平切分注重的是大表的切分,是基于大表中某个字段某种规则的切分。

什么是大表?

业界公认MySQL单表容量在1KW以下是最佳状态,因为这时它的B+TREE索引树高在3~5之间

水平切分

优点:

  • 切分后不存在单库大表,读写性能更好

缺点:

  • 切分规则会对SQL存在影响(任何SQL都需要带有Sharding Key
  • 跨库查询性能差,跨库事务处理复杂(涉及到分布式事务)
  • 需要考虑后期数据的二次扩展
  • 数据归档与归档查询
  • 跨库JOIN性能差

切分策略

由于垂直切分是基于业务边界的切分,需要问题在于业务边界的划分,这里不再讨论。

这里仅分析水平切分

切分策略可以分为:Hash切分Range切分

数据切分都是基于Sharding Key的,而Sharding Key的选择与业务SQL是强关联的。

例如,数据库应存在1亿的用户数据,此时需要对用户表进行切分,考虑到业务场景下,大部分的请求都是基于用户纬度来查询的,因此使用用户ID作为Sharding Key来做数据切分。

Hash切分

Hash切分是最为常用的切分策略,一般采用取模MOD来切分数据表,但其水平扩展会造成数据的迁移

Hash切分

建议取模方式:MOD(2^n),可以避免部署数据的迁移,例如,

1
10 % 4 == 10 % 8 == 2

考虑到水平扩展的性能,可以使用一致性Hash算法来切分数据。

一致性Hash切分

与哈希方法一样,Hash环利用环形形状来表示取模MOD后节点分布情况。

但是在节点扩容的情况下,不再需要

一致性Hash的优势在于:节点扩容和缩容不会导致大量数据的迁移。

一致性Hash的关键:

  • 顺序针寻找最近的节点作为存储数据存储节点
  • 利用虚拟节点来均衡各个节点的负载情况

一致性Hash

Range切分

Range切分是利用数据时间ID来切分数据表,常用于日志、归档数据的切分,是一种顺序切分。

切分规则可控,水平扩展不会造成数据迁移,扩展更容易。

由于数据是按顺序切分的,无法解决集中写入瓶颈的问题,而且存在跨库跨表查询性能查。

Range切分

拆分数量

数据库一般被部署在物理机上,其性能受物理机的影响。

数据库的性能与CPU、内存有直接关系。

常见情况下,一台物理机部署多个数据库实例,多个数据库实例共享同一台主机的性能。

由于物理机的性能有限,所以在同一台物理机上增加过多的实例是无效的。

集群性能的定义:

Sharding集群性能 = Sharding集群物理机的数量 * 单台物理机的性能

具体拆分方式不仅需要通过业务场景来决定的,而且需要DBA合理部署Sharding集群,从而才能实现Sharding集群性能的最大化。

分布式唯一ID

在单库单表的情况下,直接使用数据库自增ID

在分库分表的情况下,数据分布在不同库表上,无法使用数据库自增特性,需要使用分布式唯一ID

UUID

UUID一般通过本地网卡、时间和随记数来生成。

  • 16字节128位,36个字符长度不适合做主键,存储成本高
  • 无序,作为主键时会导致B+树索引在写的时候有过多的随机写操作

聚集主键的InnoDB会按照主键进行数据排序,因此要求分布式唯一ID具有有序性

Redis

Redis提供了incr/increby原子操作,从而可以用来生成分布式ID。

  • 基于内存自增生成ID,性能较好
  • 内存自增操作保证ID有序
  • 单机部署存在可用性问题,需要集群解决单点问题
  • 双击部署、数据持久化

数据库在插入数据时对分布式ID是强依赖,如果出现问题会导致无法正常插入数据,所以需要保证分布式ID生成器的高可用。

使用incr仅可以生成一个ID;
使用increby可以批量生成多个ID,可用于批量插入数据;

Snowflake

SnowflakeTwitter利用Zookeeper实现了一个全局ID生成的服务。

Snowflake生成的分布式唯一ID的组成 (由高位到低位):

  • 41 bits: Timestamp (毫秒级)
  • 10 bits: 节点 ID (datacenter ID 5 bits + worker ID 5 bits)
  • 12 bits: sequence number

雪花算法

优点:

  • 高性能,低延迟
  • 按时间有序

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态
其他

Leaf——美团点评分布式ID生成系统

数据迁移

数据迁移是分库分表的重要工作,按照切分逻辑把历史数据迁移至新的分库分表数据库中。

常见的两种方式:停机迁移在线迁移

停机迁移

数据迁移过程中需要停止相关服务,优势在于迁移过程简单。

由于迁移过程中不会产生新的数据,所以迁移过程仅需要一次,迁移完成后恢复服务运行。

此方法仅适用于那些调用频次低、暂时停机无影响的服务。

在线迁移

数据迁移过程中服务正常运行,优势在于不影响系统运行,但迁移过程复杂。

迁移步骤为:

  • 在不影响主库的情况下,基于从库同步历史数据到目标库表中
  • 上一步同步完成后,切换数据写入开关状态到双写状态1(同步写入历史库表、异步写入切分库表)
  • 定期检查历史库表和切分库表的数据差异,如果存在数据差异,再次同步差异数据到切分库表
  • 当数据基本无差异的情况下,切换数据开关状态到双写状态2(同步写入切分库表、异步写入历史库表),此时读写迁移至切分库表
  • 定期检查历史库表和切分库表的数据差异,如果存在数据差异,再次同步差异数据到历史库表
  • 稳定运行一段时间后才可以停止历史库表的写入,否则无法实现切分的回滚

双写状态这里需要注意下,尽量避免双数据源的同步写入,辅助同步逻辑可以通过异步方式实现。

数据迁移

使用场景

只有当数据库性能不能满足业务发展时,才真的需要进行分库分表。

分库分表只会给系统增加复杂度,原本一条SQL可以处理的数据,跨库操作存在大量的网络延迟及数据聚合,将会带来大量的性能损耗。


分库分表不是必须的!

有些场景下,数据是可以通过归档的方式来减少数据的量级。

ES+HBase是比较常见的归档方案,其中,ES仅存储索引字段,HBase存储全量数据。

参考

数据库分库分表基础和实践
MySQL分库分表方案
大众点评订单系统分库分表实践
分库分表最佳实践