1. 引言
在互联网业务高速发展的今天,单库单表往往成为系统性能的瓶颈。当数据量达到千万级甚至亿级时,数据库的读写性能会急剧下降,索引膨胀、锁竞争、连接数耗尽等问题接踵而至。此时,分库分表便成为Java后端架构中不可或缺的优化手段。
本文将从实际业务场景出发,系统讲解分库分表的核心概念、主流中间件Apache ShardingSphere的实战用法、分布式ID的生成方案,以及分库分表后必然面临的跨库查询难题与应对策略。文章配有大量可运行的代码示例,帮助读者从理论走向落地。
2. 为什么需要分库分表
2.1 单库单表的瓶颈
在业务初期,一个数据库实例、一张大表往往能支撑起整个系统。但随着用户量和数据量的增长,会出现以下问题:
- 存储瓶颈:单表数据量过大,B+树索引层级加深,查询IO次数增多。
- 写入瓶颈:单库写入并发有限,主从延迟放大。
- 连接瓶颈:数据库连接数有限,高并发下连接池被占满。
- 运维瓶颈:大表DDL(如加索引、加字段)耗时极长,甚至锁表。
2.2 分库分表的两种维度
| 维度 | 说明 | 典型场景 |
|---|---|---|
| 垂直拆分 | 按业务模块拆库/拆表,如订单库、用户库、商品库 | 微服务化、模块解耦 |
| 水平拆分 | 按某个字段(分片键)将数据分散到多个库/表,如按用户ID取模 | 单表数据量巨大、写入并发高 |
实际项目中,通常是先垂直拆分,再对核心大表做水平拆分。
3. 分库分表核心概念
在动手实践前,需要先理解几个关键术语:
- 逻辑表:对用户而言,操作的是逻辑表名(如
t_order),实际数据分散在多个物理表中。 - 物理表:真实存储数据的表,如
t_order_0、t_order_1。 - 分片键(Sharding Key):用于计算数据归属的字段,如
order_id、user_id。 - 分片算法:决定数据如何分布,常见有取模、哈希、范围、时间等。
- 数据节点:一个物理表实例,如
ds0.t_order_0。
4. ShardingSphere 实战
4.1 ShardingSphere 简介
Apache ShardingSphere 是一套开源的分布式数据库中间件解决方案,由三个产品组成:
- ShardingSphere-JDBC:轻量级Java框架,以jar包形式提供服务,适合单体或微服务应用。
- ShardingSphere-Proxy:透明数据库代理,以独立服务形式部署,对应用无侵入。
- ShardingSphere-Sidecar:云原生环境下的代理(目前演进中)。
本文重点讲解最常用的ShardingSphere-JDBC。
4.2 环境准备
<!-- pom.xml 引入依赖 --><dependency><groupId>org.apache.shardingsphere</groupId><artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId><version>5.4.1</version></dependency>4.3 配置文件(application.yml)
spring:shardingsphere:datasource:names:ds0,ds1ds0:type:com.zaxxer.hikari.HikariDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverjdbc-url:jdbc:mysql://localhost:3306/order_db_0?useSSL=falseusername:rootpassword:rootds1:type:com.zaxxer.hikari.HikariDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverjdbc-url:jdbc:mysql://localhost:3306/order_db_1?useSSL=falseusername:rootpassword:rootrules:sharding:tables:t_order:actual-data-nodes:ds$->{0..1}.t_order_$->{0..1}table-strategy:standard:sharding-column:order_idsharding-algorithm-name:order_inlinekey-generate-strategy:column:order_idkey-generator-name:snowflakesharding-algorithms:order_inline:type:INLINEprops:algorithm-expression:t_order_$->{order_id % 2}key-generators:snowflake:type:SNOWFLAKEprops:sql-show:true4.4 实体与Mapper
@Data@TableName("t_order")publicclassOrder{privateLongorderId;privateLonguserId;privateBigDecimalamount;privateLocalDateTimecreateTime;}@MapperpublicinterfaceOrderMapperextendsBaseMapper<Order>{// 使用 MyBatis-Plus,无需额外SQL}4.5 写入与查询测试
@ServicepublicclassOrderService{@ResourceprivateOrderMapperorderMapper;publicvoidinsertOrder(){for(inti=0;i<10;i++){Orderorder=newOrder();order.setUserId(1000L+i);order.setAmount(newBigDecimal("99.90"));order.setCreateTime(LocalDateTime.now());orderMapper.insert(order);// order_id 由雪花算法自动生成}}publicOrderqueryOrder(LongorderId){// 根据分片键查询,ShardingSphere 自动路由到正确的物理表returnorderMapper.selectById(orderId);}}注意:查询条件必须包含分片键,否则会触发全库全表路由(广播查询),性能较差。
5. 分布式ID 生成方案
分库分表后,数据库自增主键无法保证全局唯一,因此需要分布式ID。常见方案如下:
5.1 方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 实现简单、无中心化 | 无序、过长,影响索引性能 |
| 数据库号段 | 有序、性能较好 | 依赖数据库,需维护号段表 |
| Redis INCR | 性能高 | 依赖Redis,需考虑持久化 |
| 雪花算法(Snowflake) | 趋势递增、高性能、无中心化 | 依赖机器时钟,时钟回拨会出问题 |
5.2 雪花算法原理
雪花算法生成的ID为64位Long型,结构如下:
| 1bit 符号位 | 41bit 时间戳 | 10bit 机器ID | 12bit 序列号 |- 41bit时间戳:可表示约69年。
- 10bit机器ID:支持1024台机器。
- 12bit序列号:同一毫秒内可生成4096个ID。
5.3 自定义雪花算法实现
publicclassSnowflakeIdGenerator{privatefinallongworkerId;privatefinallongdatacenterId;privatelongsequence=0L;privatelonglastTimestamp=-1L;privatestaticfinallongTWEPOCH=1288834974657L;privatestaticfinallongWORKER_ID_BITS=5L;privatestaticfinallongDATACENTER_ID_BITS=5L;privatestaticfinallongSEQUENCE_BITS=12L;publicSnowflakeIdGenerator(longworkerId,longdatacenterId){this.workerId=workerId;this.datacenterId=datacenterId;}publicsynchronizedlongnextId(){longtimestamp=System.currentTimeMillis();if(timestamp<lastTimestamp){thrownewRuntimeException("时钟回拨异常");}if(timestamp==lastTimestamp){sequence=(sequence+1)&4095;if(sequence==0){timestamp=tilNextMillis(lastTimestamp);}}else{sequence=0L;}lastTimestamp=timestamp;return((timestamp-TWEPOCH)<<22)|(datacenterId<<17)|(workerId<<12)|sequence;}privatelongtilNextMillis(longlastTimestamp){longtimestamp=System.currentTimeMillis();while(timestamp<=lastTimestamp){timestamp=System.currentTimeMillis();}returntimestamp;}}生产环境建议直接使用 ShardingSphere 内置的雪花算法,或引入成熟的
hutool、mybatis-plus内置ID生成器。
6. 跨库查询难题与应对
分库分表后,原本简单的单表查询变得复杂,主要面临以下问题:
6.1 常见问题
- 跨库JOIN:数据分散在不同库,无法直接JOIN。
- 分页排序:全局分页需要先在各分片排序,再归并。
- 聚合函数:COUNT、SUM等需要各分片计算后汇总。
- 分布式事务:跨库写入需要分布式事务保证一致性。
6.2 应对策略
| 问题 | 解决方案 |
|---|---|
| 跨库JOIN | 冗余字段、应用层组装、宽表设计 |
| 全局分页 | 使用ShardingSphere的归并功能,或禁止深分页 |
| 聚合统计 | 使用ShardingSphere的分布式聚合,或离线数仓 |
| 分布式事务 | Seata AT模式、TCC、本地消息表 |
6.3 ShardingSphere 归并示例
// 分页查询,ShardingSphere 会自动归并各分片结果Page<Order>page=newPage<>(1,10);LambdaQueryWrapper<Order>wrapper=newLambdaQueryWrapper<>();wrapper.orderByDesc(Order::getCreateTime);orderMapper.selectPage(page,wrapper);注意:深分页(如第10000页)在分库分表场景下性能极差,建议通过「游标分页」或「禁止跳页」来规避。
6.4 分布式事务(Seata 简介)
@GlobalTransactionalpublicvoidcreateOrderWithDeductStock(){// 1. 插入订单(订单库)orderMapper.insert(order);// 2. 扣减库存(库存库)stockMapper.deduct(stockId,count);// 3. 任一失败,全局回滚}7. 分库分表最佳实践
- 分片键选择:尽量选择查询频率高、分布均匀的字段(如
user_id、order_id)。 - 避免跨分片查询:业务设计上尽量让查询带上分片键。
- 容量规划:提前评估数据增长,合理设置分片数量,避免后期扩容。
- 读写分离结合:分库分表通常与读写分离搭配使用。
- 监控与治理:通过ShardingSphere的SQL日志、监控面板观察路由与性能。
8. 总结
分库分表是解决海量数据存储与高并发写入的关键技术。本文从瓶颈分析出发,介绍了垂直与水平拆分,重点演示了ShardingSphere-JDBC的配置与使用,并详细讲解了分布式ID的雪花算法实现,最后分析了跨库查询的挑战与应对方案。
在实际项目中,分库分表并非银弹,需要结合业务特点、数据规模、团队维护成本综合权衡。建议读者在理解原理的基础上,通过本地搭建环境动手实践,才能真正掌握这门核心技能。
9. 参考与延伸阅读
- Apache ShardingSphere 官方文档:https://shardingsphere.apache.org/
- Seata 分布式事务框架:https://seata.io/
- MyBatis-Plus 官方文档:https://baomidou.com/