数据库分库分表(sharding)系列转jimcsharp

图1.数据库分库分表(sharding)实施策略图解

1.准备阶段

对数据库进行分库分表(Sharding化)前,需要开发人员充分了解系统业务逻辑和数据库schema.一个好的建议是绘制一张数据库ER图或领域模型图,以这类图为基础划分shard,直观易行,可以确保开发人员始终保持清醒思路。对于是选择数据库ER图还是领域模型图要根据项目自身情况进行选择。如果项目使用数据驱动的开发方式,团队以数据库ER图作为业务交流的基础,则自然会选择数据库ER图,如果项目使用的是领域驱动的开发方式,并通过OR-Mapping构建了一个良好的领域模型,那么领域模型图无疑是最好的选择。就我个人来说,更加倾向使用领域模型图,因为进行切分时更多的是以业务为依据进行分析判断,领域模型无疑更加清晰和直观。

2.分析阶段

1.垂直切分

垂直切分的依据原则是:将业务紧密,表间关联密切的表划分在一起,例如同一模块的表。结合已经准备好的数据库ER图或领域模型图,仿照活动图中的泳道概念,一个泳道代表一个shard,把所有表格划分到不同的泳道中。下面的分析示例会展示这种做法。当然,你也可以在打印出的ER图或模型图上直接用铅笔圈,一切取决于你自己的喜好。

2.水平切分

垂直切分后,需要对shard内表格的数据量和增速进一步分析,以确定是否需要进行水平切分。

2.1若划分到一起的表格数据增长缓慢,在产品上线后可遇见的足够长的时期内均可以由单一数据库承载,则不需要进行水平切分,所有表格驻留同一shard,所有表间关联关系会得到最大限度的保留,同时保证了书写SQL的自由度,不易受join、groupby、orderby等子句限制。

2.2若划分到一起的表格数据量巨大,增速迅猛,需要进一步进行水平分割。进一步的水平分割就这样进行:

2.2.1.结合业务逻辑和表间关系,将当前shard划分成多个更小的shard,通常情况下,这些更小的shard每一个都只包含一个主表(将以该表ID进行散列的表)和多个与其关联或间接关联的次表。这种一个shard一张主表多张次表的状况是水平切分的必然结果。这样切分下来,shard数量就会迅速增多。如果每一个shard代表一个独立的数据库,那么管理和维护数据库将会非常麻烦,而且这些小shard往往只有两三张表,为此而建立一个新库,利用率并不高,因此,在水平切分完成后可再进行一次“反向的Merge”,即:将业务上相近,并且具有相近数据增长速率(主表数据量在同一数量级上)的两个或多个shard放到同一个数据库上,在逻辑上它们依然是独立的shard,有各自的主表,并依据各自主表的ID进行散列,不同的只是它们的散列取模(即节点数量)必需是一致的。这样,每个数据库结点上的表格数量就相对平均了。

2.2.2.所有表格均划分到合适的shard之后,所有跨越shard的表间关联都必须打断,在书写sql时,跨shard的join、groupby、orderby都将被禁止,需要在应用程序层面协调解决这些问题。

特别想提一点:经水平切分后,shard的粒度往往要比只做垂直切割的粒度要小,原单一垂直shard会被细分为一到多个以一个主表为中心关联或间接关联多个次表的shard,此时的shard粒度与领域驱动设计中的“聚合”概念不谋而合,甚至可以说是完全一致,每个shard的主表正是一个聚合中的聚合根!

3.实施阶段

第二部分:示例演示

图2.jpetstore领域模型

由于系统较简单,我们很容易从模型上看出,其主要由三个模块组成:用户,产品和订单。那么垂直切分的方案也就出来了。接下来看水平切分,如果我们从一个实际的宠物店出发考虑,可能出现数据激增的单表应该是Account和Order,因此这两张表需要进行水平切分。对于Product模块来说,如果是一个实际的系统,Product和Item的数量都不会很大,因此只做垂直切分就足够了,也就是(Product,Category,Item,Iventory,Supplier)五张表在一个数据库结点上(没有水平切分,不会存在两个以上的数据库结点)。但是作为一个演示,我们假设产品模块也有大量的数据需要我们做水平切分,那么分析来看,这个模块要拆分出两个shard:一个是(Product(主),Category),另一个是(Item(主),Iventory,Supplier),同时,我们认为:这两个shard在数据增速上应该是相近的,且在业务上也很紧密,那么我们可以把这两个shard放在同一个数据库节点上,Item和Product数据在散列时取一样的模。根据前文介绍的图纸绘制方法,我们得到下面这张sharding示意图:

图3.jpetstoresharding示意图

对于这张图再说明几点:

1.使用泳道表示物理shard(一个数据库结点)

2.若垂直切分出的shard进行了进一步的水平切分,但公用一个物理shard的话,则用虚线框住,表示其在逻辑上是一个独立的shard。

3.深色实体表示主表

4.X表示需要打断的表间关联

(二)全局主键生成策略

第一部分:一些常见的主键生成策略

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。目前几种可行的主键生成策略有:

1.UUID:使用UUID作主键是最简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。

2.结合数据库维护一个Sequence表:此方案的思路也很简单,在数据库中建立一个Sequence表,表的结构类似于:

[sql]viewplaincopy

01.CREATETABLE`SEQUENCE`(

02.`tablename`varchar(30)NOTNULL,

03.`nextid`bigint(20)NOTNULL,

04.PRIMARYKEY(`tablename`)

05.)ENGINE=InnoDB

每当需要为某个表的新纪录生成ID时就从Sequence表中取出对应表的nextid,并将nextid的值加1后更新到数据库中以备下次使用。此方案也较简单,但缺点同样明显:由于所有插入任何都需要访问该表,该表很容易成为系统性能瓶颈,同时它也存在单点问题,一旦该表数据库失效,整个应用程序将无法工作。有人提出使用Master-Slave进行主从同步,但这也只能解决单点问题,并不能解决读写比为1:1的访问压力问题。

除此之外,还有一些方案,像对每个数据库结点分区段划分ID,以及网上的一些ID生成算法,因为缺少可操作性和实践检验,本文并不推荐。实际上,接下来,我们要介绍的是Fickr使用的一种主键生成方案,这个方案是目前我所知道的最优秀的一个方案,并且经受了实践的检验,可以为大多数应用系统所借鉴。

第二部分:一种极为优秀的主键生成策略

flickr开发团队在2010年撰文介绍了flickr使用的一种主键生成测策略,同时表示该方案在flickr上的实际运行效果也非常令人满意,原文连接:TicketServers:DistributedUniquePrimaryKeysontheCheap这个方案是我目前知道的最好的方案,它与一般Sequence表方案有些类似,但却很好地解决了性能瓶颈和单点问题,是一种非常可靠而高效的全局主键生成方案。

图1.flickr采用的sharding主键生成方案示意图

flickr这一方案的整体思想是:建立两台以上的数据库ID生成服务器,每个服务器都有一张记录各表当前ID的Sequence表,但是Sequence中ID增长的步长是服务器的数量,起始值依次错开,这样相当于把ID的生成散列到了每个服务器节点上。例如:如果我们设置两台数据库ID生成服务器,那么就让一台的Sequence表的ID起始值为1,每次增长步长为2,另一台的Sequence表的ID起始值为2,每次增长步长也为2,那么结果就是奇数的ID都将从第一台服务器上生成,偶数的ID都从第二台服务器上生成,这样就将生成ID的压力均匀分散到两台服务器上,同时配合应用程序的控制,当一个服务器失效后,系统能自动切换到另一个服务器上获取ID,从而保证了系统的容错。

关于这个方案,有几点细节这里再说明一下:

实现该方案,应用程序同样需要做一些处理,主要是两方面的工作:

1.自动均衡数据库ID生成服务器的访问

2.确保在某个数据库ID生成服务器失效的情况下,能将请求转发到其他服务器上执行。

(三)关于使用框架还是自主开发以及sharding实现层面的考量

一、sharding逻辑的实现层面

从一个系统的程序架构层面来看,sharding逻辑可以在DAO层、JDBCAPI层、介于DAO与JDBC之间的Spring数据访问封装层(各种spring的template)以及介于应用服务器与数据库之间的sharding代理服务器四个层面上实现。

在DAO层实现

当团队决定自行实现sharding的时候,DAO层可能是嵌入sharding逻辑的首选位置,因为在这个层面上,每一个DAO的方法都明确地知道需要访问的数据表以及查询参数,借助这些信息可以直接定位到目标shard上,而不必像框架那样需要对SQL进行解析然后再依据配置的规则进行路由。另一个优势是不会受ORM框架的制约。由于现在的大多数应用在数据访问层上会依赖某种ORM框架,而多数的shrading框架往往无法支持或只能支持一种orm框架,这使得在选择和应用框架时受到了很大的制约,而自行实现sharding完全没有这方面的问题,甚至不同的shard使用不同的orm框架都可以在一起协调工作。比如现在的java应用大多使用hibernate,但是当下还没有非常令人满意的基于hibernate的sharding框架,(关于hibernatehards会在下文介绍),因此很多团队会选择自行实现sharding。

简单总结一下,在DAO层自行实现sharding的优势在于:不受ORM框架的制约、实现起来较为简单、易于根据系统特点进行灵活的定制、无需SQL解析和路由规则匹配,性能上表现会稍好一些;劣势在于:有一定的技术门槛,工作量比依靠框架实现要大(反过来看,框架会有学习成本)、不通用,只能在特定系统里工作。当然,在DAO层同样可以通过XML配置或是注解将sharding逻辑抽离到“外部”,形成一套通用的框架.不过目前还没有出现此类的框架。

在ORM框架层实现

在JDBCAPI层实现

JDBCAPI层是很多人都会想到的一个实现sharding的绝佳场所,如果我们能提供一个实现了sharding逻辑的JDBCAPI实现,那么sharding对于整个应用程序来说就是完全透明的,而这样的实现可以直接作为通用的sharding产品了。但是这种方案的技术门槛和工作量显然不是一般团队能做得来的,因此基本上没有团队会在这一层面上实现sharding,甚至也没有此类的开源产品。笔者知道的只有一款商业产品dbShards采用的是这一方案。

在介于DAO与JDBC之间的Spring数据访问封装层实现

在springd大行其道的今天,几乎没有哪个java平台上构建的应用不使用spring,在DAO与JDBC之间,spring提供了各种template来管理资源的创建与释放以及与事务的同步,大多数基于spring的应用都会使用template类做为数据访问的入口,这给了我们另一个嵌入sharding逻辑的机会,就是通过提供一个嵌入了sharding逻辑的template类来完成sharding工作.这一方案在效果上与基于JDBCAPI实现的方案基本一致,同样是对上层代码透明,在进行sharding改造时可以平滑地过度,但它的实现却比基于JDBCAPI的方式简单,因此成为了不少框架的选择,阿里集团研究院开源的CobarClient就是这类方案的一种实现。

在应用服务器与数据库之间通过代理实现

在应用服务器与数据库之间加入一个代理,应用程序向数据发出的数据请求会先通过代理,代理会根据配置的路由规则,对SQL进行解析后路由到目标shard,因为这种方案对应用程序完全透明,通用性好,所以成为了很多sharding产品的选择。在这方面较为知名的产品是mysql官方的代理工具:MysqlProxy和一款国人开发的产品:amoeba。mysqlproxy本身并没有实现任何sharding逻辑,它只是作为一种面向mysql数据库的代理,给开发人员提供了一个嵌入sharding逻辑的场所,它使用lua作为编程语言,这对很多团队来说是需要考虑的一个问题。amoeba则是专门实现读写分离与sharding的代理产品,它使用非常简单,不使用任何编程语言,只需要通过xml进行配置。不过amoeba不支持事务(从应用程序发出的包含事务信息的请求到达amoeba时,事务信息会被抹去,因此,即使是单点数据访问也不会有事务存在)一直是个硬伤。当然,这要看产品的定位和设计理念,我们只能说对于那些对事务要求非常高的系统,amoeba是不适合的。

二、使用框架还是自主开发?

前面的讨论中已经罗列了很多开源框架与产品,这里再整理一下:基于代理方式的有MySQLProxy和Amoeba,基于Hibernate框架的是HibernateShards,通过重写spring的ibatistemplate类是CobarClient,这些框架各有各的优势与短板,架构师可以在深入调研之后结合项目的实际情况进行选择,但是总的来说,我个人对于框架的选择是持谨慎态度的。一方面多数框架缺乏成功案例的验证,其成熟性与稳定性值得怀疑。另一方面,一些从成功商业产品开源出框架(如阿里和淘宝的一些开源项目)是否适合你的项目是需要架构师深入调研分析的。当然,最终的选择一定是基于项目特点、团队状况、技术门槛和学习成本等综合因素考量确定的。

(四)多数据源的事务处理

分布式事务

这是最为人们所熟知的多数据源事务处理机制。本文并不打算对分布式事务做过多介绍,读者可参考此文:关于分布式事务、两阶段提交、一阶段提交、BestEfforts1PC模式和事务补偿机制的研究。在这里只想对分布式事务的利弊作一下分析。

优势:

1.基于两阶段提交,最大限度地保证了跨数据库操作的“原子性”,是分布式系统下最严格的事务实现方式。

2.实现简单,工作量小。由于多数应用服务器以及一些独立的分布式事务协调器做了大量的封装工作,使得项目中引入分布式事务的难度和工作量基本上可以忽略不计。

劣势:

基于BestEfforts1PC模式的事务

与分布式事务采用的两阶段提交不同,BestEfforts1PC模式采用的是一阶段端提交,牺牲了事务在某些特殊情况(当机、网络中断等)下的安全性,却获得了良好的性能,特别是消除了对水平伸缩的桎酷。DistributedtransactionsinSpring,withandwithoutXA一文对BestEfforts1PC模式进行了详细的说明,该文提供的Demo代码更是直接给出了在Spring环境下实现一阶段提交的多数据源事务管理示例。不过需要注意的是,原示例是基于spring3.0之前的版本,如果你使用spring3.0+,会得到如下错误:java.lang.IllegalStateException:Cannotactivatetransactionsynchronization-alreadyactive,如果使用spring3.0+,你需要参考spring-data-neo4j的实现。鉴于BestEfforts1PC模式的性能优势,以及相对简单的实现方式,它被大多数的sharding框架和项目采用。

事务补偿机制

小结

分布式事务,最严格的事务实现,但性能是个大问题;BestEfforts1PC模式,性能与事务可靠性的平衡,支持系统水平伸缩,大多数情况下是最合适的选择;事务补偿机制,只能适用于对事务性要求不高,允许数据“最终一致”即可的系统,牺牲实时一致性,获得最大的性能回报。

(五)一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案

本文将重点围绕“数据库扩容”进行深入讨论,并提出一种允许自由规划并能避免数据迁移和修改路由代码的Sharding扩容方案

Sharding扩容——系统维护不能承受之重

一般来说,“理想”的扩容方案应该努力满足以下几个要求:

目前,能够避免数据迁移的优秀方案并不多,相对可行的有两种,一种是维护一张记录数据ID和目标Shard对应关系的映射表,写入时,数据都写入新扩容的Shard,同时将ID和目标节点写入映射表,读取时,先查映射表,找到目标Shard后再执行查询。该方案简单有效,但是读写数据都需要访问两次数据库,且映射表本身也极易成为性能瓶颈。为此系统不得不引入分布式缓存来缓存映射表数据,但是这样也无法避免在写入时访问两次数据库,同时大量映射数据对缓存资源的消耗以及专门为此而引入分布式缓存的代价都是需要权衡的问题。另一种方案来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。

取长补短,兼容并包——一种理想的Sharding扩容方案

原理

图1.Sharding拓扑结构领域模型

在这个模型中,有几个细节需要注意:ShardGroup的writable属性用于标识该ShardGroup是否可以写入数据,一个Partition在任何时候只能有一个ShardGroup是可写的,这个ShardGroup往往是最近一次扩容引入的;startId和endId属性用于标识该ShardGroup的ID增量区间;Shard的hashValue属性用于标识该Shard节点接受哪些散列值的数据;FragmentTable的startId和endId是用于标识该分段表储存数据的ID区间。

示例

让我们通过示例来了解这套方案是如何工作的。

阶段一:初始上线

假设某系统初始上线,规划为某表提供4000W条记录的存储能力,若单表存储上限为1000W条,单库存储上限为2000W条,共需2个Shard,每个Shard包含两个分段表,ShardGroup增量区间为0-4000W,按2取余分散到2个Shard上,具体规划方案如下:

图2.初始4000W存储规模的规划方案

与之相适应,Sharding拓扑结构的元数据如下:

图3.对应Sharding元数据

阶段二:系统扩容

图4.二次扩容6000W存储规模的规划方案

相应拓扑结构表数据下:

图5.对应Sharding元数据

从这个扩容案例中我们可以看出该方案允许根据硬件情况进行灵活规划,对扩容规模和节点数量没有硬性规定,是一种非常自由的扩容方案。

增强

图6.增强后的Sharding拓扑结构领域模型

让我们还是通过一个示例来了解升级后的方案是如何工作的。

阶段三:不扩容,重复利用再生存储空间

图7.重复利用2000W再生存储空间的规划方案

相应拓扑结构的元数据如下:

图8.对应Sharding元数据

这套方案综合利用了增量区间和散列两种路由方式的优势,避免了数据迁移和“热点”问题,同时,它对Sharding拓扑结构建模,使用了一致的路由算法,从而避免了扩容时修改路由代码,是一种理想的Sharding扩容方案。

THE END
1.宠物管理系统ER图流程图模板er图 宠物店管理系统 用于宠物管理系统 生活常识 作者其他创作 大纲/内容 头像 用户名 用户id n 1 点赞 动物照片 心理测试 点击次数 发布人 回复内容 邮箱 账号 评论内容 性情 查看 测试时长 用户 简介 公示内容 审核状态 姓名 性别 身体状况 创建时间 捐助时间 管理员ID 手机号 用户互动 管理员 捐助资金 ...https://www.processon.com/view/66348c41cff06e2d3972d4be
2.E宠商城网站宠物论文.doc数据库设计 15 4.3.1 数据库E-R图 15 4.3.2 数据库实体 15 4.3.3数据库设计表 17 5系统详细设计 19 5.1 系统首页 20 5.2用户注册 20 5.3用户登录 21 5.4商品信息 22 5.5宠物养护知识信息 23 5.6登陆模块的实现 24 5.7 宠物养护知识管理 25 5.8 商品管理 26 5.9修改密码 27 6系统测试 29 6.1 E宠...https://max.book118.com/html/2022/0624/6023231151004202.shtm
3.古道西风瘦马是什么意思“古道”指古驿道,残破的荒路,说出了苍凉寂寥的意境 “西风”是萧瑟悲凉的一种气氛 ...https://iask.sina.com.cn/jx/sh/4E0WE3La3hBN.html
1.java基于springboot的可爱多宠物店管理系统(源码+vue+部署文档+前后端...经过分析,本系统选择java技术,运用了MySQL为后台数据库,将用户分为管理员、用户和员工这三大类,系统实现了首页、用户、员工、宠物信息、宠物用品、商品类型、采购信息、宠物服务、服务类型、服务预约、轮播图管理、订单管理、我的信息等必需的功能;其操作简单,界面友好,运行比较稳定,适用于可爱多宠物店管理系统的管理。https://blog.csdn.net/QQ1039692211/article/details/143472539
2.宠物店管理系统mysql宠物店管理系统er图宠物店管理系统mysql 宠物店管理系统er图 宠家---项目系统设计与数据库设计 作业所属课程 一、整体计划安排 二、团队项目的预期开发计划分工安排 三、项目设计相关图 1.体系结构图 2.功能模块图 图3.1宠物医院信息管理系统功能模块图 图3.2医生信息管理模块功能图 ...https://blog.51cto.com/u_16213579/9162766
3.宠物店ER图E-R图也称实体-联系图,提供了表示实体类型、属性和联系的方法,用来描述现实世界的概念模型。本图呈现的是关于宠物店的内部实体联系,清晰直观,欢迎参考~https://www.edrawmax.cn/templates/file/1047227