PolarDB-X 最佳实践系列(四):如何设计一张订单表
本文主要内容是如何使用全局索引与CO_HASH分区算法(CO_HASH),实现高效的多维度查询。
淘宝订单号中的秘密
有一个很有趣的事情。
打开你的淘宝客户端或者PC端的淘宝,点开订单列表,打开几个订单,查看他们的订单号,你会发现什么?
比如这是我最近的3个订单,和10年前的3个订单。其订单号分别是:
1249511065364414810
1238822988656414810
1236446127134414810
103698817404810
107655289504810
103719620094810
也许你会惊奇的发现,订单号的后几位好像是一样的。比如我的账号,从10年前到今天,订单号的后四位一直是4810,那么为什么?这个其实和使用分布式数据库的一个最佳实践相关。
经典的买卖家例子
好多年前就流传着淘宝买卖家的案例...
淘宝中有一个非常重要的表,订单表,他里面存着订单的一些关键信息,例如订单号(order_id)、卖家id(seller_id)、买家id(buyer_id)、商品id等等。有两类查询是这个表上的高频查询:
select * from orders where buyer_id = ? select * from orders where seller_id = ?
这两个SQL的业务含义一目了然,分别是买家查询自己的订单列表和卖家查询自己的订单列表。
如果我们在单机数据库中做这两条SQL,都知道怎么做。嗯,在buyer_id和seller_id上分别建个建索引就可以了:
create index idx_buyer_id on orders (buyer_id); create index idx_seller_id on orders (seller_id);
在数据库中,空间换时间是一个非常基本的思路,例如加索引。
如果你是用一些分库分表中间件,例如MyCAT之类的产品,对这个表做了分库分表,就需要面临一个跟单机数据库完全不一样的一个问题,该如何选择分库分表键?
一般此类中间件都会告诉你,你哪个列查的最多,就选哪个列做拆分键。
但问题来了,这两类SQL都很高频,选了buyer_id做分库分表键,那按seller_id查就会全库全表扫描;如果按seller_id做分库分表键,那按buyer_id查就会全库全表扫描。
难道鱼和熊掌不可兼得?
一般解决这类的问题的方案是,使用两套订单表,其中一套使用buyer_id做分库分表建,另一套使用seller_id做分库分表键,中间使用binlog来做同步,类似下面的样子:
这个方案是OK的,能够落地的,只不过做的过程会有些小痛苦要解决,例如:
1、这个同步怎么搞啊...,用开源的binlog订阅组件比如canal吗?那这个canal怎么运维啊...好烦
2、这个同步是有延迟的,延迟代表了数据死不一致的,应用需要有一些容错机制来避免不一致带来的影响,好烦+1
3、需要在业务里自己控制应该访问哪个表,好烦+2
4、分库分表下面有很多的mysql,要同时同步这么多的mysql...,好烦+3
5、做DDL要有些技巧,比如加列先加目标端,减列先减源端...,很多的潜规则,好烦+4
6、这才是一张表呢!我有一堆类似的场景怎么办!!好烦+10086
我们先不管这些缺点,假设我们已经这样做到了,我们成功的解决了买卖家订单问题。
买卖家问题进阶
我们现在又有了一个新的要考虑的SQL:
select * from orders where order_id = ?
这个SQL作用太简单了,根据订单id查订单详情嘛!
为了做这个SQL,单机里给order_id建索引即可,分库分表应该怎么做?
抢答一个!把订单表再复制一份,使用order_id做分库分表键。
听起来可以,应该能解决问题。但是,这个表多复制一份,就是多一份的代价,比如空间,比如同步链路的维护。所以,有没有更好的方法?
我们能关注到一个事情,订单id,是由我们程序控制生成的。在生成订单的时候,我们一定知道它的买家id,假如我们将买家id隐藏在订单id里,有了订单id就能算出买家id,有了买家id就可以去查买家维度的订单表了。
这样,我们还是只需要存两份数据(买家维度和卖家维度),就能同时解决三个维度的查询(买家维度、卖家维度、订单维度)。
所以现在应该明白了,为什么同一个人的淘宝订单后4位是相同的了吧。
小花絮:
我发现我11年7月10的订单号还是4810结尾,但11年6月28号及之前的订单并没有遵循这个规律。呃...这说明,淘宝应该是在11年的6.28-7.10之间做了这个优化。
PolarDB-X中如何实现
OK,回到我们的云原生分布式数据库PolarDB-X。
如果我们在PolarDB-X中要解决上述买卖家问题,应该怎么做?
答案是,我们只需执行以下几条SQL即可:
jcreate database ms1 mode=auto; use ms1; create table orders( order_id varchar(128) primary key, buyer_id varchar(128), seller_id varchar(128), index idx_buyer_id(buyer_id), index idx_seller_id(seller_id) )partition by hash(order_id); create clustered index gsi_buyer_id on orders (buyer_id) partition by hash(buyer_id); create clustered index gsi_seller_id on orders (seller_id) partition by hash(seller_id);
然后?没有然后了啊,这就可以了。
真的可以了,你不用改SQL,不用研究Canal(其实canal作者就坐我旁边,要考虑下来玩玩吗),不用维护同步链路,不用担心数据不一致,就O!K!了!
至于按订单id查?订单id本来就是orders的主键,默认就是orders表的分区键,所以没问题的。
这么简单就OK了?为什么?
PolarDB-X中的全局索引
这两条语句发生了什么?实际上,他们在orders表上,创建了两条全局索引。全局索引和单机索引的原理差不多,也是空间换时间的思想,只不过它的数据以索引的key分布在整个集群中。
全局索引的创建、维护,都在PolarDB-X内核中完成的,完全不需要用户去操心。
有一个小问题,建索引的语句里面,有个clustered关键字,这是什么意思?
我们先看,如果不加clustered,会发生什么,例如:
jcreate global index gsi_buyer_id on orders (buyer_id) partition by hash(buyer_id);
主表:
PolarDB-X会定义这样的一个索引结构:
这个索引中,会包含索引的key以及主键两个列,也即order_id与buyer_id。
PolarDB-X在执行select * from orders where buyer_id = ? 的时候,会先根据buyer_id在索引idx_buyer_id上扫描出order_id,再使用order_id到主表上进行回表操作。
听起来好像没有什么问题。
但是,有一点需要考虑。请打开你的订单列表,看一下,你有多少订单:
呃...,我有126页订单,数了下,每页15个,也就是大约1800个订单。
淘宝的订单表的分区数大约是数千这个量级,你会发现,这1800个订单的回表,要覆盖相当比例的分区,似乎跟全表扫描的代价没有什么太大的差异了。
怎么办?
我们为什么要回表?其是是因为,我们的查询是SELECT *,需要这个表所有的列,而我们的索引里只包含了索引key和主键,因此需要到主表中找到剩下的列。
所以为了不回表,我们想到的一个办法,是在索引表中冗余主表的所有列,用更多的时间来换取空间。
所以,一个合格的分布式数据库,不仅需要有全局索引,还需要有聚簇的(clustered)全局索引。
Clustered index就是PolarDB-X中的概念,它相对于普通的全局索引的区别就是,它包含了表的所有列,可以避免回表的代价。
PolarDB-X中的CO_HASH
如果如上文所说,订单ID的后四位与买家ID的后四位相同,如何使用PolarDB-X实现此类路由逻辑呢。
PolarDB-X提供了名为CO_HASH的分区算法,可以完成这个功能:
jcreate database ms1 mode=auto; use ms1; create table orders( order_id varchar(128) primary key, buyer_id varchar(128), seller_id varchar(128), index idx_buyer_id(buyer_id), index idx_seller_id(seller_id) )partition by co_hash(right(order_id,4), right(buyer_id,4)); create clustered index gsi_seller_id on orders (seller_id) partition by hash(seller_id); select * from orders where buyer_id=? //主表 select * from orders where order_id=? //主表 select * from orders where seller_id=? //gsi_seller_id
通过使用CO_HASH,可以省略掉buyer_id上的全局索引。
CO_HASH的更多用法参考:如何使用DDL语句创建分区表
CO_HASH的核心理念是:
- 有些信息来源用户的输入,例如用户系统的手机号、邮箱,订单系统中的buyer_id、seller_id;
- 有些信息是由“业务系统”生成的,例如用户系统中的user_id,订单系统中的order_id。
我们有时可以通过控制“业务系统”的生成逻辑,将其生成的内容与用户的输入关联起来,来达到降维的目的。
例如:
- user_id中可以携带手机号的某几位,或者携带邮箱的hash值的某几位;
- order_id可以携带seller_id,或者buyer_id的某几位。
CO_HASH是一种非常有用的小技巧,合理使用可以有效的减少GSI的数目。
但CO_HASH也不是万能的,它只能减少两种存在关联的维度中的一维,如果完全不存在关联,就必须使用GSI了。
小结
OK,总结几条这个例子告诉我们的PolarDB-X的最佳实践:
1、使用全局索引来解决类似买卖家问题的多维度查询的问题
2、当索引与主表是一对多的关系的时候,考虑使用clustered index来消灭回表的代价
3、对订单ID等做巧妙的设计,结合CO_HASH,可以省略部分全局索引。
怎么样,PolarDB-X用起来是不是非常简单,欢迎持续关注我们。
本文为阿里云原创内容,未经允许不得转载。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
阿里云推出 3.x Java 探针,解锁应用观测与治理的全新姿势
作者:张铭辉、泮圣伟 前言 随着春节大促即将到来,为了确保线上业务高效稳定地运行,电商企业大多会对旗下关键业务应用进行多轮测试。通过模拟线上较高流量的请求,来观察服务性能的实际表现。以某企业的业务测试报告举例: 图 1 压测报告显示,成功率非常低,且全局接口成功率都很低 通过报告可以看到: 当应用所承受的流量增加至特定临界点时,请求成功率大幅下降,导致整个测试周期内平均成功率相当惨淡,仅有 9.89%,并伴随着较高的响应时间(RT)。经过深入分析,发现这种高失败率现象普遍存在于所有接口上,并且在整个压测过程中未显示出任何回归稳定状态的迹象。 面对这类压测结果的直观推断是应用可能已达到其性能瓶颈。Prometheus 监控所采集的 CPU 使用情况进一步印证了这一假设:应用实例的 CPU 使用率几乎达到饱和状态,应用在当前情景下无法处理如此高 TPS(每秒事务数)。该企业采用的优化手段为:一方面,通过链路追踪(Tracing)数据和 CPU 火焰图,逐步定位到代码层面性能瓶颈,并开展针对性优化;另一方面,为应用集成开源的流量控制防护和熔断降级组件来应对线上流量的不确定性,并对关键业务应用...
- 下一篇
Marsh v1.6.1 已经发布,微服务开发脚手架
Marsh v1.6.1 已经发布,微服务开发脚手架 此版本更新内容包括: solon 升为 2.6.6 wood 升为 1.2.6 snack3 升为 3.2.88 grit 升为 1.7.1 water 升为 2.12.1 详情查看:https://gitee.com/noear/marsh/releases/v1.6.1
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 2048小游戏-低调大师作品
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2全家桶,快速入门学习开发网站教程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,CentOS7官方镜像安装Oracle11G