精华内容
下载资源
问答
  • 数据库瓶颈不管是IO瓶颈还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载的活跃连接数的阈值。在业务service来看, 就是...分库和垂直分表第二种:网络IO瓶颈,请求的数据太多,网络...

    数据库瓶颈

    不管是IO瓶颈还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载的活跃连接数的阈值。在业务service来看, 就是可用数据库连接少甚至无连接可用,接下来就可以想象了(并发量、吞吐量、崩溃)。

    IO瓶颈第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询会产生大量的IO,降低查询速度->分库和垂直分表

    第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 ->分库

    CPU瓶颈第一种:SQl问题:如SQL中包含join,group by, order by,非索引字段条件查询等,增加CPU运算的操作->SQL优化,建立合适的索引,在业务Service层进行业务计算。

    第二种:单表数据量太大,查询时扫描的行太多,SQl效率低,增加CPU运算的操作。->水平分表。

    分库分表

    水平分库

    ea2c62e34dcfcb09f62f87760ad34263.png

    1、概念:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。

    2、结果:

    每个库的结构都一样

    每个库中的数据不一样,没有交集

    所有库的数据并集是全量数据

    3、场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库的情况下。

    4、分析:库多了,io和cpu的压力自然可以成倍缓解

    水平分表

    1112c8be783036039b2d5f071dc4ec4c.png

    1、概念:以字段为依据,按照一定策略(hash、range等),讲一个表中的数据拆分到多个表中。

    2、结果:

    每个表的结构都一样

    每个表的数据不一样,没有交集,所有表的并集是全量数据。

    3、场景:系统绝对并发量没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈,可以考虑水平分表。

    4、分析:单表的数据量少了,单次执行SQL执行效率高了,自然减轻了CPU的负担。

    垂直分库

    e4053a66250940116627bcd2cbe5c5ed.png

    1、概念:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。

    2、结果:

    每个库的结构都不一样

    每个库的数据也不一样,没有交集

    所有库的并集是全量数据

    3、场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块的情况下。

    4、分析:到这一步,基本上就可以服务化了。例如:随着业务的发展,一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再者,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

    垂直分表

    932458b80f24eb6d3f92b92ff5a7a72d.png

    1、概念:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表中(主表和扩展表)。

    2、结果:

    每个表的结构不一样。

    每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据。

    所有表的并集是全量数据。 3、场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大,以至于数据库缓存的数据行减少,查询时回去读磁盘数据产生大量随机读IO,产生IO瓶颈。

    4、分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能经常会查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表,这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获取全部数据就需要关联两个表来取数据。

    但记住千万别用join,因为Join不仅会增加CPU负担并且会将两个表耦合在一起(必须在一个数据库实例上)。关联数据应该在service层进行,分别获取主表和扩展表的数据,然后用关联字段关联得到全部数据。

    分库分表工具sharding-jdbc(当当)

    TSharding(蘑菇街)

    Atlas(奇虎360)

    Cobar(阿里巴巴)

    MyCAT(基于Cobar)

    Oceanus(58同城)

    Vitess(谷歌) 各种工具的利弊自查

    分库分表带来的问题

    分库分表能有效缓解单机和单表带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来一些问题,下面将描述这些问题和解决思路。事务一致性问题

    分布式事务

    当更新内容同时存在于不同库找那个,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用“XA协议”和“两阶段提交”处理。 分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间,导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

    最终一致性

    对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误立刻回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等。

    跨节点关联查询join问题

    切分之前,系统中很多列表和详情表的数据可以通过join来完成,但是切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用Join查询。解决的一些方法:

    全局表

    全局表,也可看做“数据字典表”,就是系统中所有模块都可能依赖的一些表,为了避免库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少修改,所以不必担心一致性的问题。

    字段冗余

    一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如,订单表在保存userId的时候,也将userName也冗余的保存一份,这样查询订单详情顺表就可以查到用户名userName,就不用查询买家user表了。但这种方法适用场景也有限,比较适用依赖字段比较少的情况,而冗余字段的一致性也较难保证。

    数据组装

    在系统service业务层面,分两次查询,第一次查询的结果集找出关联的数据id,然后根据id发起器二次请求得到关联数据,最后将获得的结果进行字段组装。这是比较常用的方法。

    ER分片

    关系型数据库中,如果已经确定了表之间的关联关系(如订单表和订单详情表),并且将那些存在关联关系的表记录存放在同一个分片上,那么就能较好地避免跨分片join的问题,可以在一个分片内进行join。在1:1或1:n的情况下,通常按照主表的ID进行主键切分。

    跨节点分页、排序、函数问题

    跨节点多库进行查询时,会出现limit分页、order by 排序等问题。分页需要按照指定字段进行排序,当排序字段就是分页字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂.需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户如下图:

    0ea51585a04f7d1f1b2c62c9fd0b9e7d.png

    上图只是取第一页的数据,对性能影响还不是很大。但是如果取得页数很大,情况就变得复杂的多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体排序,这样的操作很耗费CPU和内存资源,所以页数越大,系统性能就会越差。

    在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总再次计算。

    全局主键避重问题

    在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成ID无法保证全局唯一。因此需要单独设计全局主键,避免跨库主键重复问题。这里有一些策略:

    UUID

    UUID标准形式是32个16进制数字,分为5段,形式是8-4-4-4-12的36个字符。 UUID是最简单的方案,本地生成,性能高,没有网络耗时,但是缺点明显,占用存储空间多,另外作为主键建立索引和基于索引进行查询都存在性能问题,尤其是InnoDb引擎下,UUID的无序性会导致索引位置频繁变动,导致分页。

    结合数据库维护主键ID表

    在数据库中建立sequence表:CREATE TABLE `sequence` (

    `id` bigint(20) unsigned NOT NULL auto_increment,

    `stub` char(1) NOT NULL default '',

    PRIMARY KEY (`id`),

    UNIQUE KEY `stub` (`stub`)

    ) ENGINE=MyISAM;

    复制代码

    stub字段设置为唯一索引,同一stub值在sequence表中只有一条记录,可以同时为多张表生辰全局ID。使用MyISAM引擎而不是InnoDb,已获得更高的性能。MyISAM使用的是表锁,对表的读写是串行的,所以不用担心并发时两次读取同一个ID。当需要全局唯一的ID时,执行:REPLACE INTO sequence (stub) VALUES ('a');

    SELECT LAST_INSERT_ID();

    复制代码

    此方案较为简单,但缺点较为明显:存在单点问题,强依赖DB,当DB异常时,整个系统不可用。配置主从可以增加可用性。另外性能瓶颈限制在单台Mysql的读写性能。 另有一种主键生成策略,类似sequence表方案,更好的解决了单点和性能瓶颈问题。这一方案的整体思想是:建立2个以上的全局ID生成的服务器,每个服务器上只部署一个数据库,每个库有一张sequence表用于记录当前全局ID。 表中增长的步长是库的数量,起始值依次错开,这样就能将ID的生成散列到各个数据库上

    2429849b0abf775de72a5ae1125b6cfe.png

    这种方案将生成ID的压力均匀分布在两台机器上,同时提供了系统容错,第一台出现了错误,可以自动切换到第二台获取ID。但有几个缺点:系统添加机器,水平扩展较复杂;每次获取ID都要读取一次DB,DB的压力还是很大,只能通过堆机器来提升性能。

    Snowflake分布式自增ID算法

    91fbc509914d4c3df6186762474e1272.png

    Twitter的snowfalke算法解决了分布式系统生成全局ID的需求,生成64位Long型数字,组成部分:

    第一位未使用

    接下来的41位是毫秒级时间,41位的长度可以表示69年的时间

    5位datacenterId,5位workerId。10位长度最多支持部署1024个节点

    最后12位是毫秒内计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列。

    数据迁移、扩容问题

    当业务高速发展、面临性能和存储瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据的迁移问题。一般做法是先读出历史数据,然后按照指定的分片规则再将数据写入到各分片节点中。此外还需要根据当前的数据量个QPS,以及业务发展速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片的单表数据量不超过1000W)

    什么时候考虑分库分表

    能不分就不分

    并不是所有表都需要切分,主要还是看数据的增长速度。切分后在某种程度上提升了业务的复杂程度。不到万不得已不要轻易使用分库分表这个“大招”,避免“过度设计”和“过早优化”。分库分表之前,先尽力做力所能及的优化:升级硬件、升级网络、读写分离、索引优化等。当数据量达到单表瓶颈后,在考虑分库分表。

    数据量过大,正常运维影响业务访问

    这里的运维是指:对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO

    对一个很大的表做DDL,MYSQL会锁住整个表,这个时间会很长,这段时间业务不能访问此表,影响很大。

    大表经常访问和更新,就更有可能出现锁等待。

    随着业务发展,需要对某些字段垂直拆分

    这里就不举例了。在实际业务中都可能会碰到,有些不经常访问或者更新频率低的字段应该从大表中分离出去。

    数据量快速增长

    随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。

    展开全文
  • 数据库分库分表中间件架构解析Processor(1) Processor(n) MySQL MySQL MySQL Cobar结构 Front-end Communication Application1 MySQL MySQL Protocol MySQL MySQL Monitor Configure MySQL MySQL SQL Executor SQL ...

    数据库分库分表中间件架构解析

    Processor(1) Processor(n) MySQL MySQL MySQL Cobar结构 Front-end Communication Application1 MySQL MySQL Protocol MySQL MySQL Monitor Configure MySQL MySQL SQL Executor SQL Router SQL Parser ResultMerger SQL Executor SQL Router SQL Parser ResultMerger ... Manager MySQL Protocol MySQL Protocol MySQL Protocol MySQL Protocol ManagementProtocol MySQL Protcol Adaptor (BIO) Data Nodes HA Pool 后台数据访问逻辑层次 HA Pool MySQL Protocol Adapter Data Node S M S M S M S M S M ip:port/offer ip:port/offer ip:port/ibank ip:port/ibank ip:port/ibank 数据库连接 基于协议数据包 与MySQL交互 基于MySQL协议 MySQL 新Cobar select * from offer select * from offer 基于MySQL协议 MySQL 新Cobar HEADER FIELD EOF ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW ROW EOF select * from offer Result 2GB select * from offer 后台数据访问逻辑层次 S M HA Pool MySQL Protocol Adapter Data Node 主备连接池 S M S M S M S M S M 后台数据访问逻辑层次 S M 心跳检测后端连接 S M S M S M S M S M HA Pool MySQL Protocol Adapter Data Node X 后台数据访问逻辑层次 S M 心跳检测后端连接 主库失效自动切换至备库 重置池中连接 S M S M S M S M S M HA Pool MySQL Protocol Adapter Data Node 分库2 分库3 分库4 分库5 分库6 分库1 后台数据访问逻辑层次 S M 水平拆分的分库 S M S M S M S M S M HA Pool MySQL Protocol Adapter Data Node 分库2 分库3 分库4 分库5 分库6 分库1 物理机 灵活的层间对应关系 S M S M S M S M S M S M 物理机 物理机 物理机 物理机 物理机 大纲 中间件引入 Cobar策略 系统实现 MySQL协议 通信 解析 后端连接 实施应用 Cobar 的部署 Cobar集群 Cobar集群 Cobar集群 Cobar集群 Cobar集群 Cobar集群 Cobar集群 ... 青岛 杭州 美国 中文站 国际站 offer/ibank/snapshot…… messagecenter hermes product ... ... CobarManager CobarManager CobarManager Cobar 的部署 Cobar集群 德胜机房Cobar集群 兴义机房Cobar集群 Cobar集群 ... 青岛 杭州 美国 ... ... CobarManager CobarManager CobarManager Cobar集群 MySQL Protocol MySQL Protocol MySQL Protocol MySQL Protocol MySQL Protocol MySQL Protocol HTTP 数据迁移 场景 MySQL数据库节点扩容 拆分规则更改 目标 迁移过程中应用保持可用 数据不能丢失、多余或者不一致 迁移步骤 数据的全量dump 变更数据的增量dump 路由规则切换 清理 分库1 分库2 分库1 分库2 分库3 数据分片数量变更 分库2 分库1 全量dump 分库3 分库1 分库2 分库3 分库2 分库1 全量dump 分库3 分库1 分库2 分库3 dump dump 此时仍使用两份分库的拆分规则 分库2 分库1 全量dump 分库3 invisible invisib

    展开全文
  • 摘要:本文通过实际案例,说明如何按日期来对订单数据进行水平分库分表,实现数据的分布式查询和操作。
    摘要:本文通过实际案例,说明如何按日期来对订单数据进行水平分库和分表,实现数据的分布式查询和操作。

    本文分享自华为云社区《数据库分库分表Java实战经验总结 丨【绽放吧!数据库】》,作者: jackwangcumt。

    我们知道,当前的应用都离不开数据库,随着数据库中的数据越来越多,单表突破性能上限记录时,如MySQL单表上线估计在近千万条内,当记录数继续增长时,从性能考虑,则需要进行拆分处理。而拆分分为横向拆分和纵向拆分。一般来说,采用横向拆分较多,这样的表结构是一致的,只是不同的数据存储在不同的数据库表中。其中横向拆分也分为分库和分表。

    1 示例数据库准备

    为了说清楚如何用Java语言和相关框架实现业务表的分库和分表处理。这里首先用MySQL数据库中创建两个独立的数据库实例,名字为mydb和mydb2,此可演示分库操作。另外在每个数据库实例中,创建12个业务表,按年月进行数据拆分。具体的创建表脚本如下:

    CREATE TABLE `t_bill_2021_1` (
      `order_id` bigint(20) NOT NULL  COMMENT '订单id',
      `user_id` int(20) NOT NULL COMMENT '用户id',
      `address_id` bigint(20) NOT NULL COMMENT '地址id',
      `status` char(1) DEFAULT NULL COMMENT '订单状态',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      PRIMARY KEY (`order_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    
    CREATE TABLE `t_bill_2021_2` (
      `order_id` bigint(20) NOT NULL  COMMENT '订单id',
      `user_id` int(20) NOT NULL COMMENT '用户id',
      `address_id` bigint(20) NOT NULL COMMENT '地址id',
      `status` char(1) DEFAULT NULL COMMENT '订单状态',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      PRIMARY KEY (`order_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
    -- 省略....
    CREATE TABLE `t_bill_2021_12` (
      `order_id` bigint(20) NOT NULL  COMMENT '订单id',
      `user_id` int(20) NOT NULL COMMENT '用户id',
      `address_id` bigint(20) NOT NULL COMMENT '地址id',
      `status` char(1) DEFAULT NULL COMMENT '订单状态',
      `create_time` datetime DEFAULT NULL COMMENT '创建时间',
      PRIMARY KEY (`order_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    成功执行脚本后,在MySQL管理工具中可以看到如下的示例界面:

    2 分库分表实现

    在Java语言下的框架中,有众多的开源框架,其中关于分库分表的框架,可以选择Apache ShardingSphere,其官网介绍说:ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既能够独立部署,又支持混合部署配合使用的产品组成。 它们均提供标准化的数据水平扩展分布式事务分布式治理等功能,可适用于如 Java 同构、异构语言、云原生等各种多样化的应用场景。Apache ShardingSphere 5.x 版本开始致力于可插拔架构。 目前,数据分片、读写分离、数据加密、影子库压测等功能,以及 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。官网地址为: https://shardingsphere.apache.org/index_zh.html 。

    下面的示例采用Spring Boot框架来实现,相关的库通过Maven进行管理。首先给出pom.xml配置文件的定义:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.5.3</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    	<groupId>com.example</groupId>
    	<artifactId>wyd</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>wyd</name>
    	<description>Demo project for Spring Boot</description>
    	<properties>
    		<java.version>1.8</java.version>
    		<mybatis-plus.version>3.1.1</mybatis-plus.version>
    		<sharding-sphere.version>4.0.0-RC2</sharding-sphere.version>
    		<shardingsphere.version>5.0.0-beta</shardingsphere.version>
    	</properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>2.0.1</version>
    		</dependency>
    		<dependency>
    			<groupId>com.baomidou</groupId>
    			<artifactId>mybatis-plus-boot-starter</artifactId>
    			<version>${mybatis-plus.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.projectlombok</groupId>
    			<artifactId>lombok</artifactId>
    			<optional>true</optional>
    		</dependency>
    		<dependency>
    			<groupId>joda-time</groupId>
    			<artifactId>joda-time</artifactId>
    			<version>2.9.8</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.shardingsphere</groupId>
    			<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    			<version>${sharding-sphere.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.apache.shardingsphere</groupId>
    			<artifactId>sharding-jdbc-spring-namespace</artifactId>
    			<version>${sharding-sphere.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
    			<scope>runtime</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.postgresql</groupId>
    			<artifactId>postgresql</artifactId>
    			<scope>runtime</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    			<scope>test</scope>
    		</dependency>
    	</dependencies>
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.boot</groupId>
    				<artifactId>spring-boot-maven-plugin</artifactId>
    			</plugin>
    		</plugins>
    	</build>
    </project>

    其次,给出一个实体类,它对应于上述创建的数据库表t_bill,其定义如下:

    package com.example.wyd.dao;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import java.util.Date;
    @Data
    @TableName("t_bill")
    public class Bill {
        private Long orderId;
        private Integer userId;
        private Long addressId;
        private String status;
        private Date createTime;
        public void setOrderId(Long orderId) {
            this.orderId = orderId;
        }
        public void setUserId(Integer userId) {
            this.userId = userId;
        }
        public void setAddressId(Long addressId) {
            this.addressId = addressId;
        }
        public void setStatus(String status) {
            this.status = status;
        }
        public void setCreateTime(Date createTime) {
            this.createTime = createTime;
        }
    }

    映射类BillMapper定义如下:

    package com.example.wyd.mapper;
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.example.wyd.dao.Bill;
    public interface BillMapper extends BaseMapper<Bill> {
    
    }

    服务类接口定义如下:

    package com.example.wyd.service;
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.example.wyd.dao.Bill;
    public interface BillService extends IService<Bill> {
    
    }

    服务类接口的实现类定义如下:

    package com.example.wyd.service;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.example.wyd.dao.Bill;
    import com.example.wyd.mapper.BillMapper;
    import org.springframework.stereotype.Service;
    @Service
    public class BillServiceImpl extends ServiceImpl<BillMapper, Bill> implements BillService {
    
    }

    这里我们采用了MybatisPlus框架,它可以很方便的进行数据库相关操作,而无需过多写SQL来实现具体业务逻辑。通过上述定义,通过继承接口的方式,并提供实体类的定义,MybatisPlus框架会通过反射机制来根据数据库设置来生成SQL语句,其中包含增删改查接口,具体的实现我们并未具体定义。

    下面定义一个自定义的分库算法,具体实现如下:

    package com.example.wyd;
    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
    import java.util.Collection;
    //自定义数据库分片算法
    public class DBShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
        @Override
        public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
            //真实数据库节点
            availableTargetNames.stream().forEach((item) -> {
               System.out.println("actual db:" + item);
            });
            //逻辑表以及分片的字段名
            System.out.println("logicTable:"+shardingValue.getLogicTableName()+";shardingColumn:"+ shardingValue.getColumnName());
            //分片数据字段值
            System.out.println("shardingColumn value:"+ shardingValue.getValue().toString());
            //获取字段值
            long orderId = shardingValue.getValue();
            //分片索引计算 0 , 1
            long db_index = orderId & (2 - 1);
            for (String each : availableTargetNames) {
                if (each.equals("ds"+db_index)) {
                    //匹配的话,返回数据库名
                    return each;
                }
            }
            throw new IllegalArgumentException();
        }
    }

    下面给出数据的分表逻辑,这个定义稍显复杂一点,就是根据业务数据的日期字段值,根据月份落入对应的物理数据表中。实现示例代码如下:

    package com.example.wyd;
    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
    import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
    import java.util.Collection;
    import java.util.Date;
    //表按日期自定义分片
    public class TableShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
        @Override
        public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
            //真实数据库节点
            availableTargetNames.stream().forEach((item) -> {
                System.out.println("actual db:" + item);
            });
            //逻辑表以及分片的字段名
            System.out.println("logicTable:"+shardingValue.getLogicTableName()+";shardingColumn:"+ shardingValue.getColumnName());
            //分片数据字段值
            System.out.println("shardingColumn value:"+ shardingValue.getValue().toString());
            //获取表名前缀
            String tb_name = shardingValue.getLogicTableName() + "_";
            //根据日期分表
            Date date = shardingValue.getValue();
            String year = String.format("%tY", date);
            String mon =String.valueOf(Integer.parseInt(String.format("%tm", date)));
            //String dat = String.format("%td", date); //也可以安装年月日来分表
            // 选择表
            tb_name = tb_name + year + "_" + mon;
            //实际的表名
            System.out.println("tb_name:" + tb_name);
            for (String each : availableTargetNames) {
                //System.out.println("availableTableName:" + each);
                if (each.equals(tb_name)) {
                    //返回物理表名
                    return each;
                }
            }
            throw new IllegalArgumentException();
        }
    }

    数据的分库分表可以在Spring Boot的属性配置文件中进行设(application.properties):

    server.port=8080
    #########################################################################################################
    # 配置ds0 和ds1两个数据源
    spring.shardingsphere.datasource.names = ds0,ds1
    
    #ds0 配置
    spring.shardingsphere.datasource.ds0.type = com.zaxxer.hikari.HikariDataSource
    spring.shardingsphere.datasource.ds0.driver-class-name = com.mysql.cj.jdbc.Driver
    spring.shardingsphere.datasource.ds0.jdbc-url = jdbc:mysql://127.0.0.1:3306/mydb?characterEncoding=utf8
    spring.shardingsphere.datasource.ds0.username = uname
    spring.shardingsphere.datasource.ds0.password = pwd
    
    #ds1 配置
    spring.shardingsphere.datasource.ds1.type = com.zaxxer.hikari.HikariDataSource
    spring.shardingsphere.datasource.ds1.driver-class-name = com.mysql.cj.jdbc.Driver
    spring.shardingsphere.datasource.ds1.jdbc-url = jdbc:mysql://127.0.0.1:3306/mydb2characterEncoding=utf8
    spring.shardingsphere.datasource.ds1.username = uname
    spring.shardingsphere.datasource.ds1.password = pwd
    #########################################################################################################
    # 默认的分库策略:id取模
    spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column = id
    spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression = ds$->{id % 2}
    #########################################################################################################
    spring.shardingsphere.sharding.tables.t_bill.actual-data-nodes=ds$->{0..1}.t_bill_$->{2021..2021}_$->{1..12}
    #数据库分片字段
    spring.shardingsphere.sharding.tables.t_bill.database-strategy.standard.sharding-column=order_id
    #自定义数据库分片策略
    spring.shardingsphere.sharding.tables.t_bill.database-strategy.standard.precise-algorithm-class-name=com.example.wyd.DBShardingAlgorithm
    #表分片字段
    spring.shardingsphere.sharding.tables.t_bill.table-strategy.standard.sharding-column=create_time
    #自定义表分片策略
    spring.shardingsphere.sharding.tables.t_bill.table-strategy.standard.precise-algorithm-class-name=com.example.wyd.TableShardingAlgorithm
    #########################################################################################################
    # 使用SNOWFLAKE算法生成主键
    spring.shardingsphere.sharding.tables.t_bill.key-generator.column = order_id
    spring.shardingsphere.sharding.tables.t_bill.key-generator.type = SNOWFLAKE
    spring.shardingsphere.sharding.tables.t_bill.key-generator.props.worker.id=123
    #########################################################################################################
    spring.shardingsphere.props.sql.show = true

    最后,我们给出一个定义的Controller类型,来测试分库分表的查询和保存操作是否正确。HomeController类定义如下:

    package com.example.wyd.controller;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.example.wyd.dao.Bill;
    import com.example.wyd.service.BillService;
    import org.joda.time.DateTime;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.List;
    @RestController
    @RequestMapping("/api")
    public class HomeController {
        @Autowired
        private BillService billService;
        //http://localhost:8080/api/query?start=2021-02-07%2000:00:00&end=2021-03-07%2000:00:00
        @RequestMapping("/query")
        public List<Bill> queryList(@RequestParam("start") String start, @RequestParam("end") String end) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                Date date = sdf.parse(start);
                Date date2 = sdf.parse(end);
                QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
                queryWrapper.ge("create_time",date)
                        .and(qw-> qw.le("create_time", date2)).last("limit 1,10");
                List<Bill> billIPage = billService.list(queryWrapper);
                System.out.println(billIPage.size());
                billIPage.forEach(System.out::println);
                return billIPage;
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return null;
        }
        //http://localhost:8080/api/save?userid=999&addressId=999&status=M&date=2021-03-07%2000:00:00
        @RequestMapping("/save")
        public String Save(@RequestParam("userid") int userId, @RequestParam("addressId") long AddressId,
                           @RequestParam("status") String status
                ,@RequestParam("date") String strDate) {
            String ret ="0";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                Date date = sdf.parse(strDate);
                Bill bill = new Bill();
                bill.setUserId(userId);
                bill.setAddressId(AddressId);
                bill.setStatus(status);
                bill.setCreateTime(date);
                boolean isOk = billService.save(bill);
                if (isOk){
                    ret ="1";
                }
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return ret;
        }
    }

    至此,我们可以用测试类初始化一些数据,并做一些初步的数据操作测试:

    package com.example.wyd;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.example.wyd.dao.Bill;
    import com.example.wyd.dao.Order;
    import com.example.wyd.service.BillService;
    import com.example.wyd.service.OrderService;
    import org.joda.time.DateTime;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.*;
    
    public class OrderServiceImplTest extends WydApplicationTests {
        @Autowired
        private BillService billService;
        @Test
        public void testBillSave(){
            for (int i = 0 ; i< 120 ; i++){
                Bill bill = new Bill();
                bill.setUserId(i);
                bill.setAddressId((long)i);
                bill.setStatus("K");
                bill.setCreateTime((new Date(new DateTime(2021,(i % 11)+1,7,00, 00,00,000).getMillis())));
                billService.save(bill);
            }
        }
        @Test
        public void testGetByOrderId(){
            long id = 626038622575374337L; //根据数据修改,无数据会报错
            QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("order_id", id);
            Bill bill = billService.getOne(queryWrapper);
            System.out.println(bill.toString());
        }
    
        @Test
        public void testGetByDate(){
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                Date date = sdf.parse("2021-02-07 00:00:00");
                QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("create_time",date);
                List<Bill> billIPage = billService.list(queryWrapper);
                System.out.println(billIPage.size());
                System.out.println(billIPage.toString());
            } catch (ParseException e) {
                e.printStackTrace();
            }
    
        }
    
        @Test
        public void testGetByDate2(){
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            try {
                Date date = sdf.parse("2021-02-07 00:00:00");
                Date date2 = sdf.parse("2021-03-07 00:00:00");
                QueryWrapper<Bill> queryWrapper = new QueryWrapper<>();
                queryWrapper.ge("create_time",date)
                .and(qw-> qw.le("create_time", date2));
                List<Bill> billIPage = billService.list(queryWrapper);
                System.out.println(billIPage.size());
                billIPage.forEach(System.out::println);
    
            } catch (ParseException e) {
                e.printStackTrace();
            }
    
        }
    }

    执行上述测试,通过后会生成测试数据。

    3 验证

    打开浏览器,输入网址进行查询测试:http://localhost:8080/api/query?start=2021-02-07%2000:00:00&end=2021-03-07%2000:00:00

    输入如下网址进行数据新增测试:http://localhost:8080/api/save?userid=999&addressId=999&status=M&date=2021-03-07%2000:00:00

    通过跟踪分析,此数据落入如下的表中,SQL语句如下:

    SELECT * FROM mydb2.t_bill_2021_3 LIMIT 0, 1000

    这里还需要注意,ShardingSphere 还支持分布式事务,感兴趣的可以阅读官网相关资料进行学习。

    点击关注,第一时间了解华为云新鲜技术~

    展开全文
  • 在日常的工作中,关系型数据库本身比较容易成为系统的瓶颈点,虽然读写分离能分散数据库的读写压力,但并没有分散存储压力,当数据量达到千万甚至上亿时,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在...

    在日常的工作中,关系型数据库本身比较容易成为系统的瓶颈点,虽然读写分离能分散数据库的读写压力,但并没有分散存储压力,当数据量达到千万甚至上亿时,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在以下几个方面:

    数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会降下。

    数据库文件会得很大,数据库备份和恢复需要耗时很长。

    数据库文件越大,极端情况下丢失数据的风险越高。

    因此,当流量越来越大时,且单机容量达到上限时,此时需要考虑对其进行切分,切分的目的就在于减少单机数据库的负担,将由多台数据库服务器一起来分担,缩短查询时间。

    切分策略

    数据切分分为两种方式,纵向切分和水平切分

    纵向切分

    常见有纵向分库纵向分表两种。

    1). 纵向分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库,做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与“微服务治理”的做法相似,每个微服务使用单独的一个数据库。

    2). 垂直分表是基于数据库中的列进行,某个表字段较多,可以新建一张扩展表,将不经常用或者字段长度较大的字段拆出到扩展表中。在字段很多的情况下,通过大表拆小表,更便于开发与维护,也能避免跨页问题,MYSQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的开销。另外,数据库以行为单位将数据加载到内存中,这样表中字段长度越短且访问频次较高,内存能加载更多的数据,命中率更高,减少磁盘IO,从而提升数据库的性能。

    垂直切分的优点:

    解决业务系统层面的耦合,业务清晰

    与微服务的治理类似,也能对不同业务的数据进行分级管理,维护,监控,扩展等。

    高并发场景下,垂直切分一定程度的提升IO,数据库连接数,单机硬件资源的瓶颈。

    垂直切分的缺点

    部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度。

    分布式事处理复杂

    依然存在单表数据量过大的问题。

    水平切分

    当一个应用难以再细粒度的垂直切分或切分后数据量行数依然巨大,存在单库读写,存储性能瓶颈,这时候需要进行水平切分。

    水平切分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。

    库内分表只解决单一表数据量过大的问题,但没有将表分布到不同机器的库上,因些对于减轻mysql的压力来说帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。

    水平切分优点

    不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力。

    应用端改造较小,不需要拆分业务模块。

    水平切分缺点

    跨分片的事务一致性难以保证

    跨库的join关联查询性能较差

    数据多次扩展维度和维护量极大。

    路由规则

    水平切分后同一张表会出现在多个数据库或表中,每个库和表的内容不同,对于水平分表后分库后,如何知道哪条数据在哪个库里或表里,则需要路由算法进行计算,这个算法会引入一定的复杂性。

    范围路由

    选取有序的数据列,如时间戳作为路由的条件,不同分段分散到不同的数据库表中,以最常见的用户ID为例,路由算法可以按照1000000的范围大小进行分段,1 ~ 9999999放到数据库1的表中,10000000~199999999放到数据库2的表中,以此累推。

    范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多增加维护复杂度,分段太大可能会导致单表依然存在性能问题,按一般大老们的经验,分段大小100W至2000W之间,具体需要根据业务选 取合适的分段大小。

    范围路由的优点

    可以随着数据的增加平滑地扩充新的表或库,原有的数据不需要动。

    单表大小可控

    使用分片字段进行范围查找时,连续分片可快速定位查询,有效避免分片查询的问题。

    热点数据成为性能瓶颈,连续分片可能存在数据热点,例如按时单字段分片,有些分片存储最近时间内的数据,可能会被频繁读写,而有些历史数据则很少被查询。

    hash算法

    选取某个列或几个列的值进行hash运算,然后根据hash的结果分散到不同的数据库表中,以用ID为例,假如我们一开始就规划10个数据库表,路由算法可以简单地用id % 10的值来表示数据所属的数据库编号,ID为985的用户放到编号为5的子表中。ID为10086编号放到编号为6的表中。

    Hash路由设计的复杂点主要体现 在初始表数量的选取上,表数量太多维护比较麻烦,表数量太小又可能导致单表性能存在问题。而用Hash路由后,增加字表数量是非常麻烦的,所有数据都要重新分布。

    Hash路由的优缺点与范围路由相反,Hash路由的优点是表分布比较均匀,缺点是扩容时很麻烦,所有数据均需要重新分布。

    路由配置

    配置路由就是路由表,用一张独立的表来记录路由信息。同样以用户ID为例,我们新增一张ROUTER表,这个表包含table_Id两列,根据user_id就可以查询对应的修改路由表就可以了。

    配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。

    其缺点就是必须多查询一次,会影响整体性能,而且路由表本身如果太大,性能会成为瓶颈点,如果我们再将路由表分库分表,则又面临一个死循环。

    分库分表带来的问题

    join操作

    水平分表后,虽然物理上分散在多个表中,如果需要与其它表进行join查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并。

    COUNT(*)操作

    水平分表后,某些场景下需要将这些表当作一个表来处理,那么count(*)显得没有那么容易 了。

    order by 操作

    分表后,数据分散到多个表中,排序操作无法在数据库中完成,只能由业务代码或数据中间件分别查询每个子表中的数据,然后汇总进行排序。

    展开全文
  • 为了保证数据库的查询效率,当数据达成一定量时建议进行分表操作1、oracle当oracle单表的数据量大于2000万行时,建议进行水平拆。2、mysql当mysql单表的数据量大于1000万行时,建议进行水平拆。单表容量到了1000...
  • 数据库分库分表思路

    2021-02-02 05:34:24
    当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。数据库分布式核心...
  • 参考《人人都是架构师》大型网站几乎时时刻刻都在接收着高并发和海量数据的洗礼,随着用户规模的线性上升,单的性能瓶颈会逐渐暴露出来,由于数据的检索效率越来越慢,导致生产环境中产生较多的慢速SQL。...
  • 随着业务数据的增加,原有的数据库性能瓶颈凸显,以此就需要对数据库进行分库分表操作。 为啥需要分库分表 随着业务数据的增加,原有的数据库性能瓶颈凸显,主要体现在以下两个方面。 IO瓶颈 IO瓶颈主要有以下几种...
  • 关于数据库的扩展主要包括:业务拆分、主从复制,数据库分库分表。这篇文章主要讲述数据库分库分表(1)业务拆分业务起步初始,为了加快应用上线和快速迭代,很多应用都采用集中式的架构。随着业务系统的扩大,...
  • 另一方面,在分库分表以后还需要保证分库分表的和主库的事务一致性二、需要解决问题2.1 事务问题由于分库分表之后,新表在另外一个数据库中,如何保证主库和分库的事务性是必须要解决的问题。通过在主库中创建一个...
  • 对于数据库的扩展方案,主要包括:业务拆分、主从复制,数据库分库分表。这篇文章主要讲述数据库分库分表。 一、MySQL扩展方案具体的实现方式 (1)业务拆分 业务起步初始,为了加快应用上线和快速迭代,很多...
  • 一、为什么要分库分表 解决大数据存储时数据访问性能,具体来说就是解决超大容量问题和性能问题。 举例说明,订单表或用户表如果数据量达到上亿条记录,此时数据库的IO能力、处理能力就会出现一个瓶颈(MySQL官方...
  • 数据库分库分表学习

    2021-02-05 17:09:21
    大型网站数据库分库分表分库分表方案:垂直&水平1、什么是垂直拆分?指的是将一个包含了很多表的数据库,根据表的功能的不同,拆分为多个小的数据库,每个库中包含部分表。、垂直拆分的另外2种用途?1)、将一个包含...
  • 1:为什么要分库分表 业务快速发展,单数据库出现性能瓶颈的时候,要将数据进行切分。将原来在一台数据库上的数据,分散到多台数据库中,降低单体数据库负载 2:数据切分类型 1:垂直切分 垂直切分是将多个业务...
  • 数据库分库分表

    2021-02-02 05:33:29
    数据库中的数据量不一定是可控的,如果未进行分库分表,随着时间和业务的发展,库中的表会越来越多,表中的数据量也会越来越大,相应的,数据操作,增删改查的开销也会越来越大;另外,由于无法进行分布式部署,而一...
  • 分库分表 1 为什么分库分表 ​ 随着平台的业务发展,数据可能会越来越多,甚至达到亿级。以MySQL为例,单库数据量在5000万以内性能比较好,超过阈值后性能会随着数据量的增大而明显降低。单表的数据量超过1000w,...
  • 第一部分:实施策略数据库分库分表(sharding)实施策略图解1. 垂直切分垂直切分的依据原则是:将业务紧密,表间关联密切的表划分在一起,例如同一模块的表。结合已经准备好的数据库ER图或领域模型图,仿照活动图中的...
  • 在对数据库中的数据进行拆分的时候,也会带来以下问题: 事务问题;...而单库分表和分库分表会涉及到分布式事务,目前数据库并不支持跨库事务,所以在这一块需要解决分布式事务可能带来的不一致性。 分
  • mysql数据库分库分表方案,一旦数据库过于庞大,尤其是当写入过于频繁,非常难由一台主机支撑的时候,我们还是会面临到扩展瓶颈。这时候,我们就必须许找其它技术手段来解决这个瓶颈,那就是我们这一章所要介绍恶的...
  • 今天我们来学习MYSQL的分库分表面对访问数据库海量数据的情况,这张图带你解决数据库压力问题1、为什么要进行分库分表???分库分表是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成...
  • 如果数据量过大,大家一般会分库分表。分库需要注意的内容比较少,但分表需要注意的内容就多了。 工作这几年没遇过数据量特别大的业务,那些过亿的数据,因为索引设置合理,单表性能没有影响,所以实战中一直没用过...
  • 前言作为一个数据库,作为数据库中的...MyBatis实现分表最简单步骤既然文章的标题都这么写了,不如直接上干货来的比较实际,我们就先来看看如何实现最简单的分表。1、我们模拟用户表数据量超过千万(虽然实际不太可...
  • 数据库分库分表概述

    2021-03-29 14:41:01
    简单的数据库分区以及单个数据服务器已经不能满足数据的增长,这个时候我们就需要用到数据库集群了,有了数据库的集群肯定会涉及到数据库的分库分表操作数据库的分库分表操作又统称为数据库分片,其分为垂直拆分和...
  • 当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从、优化索引,做很多操作时性能仍下降严重。 此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。 数据库分布式核心...
  • 数据库分库分表

    2021-03-22 16:24:38
    主从复制-读写分离可以类比于redis的主从复制-哨兵模式,然后分库分表可以类比于redis的集群模式。总得来说,我感觉对于大规模数据的优化,主从复制-读写分离算是初级的优化,然后如果数据规模进一步庞大和QPS变得...
  • 一、数据库瓶颈1、IO瓶颈2、CPU瓶颈二、分库分表1、水平分库2、水平分表3、垂直分库4、垂直分表三、分库分表工具四、分库分表步骤五、分库分表...容问题六、分库分表总结七、分库分表示例一、数据库瓶颈↑不管是IO瓶颈...
  • 近期,当当开源了数据库分库分表中间件sharding-jdbc。Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据库分库分表访问。Sharding-JDBC是继dubbox和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 77,058
精华内容 30,823
关键字:

数据库分库分表怎么分的