现有互联网业务模式下,数据库分库分表已经成为解决数据库瓶颈的一个普遍的解决方案。分库分表有多种好处,比如高容量、大并发等,但是在拆分过程中也引入了一些使用限制,比如多维查询,非拆分键的查询请求会分发到底层所有实例进行查询,性能会大打折扣。
我们来举个例子,最常见的订单表,常用的拆分方法是按照用户 ID 作为拆分键。如果仅使用订单号作为条件来查询则会出现上述性能问题,而仅通过订单号的查询请求恰恰占有不小的比例。
在阿里云提供的新版 DRDS(5.1.28-1320920 及其以上的版本)已经实现二维查询的功能。看看他的特性 RANGE_HASH(COL1, COL2, N) :
- 拆分键的类型必须是字符类型或数字类型
- 根据任一拆分键后 N 位计算哈希值,然后再按分库数去取余,完成路由计算。N 为函数第三个参数。例如:RANGE_HASH(COL1, COL2, N) ,计算时会优先选择 COL1,截取其后N位进行计算。 COL1 不存在时找 COL2。
- 适合于需要有两个拆分键,并且查询时仅有其中一个拆分键值的场景。
- 两个拆分键皆不能修改。
- 插入数据时如果发现两个拆分键指向不同的分库或分表时,插入会失败。
针对上一个例子,使用这个功能就可以解决问题。可以这样设计订单表,拆分键选择 user_id & order_id,在 order_id 中冗余 user_id 后 N 位。这样使用 RANGE_HASH(user_id, order_id, N) 功能即可以实现仅使用 user_id 或 order_id 条件就可以快速查询所需要的数据。
DRDS 表结构:
mysql> show create table test_order_rangehash\G
*************************** 1. row ***************************
Table: test_order_rangehash
Create Table: CREATE TABLE `test_order_rangehash` (
`id` int(11) NOT NULL AUTO_INCREMENT BY GROUP,
`user_id` int(11) DEFAULT NULL,
`order_id` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `auto_shard_key_ORDER_ID` (`order_id`),
KEY `auto_shard_key_USER_ID` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 dbpartition by RANGE_HASH(`user_id`, `order_id`, 4)
底层数据库表结构:
mysql>show create table test_order_rangehash\G
*************************** 1. row ***************************
Table: test_order_rangehash
Create Table: CREATE TABLE `test_order_rangehash` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`order_id` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `auto_shard_key_ORDER_ID` (`order_id`),
KEY `auto_shard_key_USER_ID` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
拆分键所插入值指向不同分库即会报错:
mysql> insert into test_order_rangehash (user_id, order_id, create_time) values (100000001, 20170818111111100002,now());
ERROR 4300 (HY000): ERR-CODE: [TDDL-4300][ERR_ROUTE] Route : Range hash has mul rules, insert shard columns must be equal by rule. More: [http://middleware.alibaba-inc.com/faq/faqByFaqCode.html?faqCode=TDDL-4300]
仅使用 user_id 或 order_id 作为查询条件,均能准确定位到后段数据库:
mysql> explain select * from test_order_rangehash where user_id = 100000022\G
*************************** 1. row ***************************
GROUP_NAME: DCWEB_1498027405059FQIYDCWEB_JDLT_0022_RDS
SQL: select `test_order_rangehash`.`id`,`test_order_rangehash`.`user_id`,`test_order_rangehash`.`order_id`,`test_order_rangehash`.`create_time` from `test_order_rangehash` where (`test_order_rangehash`.`user_id` = 100000022)
PARAMS: {}
1 row in set (0.04 sec)
mysql> explain select * from test_order_rangehash where order_id = 2017081811113220022\G
*************************** 1. row ***************************
GROUP_NAME: DCWEB_1498027405059FQIYDCWEB_JDLT_0022_RDS
SQL: select `test_order_rangehash`.`id`,`test_order_rangehash`.`user_id`,`test_order_rangehash`.`order_id`,`test_order_rangehash`.`create_time` from `test_order_rangehash` where (`test_order_rangehash`.`order_id` = 2017081811113220022)
PARAMS: {}
1 row in set (0.03 sec)
结语:多维查询还可以使用异构索引来实现,但 RANGE_HASH 岂不更好。当然它不是万能的,某些场景下还是要选择异构索引(比如订单表以买家、卖家维度查询)。