一道经常考的题,必须掌握。

注:拆分虽然不难,但是有很多细节,也不要轻易拆。

什么情况下需要拆分?

主要需要考虑两个维度:
1、条数
如果是mysql库,单表超过1000万就需要拆分了,因为无论如何加索引、优化查询可能还是解决不了问题。
如果是oracle库,单表10亿以内都可以通过优化来解决,这里不这么高了,1亿吧,超过1亿就需要拆分了

2、字段数
字段数过多也会影响性能。一般来说,单表超过50个字段就需要拆分了。

拆分的方案

主要有两种:
1、水平拆分
单表数据量过大用水平拆分
表结构基本不变,加分片字段(一个或几个),按条数分到不同片中。
2、垂直拆分
字段过多用垂直拆分
将原表字段拆到几个表里。

ShardingSphere-JDBC

如果是springboot项目,用ShardingSphere-JDBC实现分片比较成熟。
ShardingSphere-JDBC是一个开源项目。

步骤

1、引入依赖

<dependencies>
    <!-- ShardingSphere JDBC Core -->
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core</artifactId>
        <version>5.3.2</version> <!-- 请使用最新稳定版 -->
    </dependency>
    <!-- 连接池 (推荐 HikariCP) -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
        <version>5.0.1</version>
    </dependency>
    <!-- 数据库驱动 -->
    <!-- ... -->
</dependencies>

2、创建config-sharding.yaml并进行配置,如下:

# config-sharding.yaml
dataSources:
  ds0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://localhost:3306/ds0
    username: root
    password: root
  ds1:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    jdbcUrl: jdbc:mysql://localhost:3306/ds1
    username: root
    password: root
rules:
- !SHARDING
  tables:
    t_order:
      actualDataNodes: ds${0..1}.t_order_${0..1}
      databaseStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: db_hash_mod
      tableStrategy:
        standard:
          shardingColumn: order_id
          shardingAlgorithmName: table_mod
  shardingAlgorithms:
    db_hash_mod:
      type: HASH_MOD
      props:
        sharding-count: 2
    table_mod:
      type: MOD
      props:
        sharding-count: 2

3、java代码示例

import org.apache.shardingsphere.driver.api.yaml.YamlShardingSphereDataSourceFactory;
import javax.sql.DataSource;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class ShardingJdbcExample {
    public static void main(String[] args) throws Exception {
        // 1. 加载YAML配置,创建ShardingSphere数据源
        DataSource dataSource = YamlShardingSphereDataSourceFactory.createDataSource(
            new File("path/to/your/config-sharding.yaml")
        );

        // 2. 获取连接并执行SQL,ShardingSphere会自动处理路由和结果归并
        try (Connection conn = dataSource.getConnection()) {
            // 插入数据
            String insertSQL = "INSERT INTO t_order (user_id, order_id, status) VALUES (?, ?, ?)";
            try (PreparedStatement ps = conn.prepareStatement(insertSQL)) {
                ps.setLong(1, 123L); // user_id
                ps.setLong(2, 888L); // order_id
                ps.setString(3, "PAID");
                ps.executeUpdate();
            }

            // 查询数据
            String selectSQL = "SELECT * FROM t_order WHERE user_id = ?";
            try (PreparedStatement ps = conn.prepareStatement(selectSQL)) {
                ps.setLong(1, 123L);
                ResultSet rs = ps.executeQuery();
                while (rs.next()) {
                    System.out.println("Order ID: " + rs.getLong("order_id"));
                }
            }
        }
    }
}
分片中如何实现字典表联查呢?

有现成的策略:广播表
即:在每个分片中都建立相同的表结构,在配置中标记这些表为广播表,查询的时候就直接可以用了。

1、每个分片中都创建广播表。
2、通过配置声明哪些表为广播表。
数据会自动同步、查询时会自动路由。

为什么mysql单表数据量超过2000万就会明显卡顿?

这是一道常考题,答对加分。
核心:
1、mysql的最小物理存储单位是页。
2、3层树及以内是理想值,4层是临界值。

page的结构如下:

组成部分 英文名称 占用大小 说明
1、文件头 File Header 38 字节 页的“身份证”。记录页号、上一页/下一页地址(用于双向链表)、校验和等通用信息。
2、页头 Page Header 56 字节 页的“管家”。记录页内有多少记录、空闲空间起始位置、目录槽数量等内部管理信息。
3、最小/最大记录 Infimum & Supremum 26 字节 两条虚拟的“伪记录”。分别代表页内数据的最小值和最大值,作为边界标记,确保页内始终有数据可供查找。
4、用户记录 User Records 不定长 这里才是存你真实数据的地方。数据按主键顺序排列,包含行记录头和实际列数据。
5、空闲空间 Free Space 不定长 页内还没被使用的空间。当插入新数据时,会从这里分配空间。
6、页目录 Page Directory 不定长 页内的“索引”。存储了记录的偏移量槽(Slots),用于在页内进行二分查找,加快定位速度。
7、文件尾 File Trailer 8 字节 页的“封条”。包含校验和(Checksum)和魔数,用于检测页在磁盘读写后是否完整、损坏。
3层树可以存放多少数据?

这里主要和1层2层可以存的索引数有关。

注:索引业界一般按14byte计算(8byte的+6byte的指针)
16*1024/14约等于1170。

1层 只存放索引 1170
2层 只存放索引 1170*1170
3层 存放索引+数据

所以大约是2000万。

为什么多一次io差别这么大?

有人问:一次io约10ms,多一次也就10ms啊,应该慢1/3。
其实并不是,这不是简单的多一次的事情,而是全链路都放慢,效果是指数级的。

1、内存比磁盘快多了,多一次整体会都在等这次io。
2、通常来说,索引会放在内存中,多一层索引会大很多,内存如果放不下会挤出去,还要从io从新查,这就慢了非常多。
3、还有些其他原因,先不说了。

根据id拆分还是user_id拆分?

根据id拆分最简单,但是不太贴合业务。
根据user_id拆是比较主流的做法,因为它本身就有业务属性,相对于纯id拆分,从查询时就可以直接根据该user_id来定位分片。

水平拆分后有哪些问题?

1、要面临分片的问题。
2、复杂的sql不能直接写了。
3、必须要采用其他手段来辅助定位分片。

定位分片的常见手段?

1、分片字段直接定位 # 例如user_id
2、其他字段里面包含分片字段 # 例如order_no里面包含user_id
3、利用redis等缓存主要查询字段和分片字段的关系

其他

文档

ShardingSphere-JDBC git地址:
https://github.com/apache/shardingsphere/tree/master/examples/shardingsphere-jdbc-example

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐