分库分表

分库分表

ShardingJDBC 水平分库 + 读写分离 分库分表核心配置

:::hide 核心是通过 sharding-jdbc.yml 配置 数据源、分片规则、表 / 库路由策略,以下是极简关键配置示例(以水平分表为例):

spring:
  shardingsphere:
    # 1. 配置数据源(分库0主从+分库1主从,共4个数据源)
    datasource:
      names: db0-master, db0-slave, db1-master, db1-slave
      # 分库0-主库数据源
      db0-master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://db0-master-IP:3306/db0?useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
      # 分库0-从库数据源
      db0-slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://db0-slave-IP:3307/db0?useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
      # 分库1-主库数据源
      db1-master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://db1-master-IP:3308/db1?useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
      # 分库1-从库数据源
      db1-slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://db1-slave-IP:3309/db1?useSSL=false&serverTimezone=UTC
        username: root
        password: 123456
 
    # 2. 配置读写分离规则(关联主从数据源)
    rules:
      readwrite-splitting:
        data-sources:
          # 分库0的读写分离配置(数据源组名:db0)
          db0:
            type: Static  # 静态配置(固定主从数据源)
            props:
              write-data-source-name: db0-master  # 写数据源(主库)
              read-data-source-names: db0-slave  # 读数据源(从库,多个用逗号分隔)
              load-balancer-name: round_robin  # 读负载均衡算法(轮询)
          # 分库1的读写分离配置(数据源组名:db1)
          db1:
            type: Static
            props:
              write-data-source-name: db1-master
              read-data-source-names: db1-slave
              load-balancer-name: round_robin
        # 读写分离负载均衡算法(轮询/随机)
        load-balancers:
          round_robin:
            type: ROUND_ROBIN  # 轮询算法(按顺序选择从库)
          random:
            type: RANDOM  # 随机算法(可选)
 
      # 3. 配置水平分库规则(关联读写分离的数据源组)
      sharding:
        tables:
          t_order:  # 逻辑表名
            actual-data-nodes: ${shardingsphere.rules.readwrite-splitting.data-sources.*.props.write-data-source-name.split('-')[0]}${0..1}.t_order  # 真实表:db0.t_order、db1.t_order(若分表可加表后缀)
            database-strategy:  # 分库策略
              standard:
                sharding-column: user_id  # 分库字段(按用户ID取模)
                sharding-algorithm-name: db_inline
        # 分库算法
        sharding-algorithms:
          db_inline:
            type: INLINE
            props:
              algorithm-expression: db${user_id % 2}  # 分库逻辑:user_id%2=0→db0,=1→db1
 
    # 4. 全局配置
    props:
      sql-show: true  # 打印真实执行SQL(调试用)
      readwrite-splitting.allow-read-write-same-data-source: false  # 禁止读写同一数据源(避免主从延迟问题)

:::

冷热分离实现方案

核心思路:按时间字段路由,将 “热数据”(近 3 个月)存普通表,“冷数据”(超 3 个月)存低成本存储(如 MySQL 归档表、OSS),通过 ShardingJDBC 路由规则隔离。 表设计:逻辑表 t_order,真实表 t_order_hot(热数据)、t_order_cold(冷数据); 路由规则:以 create_time 为分片字段,配置 RANGE 算法,判断时间落在热 / 冷表区间; 冷数据优化:可定期通过定时任务(如 XXL-Job)将 t_order_hot 中过期数据迁移至 t_order_cold,并对冷表做索引删除、表压缩。

分库分表查询应用层合并效率慢的解决方案

核心是 “减少合并数据量”+“异步并行查询”+“引入中间件加速”,分 3 步优化: 优化分片查询: 避免全表扫描:查询必须携带分片字段(如 user_id),精准路由到目标库表,减少查询节点; 分页优化:使用 LIMIT OFFSET 时,通过分片字段计算偏移量,避免各节点返回大量数据后合并; 应用层优化: 异步并行查询:用线程池(如 Spring Async)并行查询多个分片节点,而非串行,缩短总耗时; 结果缓存:将高频查询结果存入 Redis,有效期内直接返回,避免重复合并; 引入中间件:对复杂聚合查询(如统计、排序),直接用 ES 替代 “分库分表查询 + 应用层合并”,从源头解决效率问题。

ES 解决分库分表查询效率问题的具体方案

核心定位:ES 作为查询层,存储分库分表的 “查询镜像数据”,承接复杂查询;MySQL 作为存储层,保证数据一致性,方案分 4 步:

  1. 架构设计 存储层:MySQL 分库分表,存储完整业务数据(核心交易数据); 查询层:ES 集群,存储 MySQL 数据的 “查询友好型镜像”(仅保留查询所需字段,如订单号、用户 ID、金额、时间); 同步层:负责 MySQL 数据实时同步到 ES(下文第五点详细说明); 应用层:简单查询(如单订单查询)直接查 MySQL,复杂查询(如多条件筛选、排序、统计)查 ES。
  2. ES 索引设计(以订单为例) 索引映射(mapping) PUT /t_order_index { "settings": { "number_of_shards": 3, # 分片数(建议与MySQL分库数匹配) "number_of_replicas": 1 # 副本数(保证高可用) }, "mappings": { "properties": { "order_id": { "type": "keyword" }, # 订单号(不分词,精准查询) "user_id": { "type": "keyword" }, # 用户ID(分片字段关联) "order_amount": { "type": "double" },# 订单金额(聚合用) "create_time": { "type": "date" }, # 创建时间(范围查询用) "order_status": { "type": "integer" }# 订单状态(筛选用) } } }
  3. 查询流程 应用发起复杂查询(如 “查询用户 123 近 6 个月已支付订单,按金额降序”); 直接调用 ES API 查询 t_order_index,利用 ES 分片并行查询和倒排索引,快速返回结果; 若需获取订单详情(如商品列表),再根据 ES 返回的 order_id 精准查询 MySQL 分库分表(单条 / 少量查询效率极高)。

保证 MySQL 与 ES 数据一致性的方案

核心是 “实时同步为主,补偿机制为辅”,推荐 3 种主流方案(按可靠性优先级排序): 方案 1:Binlog + Canal(推荐,实时性高) 原理:Canal 模拟 MySQL 从库,监听 MySQL Binlog 日志; 流程: MySQL 开启 Binlog(row 模式,保证数据细节完整); Canal 读取 Binlog,解析出新增 / 修改 / 删除操作; Canal 将解析后的数据同步到 ES(可通过消息队列如 RocketMQ 缓冲,避免峰值压垮 ES); 一致性保障: 异步确保:通过消息队列的 “ack 机制”,确保消息不丢失; 最终一致:定时任务(每小时)对比 MySQL 与 ES 数据差异,修复不一致数据。 方案 2:业务代码双写(简单,适合中小项目) 流程:应用执行 MySQL 操作后,同步调用 ES API 执行对应操作(如 MySQL 插入订单→ES 插入文档); 一致性保障: 事务控制:用本地事务确保 MySQL 和 ES 操作要么都成功,要么都失败(避免单端成功); 失败重试:ES 操作失败时,存入重试队列,后台线程定时重试。 方案 3:Logstash 同步(适合非核心查询场景) 原理:Logstash 通过 JDBC 连接 MySQL,定期拉取增量数据(按 update_time 筛选),同步到 ES; 特点:配置简单,无需修改业务代码,但实时性差(延迟分钟级),适合对一致性要求不高的统计场景。

ShardingJDBC 与 MyCat 的核心区别是什么?各自适用场景?

区别:ShardingJDBC 是 “轻量级组件”(无独立进程,嵌入应用),MyCat 是 “中间件服务器”(独立部署,应用通过 JDBC 连接); 场景:ShardingJDBC 适合中小项目(部署简单、无额外运维),MyCat 适合大型分布式项目(支持读写分离、分库分表更全面,可独立扩容)。

ShardingJDBC 与 MyCat 的核心区别是什么?各自适用场景?

区别:ShardingJDBC 是 “轻量级组件”(无独立进程,嵌入应用),MyCat 是 “中间件服务器”(独立部署,应用通过 JDBC 连接); 场景:ShardingJDBC 适合中小项目(部署简单、无额外运维),MyCat 适合大型分布式项目(支持读写分离、分库分表更全面,可独立扩容)。

分库分表时,为什么不建议用自增 ID 作为主键?如何解决分布式 ID 问题?

原因:自增 ID 无法跨库表保证唯一性,且无法作为分片字段(无业务含义); 解决方案:① 雪花算法(Snowflake)生成 64 位 ID;② 基于 Redis 自增(INCR 命令);③ ShardingJDBC 内置分布式 ID 生成器(如 SNOWFLAKE 算法)。

ShardingJDBC 实现冷热分离后,如何避免冷数据查询拖慢整体性能?

① 路由隔离:通过分片规则确保热数据查询不涉及冷表,冷数据查询单独路由; ② 冷表优化:对冷表删除无用索引、开启 MySQL 表压缩,或迁移至低成本存储(如 OSS),查询时通过专用数据源连接; ③ 异步查询:冷数据查询用异步线程执行,不阻塞热数据业务流程。

分库分表中,“跨分片分页查询” 效率低的根源是什么?如何优化?

根源:传统分页(LIMIT 10000, 10)会导致所有分片返回前 10010 条数据,应用层合并后再取最后 10 条,数据传输量大; 优化方案:① 基于分片字段分页:用 “分片字段> 上一页最大值” 替代 OFFSET,精准过滤数据;② 借助 ES:跨分片分页查询直接查 ES,避免应用层合并。

为什么说 ES 适合承接分库分表的复杂查询?ES 的查询效率优势体现在哪里?

核心原因:分库分表的复杂查询(多条件、排序、聚合)需跨分片合并,ES 天然支持分布式分片查询,且有倒排索引; 效率优势:① 倒排索引:基于字段关键词快速定位文档,无需全表扫描;② 分片并行查询:ES 索引分片与 MySQL 分库表对应,并行查询后汇总,耗时更短;③ 聚合优化:内置高效聚合算法,支持统计、排序等操作。

Canal 同步 MySQL 数据到 ES 时,如何处理 “Binlog 丢失” 或 “ES 写入失败” 的问题?

① Binlog 丢失:MySQL 开启 Binlog 日志归档,Canal 记录消费位点,若位点丢失,通过归档日志恢复并重放; ② ES 写入失败:引入消息队列(RocketMQ/Kafka),Canal 将数据写入队列,ES 消费队列数据,失败时触发重试(设置重试次数和死信队列); ③ 最终一致性:定时任务对比 MySQL 与 ES 数据(按 update_time 或主键),差异数据批量同步修复。

业务代码双写 MySQL 和 ES 时,如何避免 “MySQL 成功但 ES 失败” 导致的数据不一致?

方案 1:本地事务 + 重试队列:① 用 Spring 事务确保 MySQL 操作成功;② ES 操作失败时,将数据存入本地重试队列;③ 后台线程定时重试,直至 ES 写入成功; 方案 2:分布式事务(适合核心业务):用 Seata 等框架,将 MySQL 和 ES 操作纳入分布式事务,确保要么都成功,要么都回滚。

分库分表 + ES 架构中,如何设计 “读写分离”?确保查询效率的同时不影响写入性能?

① MySQL 读写分离:主库负责写入,从库负责简单查询(如单订单详情),ShardingJDBC 配置读写分离规则; ② ES 读写分离:ES 索引设置 “主分片”(负责写入)和 “副本分片”(负责查询),应用查询时仅访问副本分片,避免写入操作占用查询资源; ③ 写入分流:MySQL 写入后,通过 Canal 异步同步到 ES,不阻塞业务写入流程。

当 ES 集群故障时,如何保证业务查询不中断?有哪些降级方案?

① 降级方案 1:切换查询源:ES 故障时,通过配置中心切换查询到 MySQL 分库分表(仅开放核心简单查询,关闭复杂查询); ② 降级方案 2:缓存兜底:将高频查询结果存入 Redis,ES 故障时直接返回 Redis 缓存数据(设置合理缓存有效期); ③ 高可用保障:ES 集群部署至少 3 个节点,设置副本分片,避免单点故障,减少降级触发概率。

ShardingJDBC 如何同时处理分库和读写分离?路由顺序是什么?

路由顺序:先执行 “分库路由”(根据分片字段确定目标数据源组,如 db0),再执行 “读写分离路由”(写→主库,读→从库); 核心逻辑:分库规则关联的是 “数据源组”(而非单个数据源),每个数据源组包含主从节点,最终由读写分离规则决定具体访问哪个节点。

主从延迟导致读从库获取不到最新数据,如何解决?

① 强制路由主库:对实时性要求高的查询(如刚创建订单后查询详情),用 ShardingJDBC Hint 强制路由主库: java 运行 // 代码中添加Hint注解(或编程式Hint) HintManager.getInstance().setWriteRouteOnly(true); // 强制走写库(主库) ② 优化主从同步:MySQL 开启并行复制、减少大事务,降低主从延迟(控制在 100ms 内); ③ 读写分离策略调整:配置 readwrite-splitting.delay-threshold(延迟阈值),超过阈值自动路由主库。

如果某个分库的从库挂了,ShardingJDBC 会如何处理?

ShardingJDBC 会自动检测从库状态(通过心跳检测),若从库不可用,会将其从读数据源列表中剔除; 所有读操作会路由到该分库的其他可用从库(若有多个从库),若仅一个从库挂了,则读操作暂时路由到主库,直到从库恢复。

水平分库 + 读写分离场景下,分布式事务如何保证?

推荐用 Seata 分布式事务(AT 模式),ShardingJDBC 与 Seata 无缝集成; 核心逻辑:Seata 协调各分库的主库执行事务,确保所有分库的主库要么都提交,要么都回滚;从库通过主从同步获取一致数据。

生产环境中,ShardingJDBC 配置有哪些避坑点?

① 数据源名统一规范:主从数据源名建议包含 “master/slave”,方便配置关联; ② 关闭读写同一数据源:避免因主从延迟导致的脏读; ③ 分库字段必带:查询时必须携带分库字段(如 user_id),避免全库扫描(ShardingJDBC 会拒绝无分片字段的全库查询,需手动开启允许); ④ 主从同步模式:Binlog 必须用 ROW 模式,否则可能导致同步数据不一致(尤其是分库分表 + ES 同步场景)。