前面我们了解了分布式数据库的架构,知道各类分布式数据库都离不开计算层、存储层、元数据层这三层关系。另外,很重要的一点是,了解了分布式数据库是把数据打散存储在一个个分片中。在基于MySQL 的分布式数据库架构中,分片就存在于 MySQL 实例中。
本篇文章,我们就来了解一下,如何正确的把数据分片,充分发挥分布式数据库架构的优势。
一、如何确定分片键
在对表中的数据进行分片时,首先要选出一个分片键(Shard Key),即用户可以通过这个字段进行数据的水平拆分。
我们以之前的订单表为例,来看一下如何分析选出分片键:
CREATE TABLE `orders` ( `O_ORDERKEY` int NOT NULL, `O_CUSTKEY` int NOT NULL, `O_ORDERSTATUS` char(1) NOT NULL, `O_TOTALPRICE` decimal(15,2) NOT NULL, `O_ORDERDATE` date NOT NULL, `O_ORDERPRIORITY` char(15) NOT NULL, `O_CLERK` char(15) NOT NULL, `O_SHIPPRIORITY` int NOT NULL, `O_COMMENT` varchar(79) NOT NULL, PRIMARY KEY (`O_ORDERKEY`), KEY `idx_custkey_orderdate` (`O_CUSTKEY`,`O_ORDERDATE`), KEY `ORDERS_FK1` (`O_CUSTKEY`), KEY `idx_custkey_orderdate_totalprice` (`O_CUSTKEY`,`O_ORDERDATE`,`O_TOTALPRICE`), KEY `idx_orderdate` (`O_ORDERDATE`), KEY `idx_orderstatus` (`O_ORDERSTATUS`), CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`O_CUSTKEY`) REFERENCES `customer` (`C_CUSTKEY`) ) ENGINE=InnoDB
对于上面的表orders,可以选择的分片键有:o_orderkey、o_orderdate、也可以是o_custkey。在选出分片键后,就要选择分片的算法,比较常见的有 RANGE 和 HASH 算法。
比如,表 orders,选择分片键 o_orderdate,根据函数 YEAR 求出订单年份,然后根据RANGE 算法进行分片,这样就能设计出基于 RANGE 分片算法的分布式数据库架构:
从图中我们可以看到,采用 RANGE 算法进行分片后,表 orders 中,1992 年的订单数据存放在分片 1 中、1993 年的订单数据存放在分片 2 中、1994 年的订单数据存放在分片 3中,依次类推,如果要存放新年份的订单数据,追加新的分片即可。
不过,RANGE 分片算法在分布式数据库架构中,是一种非常糟糕的算法,因为对于分布式架构,通常希望能解决传统单实例数据库两个痛点:
性能可扩展,通过增加分片节点,性能可以线性提升;
存储容量可扩展,通过增加分片节点,解决单点存储容量的数据瓶颈。
那么对于订单表 orders 的 RANGE 分片算法来说,可以发现以上两点都无法实现,因为当年的数据依然存储在一个分片上(即热点还是存在于一个数据节点上)。
如果继续拆细呢?比如根据每天进行 RANGE 分片?这样的确会好一些,但是对“双 11、618”这样的大促来说,依然是单分片在工作,热点依然异常集中。
所以在分布式架构中,RANGE 分区算法是一种比较糟糕的算法。但它也有好处:可以方便数据在不同机器间进行迁移(migrate),比如要把分片 2 中 1992 年的数据迁移到分片 1,直接将表进行迁移就行。
而对海量并发的 OLTP 业务来说,一般推荐用 HASH 的分区算法。这样分片的每个节点都可以有实时的访问,每个节点负载都能相对平衡,从而实现性能和存储层的线性可扩展。
那么,如上sql表而言,根据o_orderkey进行HASH分片,分片算法如下:
在上述分片算法中,分片键是 o_orderkey,总的分片数量是 4(即把原来 1 份数据打散到 4 张表中),具体来讲,分片算法是将 o_orderkey 除以 4 进行取模操作。
最终,将表orders 根据 HASH 算法进行分布式设计后的结果如下图所示:
可以看到,对于订单号除以 4,余数为 0 的数据存放在分片 1 中,余数为 1 的数据存放在分片 2 中,余数为 2 的数据存放在分片 3 中,以此类推。这种基于 HASH 算法的分片设计才能较好地应用于大型互联网业务,真正做到分布式数据库架构弹性可扩展的设计要求。
但是,表order分片键选择o_orderkey 是最好的选择吗?当然不是。
我们看一下库中的其他表,如表 customer、lineitem,这三张表应该是经常一起使用的,比如查询用户最近的订单明细。如果用 o_orderkey 作分区键,那么 lineitem 可以用 l_orderkey 作为分区键,但这时会发现表customer 并没有订单的相关信息,即无法使用订单作为分片键。如果表 customer 选择另一个字段作为分片键,那么业务数据无法做到单元化,也就是对于表customer、orders、lineitem,分片数据在同一数据库实例上。
所以,如果要实现分片数据的单元化,最好的选择是把用户字段作为分区键,在表 customer 中就是将 c_custkey 作为分片键,表orders 中将 o_custkey 作为分片键,表 lineitem 中将 l_custkey 作为分片键:
这样做的好处是:根据用户维度进行查询时,可以在单个分片上完成所有的操作,不用涉及跨分片的访问。
如以下查询sql:
SELECT * FROM orders INNER JOIN lineitem ON o_orderkey = l_orderkey INNER JOIN customer ON o_custkey = c_custkey WHERE o_custkey = 1 ORDER BY o_orderdate DESC LIMIT 10
所以,分布式数据库架构设计的原则是:选择一个适合的分片键和分片算法,把数据打散,并且业务的绝大部分查询都是根据分片键进行访问。
二、分库分表
其实,前面说的分片本质是一张张表,而不是数据库实例,只是每个分片是在 MySQL 数据库实例中,严格来说:
分片 = 实例 + 库 + 表 = ip@port:db_name:table_name
对于前面的表orders,假设根据 HASH 算法进行分片,那么可以进行如下的分库分表设计:
1、每个分片的表名库名都一样,如库 tpch,表名 orders;
2、每个分片的库名不一样,表名一样,如库名 tpch01、tpch02、tpch03、tpch04,表名orders;
3、每个分片的表名不一样,库名一样,如库名 tpch,表名分别为 orders01、orders02、orders03、orders04;
4、每个分片的库名不一样,表名也不一样,如分片 1 的表在库名 tpch01下,表名为oders01;分片 2 的表名在库名 tpch02,表名为 orders02;分片 3 的表名在库名tpch03,表名为 orders03;分片 3 的表名在库名 tpch04,表名为 orders04。
在这 4 种分库分表规则中,最推荐的是第 4 种,也是我们通常意义说的分库分表,这样做的好处有以下几点:
不同分片的数据可以在同一 MySQL 数据库实例上,便于做容量的规划和后期的扩展;
同一分片键的表都在同一库下,方便做整体数据的迁移和扩容。
注意:分布式数据库并不一定要求有很多个实例,最基本的要求是将数据进行打散分片。接着,用户可以根据自己的需要,进行扩缩容,以此实现数据库性能和容量的伸缩性。这才是分布式数据库真正的魅力所在。