SpringBoot整合Sharding-jdbc分库分表及ES搜索引擎解决无分片键查询

SpringBoot整合Sharding-jdbc分库分表及ES搜索引擎解决无分片键查询

  • [1. 分库分表的含义](#1. 分库分表的含义)
  • [2. 分库分表实战](#2. 分库分表实战)
    • [2.1 针对账户信息表的拆分方案](#2.1 针对账户信息表的拆分方案)
    • [2.2 Spring Boot 整合 Sharding-JDBC](#2.2 Spring Boot 整合 Sharding-JDBC)
      • [1. 项目依赖 (pom.xml)](#1. 项目依赖 (pom.xml))
      • [2. 配置文件 (application.yaml)](#2. 配置文件 (application.yaml))
      • [3. 数据库表结构](#3. 数据库表结构)
      • [4. 核心Java代码](#4. 核心Java代码)
      • [⚠️ 重要提醒](#⚠️ 重要提醒)
  • [3. 分库分表查询提醒](#3. 分库分表查询提醒)
    • [3.1 SQL查询的正确写法](#3.1 SQL查询的正确写法)
    • [3.2 `无分片键查询的解决方案`](#3.2 无分片键查询的解决方案)
    • [3.3 建议](#3.3 建议)
  • [4. ES搜索引擎实现方案与核心代码](#4. ES搜索引擎实现方案与核心代码)
    • [4.1 数据同步:将账户数据同步到ES](#4.1 数据同步:将账户数据同步到ES)
    • [4.2 Elasticsearch索引设计与Java代码](#4.2 Elasticsearch索引设计与Java代码)
      • [4.2.1 索引Mapping(相当于表结构)](#4.2.1 索引Mapping(相当于表结构))
      • [4.2.2 Spring Boot项目集成ES](#4.2.2 Spring Boot项目集成ES)
        • [1. 项目依赖 (pom.xml)](#1. 项目依赖 (pom.xml))
        • [2. 配置连接 (application.yaml)](#2. 配置连接 (application.yaml))
        • [3. 定义ES文档实体和仓库](#3. 定义ES文档实体和仓库)
        • [4. 核心查询服务实现](#4. 核心查询服务实现)
    • [4.3 关键注意事项](#4.3 关键注意事项)

🔍

1. 分库分表的含义

为了应对不同数据压力和业务场景,我们把"分库分表"分为3种物理拆分模式。具体含义如下:

模式 描述 适用场景
分表不分库 同一个数据库 内,将一张大表拆分成多个结构相同的小表(分片),不涉及多个数据库。 主要解决 单表数据量过大 导致查询效率下降 的问题,但数据库整体并发压力尚可。当单表数据量超过千万级或表容量超过2GB时使用。
分库不分表 将单个数据库拆分成多个不同 的库,每个库中表结构相同,数据不同。也就是,将数据分散到多个数据库中。 主要解决 高并发 问题,比如,数据库连接数不够 或 磁盘I/O达到瓶颈,就可以通过 "分库不分表" 来提升并发处理能力 和 分散存储压力。
分库又分表 同时进行库和表的拆分,数据被分散到多个数据库的多个表中。 主要解决 高并发和海量数据 同时存在的问题。这是最常见的终极拆分方案,能最大程度提升性能和扩展性。

2. 分库分表实战

假设现在要对 account_info(账户信息表)进行 "既分库又分表",该表有 唯一索引字段 ------ account_id(账户编号)

2.1 针对账户信息表的拆分方案

对于账户信息表,一个典型的既分库又分表的水平拆分方案如下:

  • 拆分目标 :将数据分散到 2 个库 (ds0, ds1),每个库有 2 张表 (account_info_0, account_info_1)。
  • 分片键 :选择 account_id (账户编号) 作为分片字段。因为该字段设置了唯一索引,这通常是理想的天然分片键。
  • 分片算法 :采用 account_id % 4(库数×表数)进行取模,确保数据均匀分布。最终路由逻辑是:
    • 库路由:ds${account_id % 2}
    • 表路由:account_info_${account_id % 2}
  • 关于唯一索引 :分库分表后,在【单个数据库】节点上给 account_id 设置唯一索引,只能保证在该节点内唯一,无法保证全局唯一 。因此,必须 使用 【分布式ID生成器(比如,雪花算法)】 来保证 【account_id】 在整个分片集群中 全局唯一。

2.2 Spring Boot 整合 Sharding-JDBC

以下配置基于 ShardingSphere-JDBC 5.2.1(2025年1月17日发布) 版本,实现上述分库分表方案。

  • ShardingSphere-JDBC-5.2.1
    • 应用场景 :Apache ShardingSphere‐JDBC 可以通过 Java, YAML, Spring 命名空间 和 Spring Boot Starter 这 **【4 种方式】**进行配置,开发者可根据场景选择适合的配置方式。
    • 官方文档:https://shardingsphere.apache.org/document/5.2.1/cn/overview/文档可下载pdf
  • ShardingSphere-JDBC-5.3.2

1. 项目依赖 (pom.xml)

首先引入必要的依赖,推荐使用 shardingsphere-jdbc-core-spring-boot-starter

xml 复制代码
    <properties>
        <java.version>17</java.version>
        <shardingsphere.version>5.2.1</shardingsphere.version>
        <mybatis-spring>4.0.0</mybatis-spring>
    </properties>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- Sharding-JDBC -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>${shardingsphere.version}</version>
        </dependency>
        
        <!-- spring-boot-starter-data-jdbc,会传递依赖 hikari数据库连接池  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <!-- MySQL驱动 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!-- MyBatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring}</version>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </dependency>
    </dependencies>

2. 配置文件 (application.yaml)

这是最核心的配置部分,定义了数据源、分片规则和主键生成策略。

yaml 复制代码
spring:
  datasource: # 无需配置,由Sharding-JDBC管理
  shardingsphere:
    # 1. 数据源配置(分库)
    # 配置示例在官方文档中有。
    # 数据源配置源码如下:
    # 	① 入口类:org.apache.shardingsphere.spring.boot.ShardingSphereAutoConfiguration#setEnvironment;
    # 	② org.apache.shardingsphere.spring.boot.datasource.DataSourceMapSetter
    #	③ org.apache.shardingsphere.infra.datasource.props.DataSourceProperties
    datasource:
      names: ds0, ds1 # 定义两个数据源名称
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db_account_0?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
        # 这里省略数据库连接池的配置
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/db_account_1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
        # 这里省略数据库连接池的配置

    # 2. 分片规则配置
    # 源码入口:org.apache.shardingsphere.sharding.spring.boot.ShardingRuleSpringBootConfiguration
    rules:
      sharding:
        # 2.1 账户信息表的分片配置
        # 分片规则配置类:org.apache.shardingsphere.sharding.spring.boot.rule.YamlShardingRuleSpringBootConfiguration
        tables:
          accountInfo: # 这里是逻辑表名,Java实体类和Mapper操作的就是这个表名
            # 真实数据节点:ds0.account_info_0, ds0.account_info_1, ds1.account_info_0, ds1.account_info_1
            actual-data-nodes: ds$->{0..1}.account_info_$->{0..1}
            # 分库策略:根据account_id取模分到ds0或ds1
            database-strategy:
              standard: # 用于单分片键的标准分片场景
                sharding-column: account_id  # 分片列名称
                sharding-algorithm-name: db-inline-mod  # 分片算法名称
            # 分表策略:根据account_id取模分到account_0或account_1
            table-strategy:
              standard:
                sharding-column: account_id
                sharding-algorithm-name: table-inline-mod
            # 分布式主键生成策略
            key-generate-strategy:
              column: account_id  # 自增列名称,缺省表示不使用自增主键生成器
              key-generator-name: snowflake  # 分布式序列算法名称

        # 2.2 分片算法定义
        sharding-algorithms:
          db-inline-mod:  # 分片算法名称
            type: INLINE  # 分片算法类型
            props:  # 分片算法属性配置
              algorithm-expression: ds$->{account_id % 2}
          table-inline-mod:
            type: INLINE
            props:
              algorithm-expression: account_$->{account_id % 2}

        # 2.3 主键生成器定义
        key-generators:
          snowflake:  # 分布式序列算法名称
            type: SNOWFLAKE # 分布式序列算法类型,雪花算法
            props:  # 分布式序列算法属性配置
              # 终端编号(微服务多节点部署(集群部署)的时候,设置的值不一样)
              worker-id: 1

    # 开启SQL日志,方便调试
    props:
      sql:
        show: true
  • 说明 :经过上面的yaml配置,Sharding会有一个自动配置类对其进行加载并创建 Sharding数据源。
    • 数据源自动配置 :类 ShardingSphereAutoConfiguration,方法 shardingSphereDataSource() 会创建 ShardingSphereDataSource(Sharding数据源)。而且,在这个配置上有这样一个注解:@AutoConfigureBefore(DataSourceAutoConfiguration.class),这个 "DataSourceAutoConfiguration 是 Spring Boot 框架中的 数据源自动配置类"。
    • 如果我们使用了上面的这种【Spring Boot Starter】的方式,那么,一般情况下就不要再去手动创建DataSource,而是让 ShardingSphere 去自动接管数据源。
  • 数据源路由:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,这个目前不探讨。

3. 数据库表结构

db_account_0db_account_1两个数据库中分别创建以下表:

sql 复制代码
-- 在db_account_0中创建
CREATE TABLE `account_info_0` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `account_id` varchar(32) NOT NULL COMMENT '账户编号',
  `account_name` varchar(50) DEFAULT NULL COMMENT '用户名',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `create_time` varchar(18) NOT NULL '账户创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_account_no` (`account_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 在db_account_0中创建
CREATE TABLE `account_info_1` (
  -- 同 account_info_0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 在account_db_1中创建:account_info_0、account_info_1

4. 核心Java代码

业务代码与操作单表时几乎无异,关键在于实体类ID的生成。

  • 实体类 (AccountInfo.java)

    java 复制代码
    @Data
    @TableName(value = "accountInfo") // 逻辑表名,与配置一致
    public class AccountInfo {
        // Sharding-JDBC的雪花算法会在插入时自动生成并填充全局唯一ID
        private Long accountId;
        private String accountName;
        private String phone;
        // ... 其他字段
    }
  • Mapper接口 (AccountInfoMapper.java)

    java 复制代码
    @Mapper
    public interface AccountInfoMapper extends BaseMapper<Account> {
        // 正常使用MyBatis-Plus或MyBatis的方法
        // 例如:根据account_id查询,Sharding-JDBC会自动路由
        Account selectByAccountId(@Param("accountId") Long accountId);
    }
  • 使用示例

    java 复制代码
    @RestController
    public class AccountInfoController {
    	@Autowired
    	private AccountInfoService accountInfoService; // 这个类省略没写
    	
    	@PostMapping("/createAccount")
    	public Result createAccountInfo(@RequestBody AccountInfo accountInfo) {
    		accountInfoService.createAccountInfo(accountInfo);
    		return Result.success("账户创建成功");
    	}
    	
    	@GetMapping("/account/{accountNo}")
    	public Result getAccountInfo(@PathVariable String accountNo) {
    		AccountInfo accountInfo = accountInfoService.getAccountInfoByNo(accountNo);
    		return Result.success(accountInfo);
    	}
    }

⚠️ 重要提醒

  1. 唯一性保证 :分片后,数据库层唯一索引失效,必须依赖分布式ID(如雪花算法)保证 account_id 全局唯一
  2. SQL限制 :分片后,应避免使用跨多个分片(库/表)的查询 ,如不带分片键(account_id)的条件查询、复杂联表(JOIN)和排序分页,这些操作性能极差,甚至不支持。
  3. 数据迁移:上线前,需要将历史数据平滑迁移到新的分片结构中,通常采用"双写"或通过中间件同步的方式。
  4. 事务问题 :涉及多个分片的更新操作将变成分布式事务,复杂度增加,需要考虑最终一致性方案。

总结:分库分表的本质是"按规则分散数据"。选择 account_id 作为分片键,结合 Sharding-JDBC 的配置,应用层代码可以基本保持不变。但在架构设计上,必须解决全局唯一ID查询路由分布式事务等新问题。


3. 分库分表查询提醒


3.1 SQL查询的正确写法

核心原则:尽量让查询条件包含 分片键account_id

  • ✅ 推荐写法(带分片键)

    sql 复制代码
    SELECT * FROM account WHERE account_id = ?;
    
    -- 使用IN查询(所有 account_id 都在同一分片)
    SELECT * FROM account WHERE account_id IN (1001, 1003, 1005);

    这类查询会被 精确路由 到单个分片,性能最好。

  • ❌ 应避免的写法(无分片键)

    sql 复制代码
    -- 问题1:没有account_id条件,导致全库表扫描
    SELECT * FROM account WHERE account_name = ?;
    
    -- 问题2:ORDER BY + LIMIT跨分片效率极低
    SELECT * FROM account ORDER BY create_time LIMIT 0, 20;

3.2 无分片键查询的解决方案

如果必须按非分片键查询,考虑以下方案:

方案 核心原理 与Sharding-JDBC的关系 优点 缺点
读扩散查询 应用层 绕过 Sharding-JDBC,直连所有分片数据源,并行查询后内存聚合。 完全无关,属于应用层的"蛮力"查询。 实现直接,无需额外中间件。 性能差 (全表扫描),内存压力大分片越多越慢
强制路由 (Hint) 通过HintManager手动指定分片值,强制SQL路由到特定分片。 利用其路由机制 ,但由程序员主动干预 可精确控制查询范围,性能好。 需业务方知道数据位置,侵入性强,不通用。
ES搜索引擎 将查询条件(如手机号)和分片键(account_id同步到ES建立索引 。查询时先查ES得到account_id,再通过Sharding-JDBC精准查询。 协同工作 。ES负责检索 ,Sharding-JDBC负责精准路由和数据获取 性能极高,支持复杂查询,对应用透明。 架构复杂,需维护数据同步,有延迟。

结论

  • ① "读扩散"是性能最差的应急方案。
  • ② "强制路由"适合管理后台等特定场景。
  • ③ 对于面向用户的不确定查询(如按姓名、手机号搜账户),ES搜索引擎是目前最成熟和推荐的生产级方案。下面会提到

3.3 建议

  1. 查询设计 :前期设计时识别高频查询,尽量让80%以上查询都带分片键。对于不带分片键的查询,规划好补偿方案(如二级索引表)。
  2. 事务设计 :评估业务容忍度,资金操作优先强一致性,非核心用最终一致性。可建立事务监控,跟踪跨分片事务成功率。
  3. 回滚准备 :分库分表是单向复杂化,要有回滚预案。可采用双写过渡期,新旧架构同时运行验证。

4. ES搜索引擎实现方案与核心代码

其核心架构是:业务数据(MySQL)分片存储保证写入扩展性,查询索引(ES)集中存储保证查询灵活性
搜索链路(保证查询灵活性) 写入链路(保证分片扩展性) 1. 写入或更新 2. 监听数据变更 3. 同步文档 4. 携带条件查询 5. 返回 account_id 列表 6. 通过 account_id 精准查询 7. 返回完整数据 Elasticsearch 索引 查询请求 应用 响应结果 MySQL 分片库表 业务应用 数据同步 Canal/监听器

以下是基于Spring Boot的实现步骤与核心代码:

4.1 数据同步:将账户数据同步到ES

将MySQL中的账户数据,特别是 查询字段 (如account_name, phone)和 分片键account_id),同步到Elasticsearch中。

  • 方案一:使用Canal监听Binlog(推荐)

    这是异步、解耦的方案。Canal模拟MySQL从库,解析binlog中的变更(增删改),然后发送到MQ或直接调用ES API更新索引。对业务代码无侵入。

  • 方案二:应用层双写

    在业务代码中,插入/更新MySQL后,同步或异步地调用ES API更新索引。实现简单,但强耦合,可能丢数据。

4.2 Elasticsearch索引设计与Java代码

4.2.1 索引Mapping(相当于表结构)

在ES中创建一个索引,例如 account_index。你需要将常用的查询字段设置为可搜索的text类型并配置分词器,同时将account_id存为用于精确查找的keyword类型。

json 复制代码
PUT /account_info_index
{
  "mappings": {
    "properties": {
      "accountId": { "type": "keyword" },   // 用于精确查找和回表
      "accountName": { "type": "text", "analyzer": "ik_max_word" }, // 中文分词
      "phone": { "type": "keyword" }      // 手机号精确匹配
      // ... 其他需要查询的字段
    }
  }
}

4.2.2 Spring Boot项目集成ES

1. 项目依赖 (pom.xml)
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. 配置连接 (application.yaml)
yaml 复制代码
spring:
  elasticsearch:
    uris: http://localhost:9200  # ES集群地址
3. 定义ES文档实体和仓库
java 复制代码
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import lombok.Data;

@Setter
@Getter
@Data
@Document(indexName = "account_info_index") // 对应ES索引名
public class AccountInfoDocument {
    // 由分布式id生成器生成
    private Long accountId;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String accountName;
    
    @Field(type = FieldType.Keyword)
    private String phone;
}

// Spring Data Elasticsearch 仓库接口
public interface AccountInfoEsRepository extends ElasticsearchRepository<AccountDocument, Long> {
    // 方法名解析查询:根据账户名模糊搜索
    List<AccountDocument> findByAccountNameContaining(String accountName);
    // 根据手机号精确查询
    AccountDocument findByPhone(String phone);
}
4. 核心查询服务实现

业务查询将变为 "先查ES,再通过Sharding-JDBC回表查详情" 的两步流程。TODO 这里的代码不完整,先写到这里,后续再补上。

java 复制代码
@Service
@RequiredArgsConstructor
public class AccountInfoSearchService {
    private final AccountInfoEsRepository accountInfoEsRepository; // ES查询
    private final AccountInfoMapper accountInfoMapper; // 原MyBatis Mapper,通过Sharding-JDBC操作分片数据库

    /**
     * 综合搜索:根据名称模糊查找账户
     */
    public List<Account> searchByAccountName(String name) {
        // 1. 查ES:获取匹配的账户ID列表
        List<AccountInfoDocument> esResults = accountInfoEsRepository.findByAccountNameContaining(name);
        if (esResults.isEmpty()) {
            return Collections.emptyList();
        }
        
        // 提取出分片键 accountId
        List<Long> accountIds = esResults.stream()
                .map(AccountInfoDocument::getAccountId)
                .collect(Collectors.toList());
        
        // 2. 回查数据库:利用Sharding-JDBC,通过account_id精准路由到各个分片获取完整数据
        // 这里的 selectByAccountIdIn 是MyBatis Mapper中定义的方法,SQL例如:SELECT * FROM account WHERE account_id IN (?, ?, ...)
        // Sharding-JDBC会根据 IN 条件中的每个account_id值,自动路由到对应的分片执行查询,并合并结果。
        return accountInfoMapper.selectByAccountIdIn(accountIds);
    }

4.3 关键注意事项

  • 数据一致性 :ES与数据库之间存在秒级延迟(最终一致),不适合对实时性要求极高的场景。这类场景可考虑"强制路由"或"读扩散"。
  • 同步可靠性:务必保证Canal或双写逻辑的健壮性,避免ES索引数据长期不一致。
  • ES集群性能:根据数据量设计合理的分片数和副本数,并监控集群健康状态。

总的来说,在分库分表架构下,ES负责"找位置"(提供account_id),Sharding-JDBC负责"取数据",两者分工协作,是解决多维查询的最佳实践。

相关推荐
码界奇点2 小时前
基于SpringBoot与Vue3的多租户中后台管理系统设计与实现
java·spring boot·后端·spring·车载系统·毕业设计·源代码管理
x***B4112 小时前
Spring Boot 实战项目如何写进简历?经验分享
经验分享·spring boot·后端
Code blocks2 小时前
SpringBoot从0-1集成Netty实现自定义协议开发
java·spring boot·后端
Java天梯之路2 小时前
Spring Boot 启动流程源码解析:从 `main()` 到 Web 服务就绪
java·spring boot·面试
WZTTMoon2 小时前
Spring Boot Swagger3 使用指南
java·spring boot·后端·swagger3
Java天梯之路2 小时前
Spring Boot 钩子全集实战(一):构造与配置阶段
java·spring boot·面试
Mr.wangh3 小时前
SpringCloudConfig(配置中心)
大数据·elasticsearch·搜索引擎·springcloud·config
程序员根根3 小时前
SpringBoot Web 入门核心知识点(快速开发案例 + 分层解耦实战)
java·spring boot
小园子的小菜3 小时前
深度解析Elasticsearch网络通信原理:节点协同与链接机制
大数据·elasticsearch·搜索引擎