文章目录
一、为什么要分库分表
1.1 什么是分库
一个数据库分成多个数据库,部署到不同机器
1.2 什么是分表
一个数据库表分成多个表
1.3 为什么要分库
如果业务量剧增,数据库可能会出现性能瓶颈,这时候我们就需要考虑拆分数据库
1.3.1 磁盘存储
业务量剧增,MySQL单机磁盘容量不足,拆成多个数据库,磁盘使用率大大降低。
1.3.2 并发连接支撑
在高并发的场景下,大量请求访问数据库,MySQL单机是扛不住的。微服务架构出现,就是为了应对高并发。它把订单、用户、商品等不同模块,拆分成多个应用,并且把单个数据库也拆分成多个不同功能模块的数据库(订单库、用户库、商品库),以分担读写压力。
1.4 为什么要分表
数据量如果太大,SQL的查询就会变慢。如果一个查询SQL没命中索引,千百万数据量的表可能会拖垮这个数据库。
即使SQL命中了索引,如果表的数据量超过千万,查询也会明显变慢。
B+树结构,数据千万级别,B+树的高度会增高,查询就变慢
tips:
Mysql现在用innodb存储引擎,数据结构B+树,磁盘的IO次数与树的高度是相同的。
B+树的分支非常多,而且每个非叶子节点只存主键值(主键索引)和指针,数据存在于叶子节点。
它的最小存储单元是页,一个页的大小默认是16KB。代表B+树的每个节点可以存16KB数据,这里我们假设我们的一行数据大小是1K,一个节点就可以存16行数据。
真正的数据都是存在叶子节点的,所以这里是指叶子节点可以存放16行数据。假设主键类型为bigint,占用8Byte,指针可以设置为占用6Byte,总共就为14Byte。这样就可以算出一个节点大概可以存放多少个指针了(指针指向下一层节点),大概为16KB/14Byte=1170个。由此,可以推算出,2层B+树的话,可以存放117016=18720行数据。3层B+树的话,可以存放11701170*16=21902400行数据,如果再增加,树高就会变高
二、分库分表解决方案
垂直切分常见有垂直分库和垂直分表两种。
2.1 垂直(纵向)切分
垂直分库是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。
垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。
2.1.1 垂直切分优点
解决业务系统层面的耦合,业务清晰
与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等
高并发场景下,垂直切分可提升IO、数据库连接数、单机硬件资源瓶颈
2.1.2 垂直切分缺点
部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度
分布式事务处理复杂
依然存在单表数据量过大的问题(需要水平切分)
2.2 水平(横向)切分
水平切分分为库内分表和分库分表,是根据表内数据内当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候需要进行水平切分。
在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。
库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。
2.2.1 水平切分优点
不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力
应用端改造较小,不需要拆分业务模块
2.2.2 水平切分缺点
跨分片的事务一致性难以保证
跨库的join关联查询性能较差
数据多次扩展难度和维护量极大
水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。
2.3 两个问题
知道了为什么要分,和分的方案,现在可能就会有两个问题:
什么时候分库分表?怎么分库分表?
我们继续看
2.4 什么时候考虑切分
2.4.1 预估数据量
阿里建议,预估三年内单表数据量大于500W或单表数据文件大于2G,考虑分库分表
2.4.2 预估数据趋势
从数据增长趋势的角度考虑,是增长速率由慢到快,还是由快到慢
2.4.3 预估应用场景
适合读多写少的业务场景
2.4.4预估业务复杂度
2.4.5 五点建议
2.4.5.1 能不切分尽量不要切分
并不是所有表都需要进行切分,主要还是看数据的增长速度。切分后会在某种程度上提升业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好的实现需求也是其重要工作之一。
不到万不得已不用轻易使用分库分表这个大招,避免"过度设计"和"过早优化"。分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、读写分离、索引优化等等。当数据量达到单表的瓶颈时候,再考虑分库分表。
2.4.5.2 数据量过大,正常运维影响业务访问切分
这里说的运维指:
1)对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO。例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的
2)对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大。如果使用pt-online-schema-change,使用过程中会创建触发器和影子表,也需要很长的时间。在此操作过程中,都算为风险时间。将数据表拆分,总量减少,有助于降低这个风险。
3)大表会经常访问与更新,就更有可能出现锁等待。将数据切分,用空间换时间,变相降低访问压力
2.4.5.3 随着业务发展,需要对某些字段垂直拆分
在项目初始阶段,这种设计是满足简单的业务需求的,也方便快速迭代开发。而当业务快速发展时,数据量激增,用户量从10w激增到10亿,用户非常的活跃,每次登录会更新 last_login_name 字段,使得 user 表被不断update,压力很大。而其他字段:id, name, personal_info 是不变的或很少更新的,此时在业务角度,就要将 last_login_time 拆分出去,新建一个 user_time 表。
2.4.5.4 数据量快速增长考虑切分
随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。此时一定要选择合适的切分规则,提前预估好数据容量。
2.4.5.5 安全性和可用性方面
在业务层面上垂直切分,将不相关的业务的数据库分隔,因为每个业务的数据量、访问量都不同,利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高。
2.5 怎么切分?-几种典型的数据分片规则为:
2.5.1 根据数值范围
按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为19999的记录分到第一个库,1000020000的分到第二个库,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。
优点:
单表大小可控
天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移
使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。
缺点:
热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询
2.5.2 根据数值取模
一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询。
优点:
数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈
缺点:
后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)
容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。
三、分库分表带来的问题
分库分表能有效的缓解单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。我们主要从五个角度来看。
3.1 事务一致性问题
3.1.1分布式事务
当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。
分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。
3.1.2最终一致性
对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。
3.2 跨节点关联查询 join 问题
切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。
解决这个问题的一些方法:
3.2.1 全局表
全局表,也可看做是"数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。
3.2.2 字段冗余
一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如:订单表保存userId时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询"买家user表"了。
但这种方法适用场景也有限,比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证,就像上面订单表的例子,买家修改了userName后,是否需要在历史订单中同步更新呢?这也要结合实际业务场景进行考虑。
3.2.3 数据组装
在系统层面,分两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。
3.2.4ER分片
关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能较好的避免跨分片join问题。在1:1或1:n的情况下,通常按照主表的ID主键切分。
3.3 跨节点分页、排序、函数问题
跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。
但是如果取得页数很大,情况则变得复杂很多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体的排序,这样的操作是很耗费CPU和内存资源的,所以页数越大,系统的性能也会越差。
在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。
3.4 全局主键避重问题
在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。有一些常见的主键生成策略:
3.4.1 UUID(不推荐)
UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符,例如:550e8400-e29b-41d4-a716-446655440000
UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,因为它是无序的,mysql的innodb中,对索引字段插入的效率比较低,也会引起数据位置频繁变动。
3.4.2 Snowflake分布式自增ID算法(推荐)
Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字,组成部分:
第一位未使用
接下来41位是毫秒级时间,41位的长度可以表示69年的时间
5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点
最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列
毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。和UUID不同,雪花算法能保证相同进程主键的有序性。
不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。
3.5 数据迁移扩容问题
当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)。
如果采用数值范围分片,只需要添加节点就可以进行扩容,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。
四、Java中支持分库分表的框架/组件/中间件
列举一些比较常见的,简单介绍一下:
sharding-jdbc(当当)
TSharding(蘑菇街)
Atlas(奇虎360)
Cobar(阿里巴巴)
MyCAT(基于Cobar)
TDDL(淘宝)
Vitess(谷歌)
4.1 sharding-jdbc
首先,第一个,可能也是最常见最常用的,Sharding-JDBC,这个是最早的名字,现在已经发展成为ShardingSphere,生态,我们在文章下面会具体介绍它的用法。
4.2 TSharding
用于蘑菇街交易平台的分库分表组件,很少的资源投入即可开发完成,支持交易订单表的Sharding需求,分库又分表,支持数据源路由,支持事务,支持结果集合并,支持读写分离。
4.3 Atlas
基于MySQL-Proxy上二次开发的,主要支持两个特性:分表和读写分离,但是分表的话只支持单库多表,即事实上是不支持分布式分表的,所有分表都在同一个库中。
4.4 Cobar
阿里的关系型数据的分布式处理系统,以proxy的形式位于前台应用和实际数据库之间,对前台的开放的接口是mysql通信协议。将前台SQL语句变更并按照数据分布规则转发到合适的后台数据分库,再合并返回结果,模拟单库下的数据库行为。
4.5 MyCAT
实现了 MySQL 协议 的服务器,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问, 而其后端可以用 MySQL 原生协议与多个 MySQL 服务器通信,也可以用 JDBC 协议与大多数 主流数据库服务器通信,其核心功能是分库分表。配合数据库的主从模式还可实现读写分离。
4.6 TDDL
主要解决了分库分表对应用的透明化以及异构数据库之间的数据复制,它是一个基于集中式配置的 jdbc datasource实现,具有主备,读写分离,动态数据库配置等功能。
4.7 Vitess
是Youtube开源的数据库扩展及高可用方案,已经用于生产环境,功能强大,但是构架复杂,部署及运维成本较高。
五、ShardingSphere
这个就是刚才第一个介绍的Sharding-JDBC升级版,什么是升级版,Sphere生态的意思,从单一产品,逐渐变成一个生态。下面就来详细介绍一下。实际操作时还是使用Sharding-JDBC。
5.1 ShardingSphere简介
最早是当当网内部使用的一款分库分表框架,名字叫Sharding-JDBC,定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零,适用于任何基于 JDBC 的 ORM 框架,支持任何第三方的数据库连接池,支持任意实现 JDBC 规范的数据库。
2017年的时候开始对外开源,在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为ShardingSphere。
2020年正式成为Apache软件基会的顶级项。由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。
5.2 Sharding-JDBC优点
Sharding-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。
客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖。
Sharding-JDBC直接封装JDBC API。
旧代码迁移成本几乎为零。
适用于任何基于 JDBC 的 ORM 框架。
支持任何第三方的数据库连接池。
支持任意实现 JDBC 规范的数据库。
5.3 核心概念
5.3.1 LogicTable(逻辑表)
数据分片的逻辑表,对于水平拆分的数据库(表),同一类表的总称。例:订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。
5.3.2 ActualTable(真实表)
在分片的数据库中真实存在的物理表。即上个示例中的t_order_0到t_order_9。
5.3.3 DataNode(数据节点)
数据分片的最小单元。由数据源名称和数据表组成,例:ds_1.t_order_0。配置时默认各个分片数据库的表结构均相同,直接配置逻辑表和真实表对应关系即可。如果各数据库的表结果不同,可使用ds.actual_table配置。
5.3.4 BindingTable(绑定表)
指在任何场景下分片规则均一致的主表和子表。例:订单表和订单项表,均按照订单ID分片,则此两张表互为BindingTable关系。BindingTable关系的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
5.3.5 BroadcastTable(广播表)
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
5.4 分片
5.4.1 分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:订单表订单ID分片尾数取模分片,则订单ID为分片字段。SQL中如果无分片字段,将执行全路由,性能较差,支持多分片字段。
5.4.2 分片算法
5.4.2.1 精确分片算法
精确分片算法(PreciseShardingAlgorithm)精确分片算法(=与IN语句),用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用
5.4.2.2 范围分片算法
范围分片算法(RangeShardingAlgorithm)用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。
5.4.2.3 复合分片算法
复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,根据多个字段处理业务逻辑。需要在复合分片策略(ComplexShardingStrategy )下使用。
5.4.2.4 Hint分片算法
Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中我们都是解析 语句提取分片键,并设置分片策略进行分片。但有些时候我们并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定SQL的目标数据库和表信息,也叫强制路由。
5.4.3 分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
一个好的分片策略=好的分片键+好的的分片算法
5.4.3.1 标准分片策略
对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
5.4.3.2复合分片策略
对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
5.4.3.3 行表达式分片策略
对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。
5.4.3.4 Hint分片策略
对应HintShardingStrategy。通过Hint而非SQL解析的方式分片的策略。
5.4.3.5 不分片策略
对应NoneShardingStrategy。不分片的策略。
5.5 分布式主键
等同于本文3.4部分
5.6 分布式事务
数据库事务需要满足ACID(原子性、一致性、隔离性、持久性)四个特性。
原子性(Atomicity)指事务作为整体来执行,要么全部执行,要么全不执行。
一致性(Consistency)指事务应确保数据从一个一致的状态转变为另一个一致的状态。
隔离性(Isolation)指多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability)指已提交的事务修改数据会被持久保存。
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为本地事务。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。 但是在基于微服务的分布式应用环境下,越来越多的应用场景要求对多个服务的访问及其相对应的多个数据库资源能纳入到同一个事务当中,分布式事务应运而生。
5.6.1 本地事务
在不开启任何分布式事务管理器的前提下,让每个数据节点各自管理自己的事务。 它们之间没有协调以及通信的能力,也并不互相知晓其他数据节点事务的成功与否。 本地事务在性能方面无任何损耗,但在强一致性以及最终一致性方面则力不从心。
5.6.2 XA强一致事务
XA协议最早的分布式事务模型是X/OPEN国际联盟提出的模型,简称XA协议。
基于XA协议实现的分布式事务对业务侵入很小。 它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用基于XA协议的分布式事务。 XA协议能够严格保障事务ACID特性。
严格保障事务ACID特性是一把双刃剑。 事务执行在过程中需要将所需资源全部锁定,它更加适用于执行时间确定的短事务。 对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务系统并发性能衰退明显。 因此,在高并发的性能至上场景中,基于XA协议的分布式事务并不是最佳选择。
5.6.3 柔性事务
如果将实现了ACID的事务要素的事务称为刚性事务的话,那么基于BASE事务要素的事务则称为柔性事务。BASE是基本可用、柔性状态和最终一致性这三个要素的缩写。
基本可用(Basically Available)保证分布式事务参与方不一定同时在线。
柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉。
而最终一致性(Eventually consistent)通常是通过消息可达的方式保证系统的最终一致性。
5.7 读写分离
5.7.1 读写分离核心概念
5.7.1.1 主库
添加、更新以及删除数据操作所使用的数据库,目前仅支持单主库。
5.7.1.2 从库
查询数据操作所使用的数据库,可支持多从库。
5.7.1.3 主从同步
将主库的数据异步的同步到从库的操作。由于主从同步的异步性,从库与主库的数据会短时间内不一致。
5.7.1.4 负载均衡策略
通过负载均衡策略将查询请求疏导至不同从库。
5.7.1.5 Config Map
配置读写分离数据源的元数据,可通过调用ConfigMapContext.getInstance()获取ConfigMap中的masterSlaveConfig数据。例:如果机器权重不同则流量可能不同,可通过ConfigMap配置机器权重元数据。
5.7.2 核心功能
提供了一主多从的读写分离配置,可独立使用,也可配合分库分表使用
同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
Spring命名空间
基于Hint的强制主库路由。
5.7.3 不支持范围
主库和从库的数据同步。
主库和从库的数据同步延迟导致的数据不一致。
主库双写或多写。
六、Sharding-JDBC实际操作
核心依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
6.1 配置分片算法前准备操作
在配置Sharding-JDBC分片算法之前,肯定都需要做的一些配置,配置数据源,数据节点,设置主键及生成算法这种基础的配置,有了这些配置之后,我们才能继续配置分片算法。
6.1.1 配置数据源
我这里设置了两个库,配置了两个数据源。
6.1.2 配置数据节点
$->是Sharding为我们提供的行表达式,这个表达式意思就是我们配置的数据节点为m0库course0表,m0库course1表,m1库course0表和m1库course1表,一共四个节点,这样的配置就很简便,我设置的库和表比较少,直接列举出来好像也行,但是如果分了很多库,很多表,逐个列举就会非常麻烦。
#数据节点
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{0..1}.course$->{0..1}
6.1.3 配置全局主键及生成策略
下面为course表设置的主键是cid,用了雪花算法保证全局主键唯一。
#设置主键
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
#主键生成策略,雪花算法
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
6.2 精准分片算法实践
6.2.1 配置类
首先要设置分片键设置逻辑表名为course的分表策略,这里就需要我们自定义算法类了
分库策略类似,将table-strategy改为database-strategy,设置一个分库策略的自定义算法类
#分片键
spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=user_id
#分表策略-精准
spring.shardingsphere.sharding.tables.course.table-strategy.standard.precise-algorithm-class-name=com.mine.sharding.algorithm.MyPreciseTableShardingAlgorithm
6.2.2 自定义算法类
实现Sharding提供的精准分片接口,重写一下doSharding方法,接下来的几个分片算法,也都是差不多的操作。
public class MyPreciseTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
//select * from course where cid = '' or cid in ('','')
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
String logicTableName = preciseShardingValue.getLogicTableName();
String columnName = preciseShardingValue.getColumnName();
Long cidValue = preciseShardingValue.getValue();
//实现course$->{cid%2}
BigInteger shardingValueB = BigInteger.valueOf(cidValue);
BigInteger resB = shardingValueB.mod(new BigInteger("2"));
String key = logicTableName+resB;
if (collection.contains(key)){
return key;
}
throw new UnsupportedOperationException("route:"+key+" is not supported,please check your config");
}
}
6.3 范围分片算法实践
6.3.1 配置类
#分片键
spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=user_id
#分表策略-范围
spring.shardingsphere.sharding.tables.course.table-strategy.standard.range-algorithm-class-name=com.mine.sharding.algorithm.MyRangeTableShardingAlgorithm
6.3.2 自定义算法类
public class MyRangeTableShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
Long upperValue = rangeShardingValue.getValueRange().upperEndpoint();
Long lowerValue = rangeShardingValue.getValueRange().lowerEndpoint();
String logicTableName = rangeShardingValue.getLogicTableName();
//course_$->{cid%2}
return Arrays.asList(logicTableName+"0",logicTableName+"1");
}
}
6.4 复合分片算法实践
什么是复合,或者是复杂,不再是单一条件了,可以看到,配置类中的sharding-column加s了,不再是单数了
这种复合的分片策略,支持多个分片键,按多个分片键组合出复杂的分片策略
6.4.1 配置类
#分片键,可多个
spring.shardingsphere.sharding.tables.course.table-strategy.complex.sharding-columns=cid,user_id
#分表策略
spring.shardingsphere.sharding.tables.course.table-strategy.complex.algorithm-class-name=com.mine.sharding.algorithm.MyComplexTableShardingAlgorithm
6.4.2 自定义算法类
public class MyComplexTableShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
Range<Long> cidRange = complexKeysShardingValue.getColumnNameAndRangeValuesMap().get("cid");
Collection<Long> userIdCol = complexKeysShardingValue.getColumnNameAndShardingValuesMap().get("user_id");
Long cidUpper = cidRange.upperEndpoint();
Long cidLower = cidRange.lowerEndpoint();
List<String> result = new ArrayList<>();
for (Long userId : userIdCol) {
BigInteger userIdB = BigInteger.valueOf(userId);
BigInteger target = userIdB.mod(new BigInteger("2"));
result.add(complexKeysShardingValue.getLogicTableName()+target);
}
return result;
}
}
6.5 Hint分片算法(强制路由)实践
6.5.1 配置类
配置文件中,没有指定分片键,为什么呢,大家可以看名字,强制路由,强制了,我也就没必要再预先设置一个分片键了,在程序中干预,指定SQL的目标数据库和表信息。
#Hint在配置中不指定分片键
#分表策略
spring.shardingsphere.sharding.tables.course.table-strategy.hint.algorithm-class-name=com.mine.sharding.algorithm.MyHintTableShardingAlgorithm
6.5.2 自定义算法类
public class MyHintTableShardingAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* Hint限制:
* 不支持union查询
* 不支持多层子查询
* 不支持函数计算
*/
@Override
public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
String key = hintShardingValue.getLogicTableName() + hintShardingValue.getValues().toArray()[0];
if (collection.contains(key)) {
return Arrays.asList(key);
}
throw new UnsupportedOperationException("route:" + key + " is not supported,please check your config");
}
}
6.6.3 测试类
Hint算法如何在程序中干预呢,我们写一个简单的测试方法
我们可以看到,是通过HintManager来指定,很方便
但是也有一点需要注意的地方,这个是线程安全的,用完之后得关掉,不能带到一下个线程里面。
/**
* Hint
* 强制路由
*/
@Test
public void queryCourseLHint() {
HintManager hintManager = HintManager.getInstance();
hintManager.addTableShardingValue("course", 0);
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
hintManager.close();
}
6.6 广播表实践
在所有的数据源里面,做同样的操作,就是说每个数据源都会保存同样的,全量的数据
spring.shardingsphere.sharding.broadcast-tables=dict
6.7 绑定表实践
对分片键相同的表做关联查询,最主要就是要避免笛卡尔积
spring.shardingsphere.sharding.binding-tables[0]=user,dict
6.8 读写分离实践
前提,在数据库层面配置好主从库
6.8.1 配置主从库数据源
6.8.2 配置主从节点读写分离
当然,这里也需要指定主键及生成算法
#主从配置、读写分离
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s
spring.shardingsphere.sharding.tables.student.actual-data-nodes=ds0.student
spring.shardingsphere.sharding.tables.student.key-generator.column=sid
spring.shardingsphere.sharding.tables.student.key-generator.type=SNOWFLAKE
这次的内容到这里就结束了,作者水平有限,文章不足之处敬请指出
Best Regards