1. 项目概述与背景
在当今互联网应用中,数据量呈现爆炸式增长。以电商系统为例,一个中等规模的平台每天可能产生数十万甚至上百万的订单记录。传统的单库单表架构很快就会遇到性能瓶颈,查询响应变慢,写入操作排队,严重影响用户体验。这时候,分库分表就成为解决这一问题的关键技术方案。
ShardingSphere作为Apache顶级开源项目,提供了一套完整的数据分片解决方案。它支持JDBC协议,可以无缝集成到现有Java应用中,几乎不需要修改业务代码就能实现分库分表功能。我在最近的一个电商项目中,就成功使用ShardingSphere-JDBC 4.1.1实现了订单模块的分库分表和读写分离,将系统吞吐量提升了近5倍。
2. 环境准备与技术选型
2.1 基础环境配置
这个示例项目基于以下技术栈构建:
- Spring Boot 2.5.10
- MyBatis-Plus 3.4.2
- MySQL 8.0.33
- ShardingSphere-JDBC 4.1.1
- Druid 1.2.8连接池
选择这些版本组合是经过实际生产验证的稳定搭配。特别是MySQL 8.0.33,它提供了更好的JSON支持和性能优化,而ShardingSphere 4.1.1版本在分片算法和分布式事务方面都有显著改进。
2.2 数据库设计
我们设计了两个物理库(ds0, ds1),每个库中包含两张订单表(t_order_0, t_order_1)。这种2库×2表的设计是分库分表的经典配置,既能有效分散数据压力,又不会增加过多运维复杂度。
表结构设计特别注意了以下几点:
- 使用BIGINT类型存储订单ID和用户ID
- 金额字段使用DECIMAL(10,2)确保精度
- 创建时间精确到毫秒(DATETIME(3))
- 为user_id和create_time建立了索引
sql复制CREATE TABLE `t_order_0` (
`order_id` BIGINT NOT NULL COMMENT '订单ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`order_no` VARCHAR(32) NOT NULL COMMENT '订单编号',
`amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`status` TINYINT NOT NULL DEFAULT 0,
`create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`order_id`),
INDEX `idx_user_id` (`user_id`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. ShardingSphere核心配置详解
3.1 数据源配置
在application.yml中,我们配置了三个数据源:一个主库(ds2)和两个从库(ds0, ds1)。这里使用了Druid连接池,这是阿里巴巴开源的优秀连接池实现,特别适合分库分表场景。
yaml复制spring:
shardingsphere:
datasource:
names: ds2, ds0, ds1
ds2:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://10.16.66.88:3306/ds2
username: center
password: 123456
initial-size: 5
max-active: 20
特别注意:生产环境密码应该使用加密配置,可以通过Druid的ConfigFilter实现。
3.2 分库分表规则
我们采用userId作为分库键,orderId作为分表键。这种设计可以确保同一个用户的订单数据尽量落在同一个库中,减少跨库查询。
yaml复制sharding:
default-database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ms_ds
tables:
t_order:
actual-data-nodes: ms_ds.t_order_$->{0..1}
table-strategy:
inline:
sharding-column: order_id
algorithm-expression: t_order_$->{order_id % 2}
key-generator:
column: order_id
type: SNOWFLAKE
3.3 读写分离配置
ShardingSphere的读写分离配置非常简洁。我们定义了一个主从规则ms_ds,指定ds2为主库,ds0和ds1为从库,采用轮询负载均衡策略。
yaml复制master-slave-rules:
ms_ds:
master-data-source-name: ds2
slave-data-source-names: ds0, ds1
load-balance-algorithm-type: round_robin
4. 业务代码实现要点
4.1 实体类设计
Order实体类使用MyBatis-Plus的@TableName注解指定逻辑表名。特别注意我们没有在orderId字段上使用@TableId注解,这是为了让ShardingSphere的分布式主键生成器生效。
java复制@Data
@TableName("t_order")
public class Order {
private Long orderId; // 由ShardingSphere生成
private Long userId; // 分库键
private String orderNo;
private BigDecimal amount;
private Integer status;
private Date createTime;
}
4.2 Mapper层实现
我们结合了MyBatis-Plus的BaseMapper和自定义XML映射文件。BaseMapper提供了基础的CRUD操作,而复杂的分片查询则通过XML实现。
java复制@Mapper
public interface OrderMapper extends BaseMapper<Order> {
@Select("SELECT COUNT(*) FROM t_order WHERE user_id = #{userId}")
int countByUserId(@Param("userId") Long userId);
List<Order> selectByUserIds(@Param("userIds") List<Long> userIds);
}
对应的XML文件中,我们特别注意了批量插入的语法,这是分库分表环境下性能优化的关键。
xml复制<insert id="batchInsert" parameterType="list">
INSERT INTO t_order (order_id, user_id, order_no, amount, status, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.orderId}, #{item.userId}, #{item.orderNo},
#{item.amount}, #{item.status}, #{item.createTime})
</foreach>
</insert>
4.3 服务层实现
服务层实现了几个关键功能:
- 批量插入订单
- 根据用户ID查询
- 复杂条件查询
- 多用户ID查询
java复制@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
implements OrderService {
@Transactional
public boolean insertBatch(List<Order> orders) {
orders.forEach(order -> {
if(order.getOrderId() == null) {
order.setOrderNo(generateOrderNo());
order.setCreateTime(new Date());
}
});
return saveBatch(orders);
}
public List<Order> getOrdersByUserIds(List<Long> userIds) {
return baseMapper.selectByUserIds(userIds);
}
}
5. 实战经验与性能优化
5.1 批量插入优化
在分库分表环境下,批量插入需要特别注意:
- 每批数据量控制在100-500条
- 确保分片键(userId)有值
- 使用ShardingSphere的分布式主键生成器
我们实测发现,批量插入比单条插入性能提升约8-10倍。
5.2 查询优化建议
- 避免全表扫描:确保查询条件包含分片键
- 绑定表:关联查询的表使用相同的分片规则
- 分页优化:先获取ID,再根据ID查询详情
yaml复制sharding:
binding-tables:
- t_order
- t_order_item
5.3 分布式事务处理
ShardingSphere支持多种分布式事务方案:
- XA事务
- Seata AT模式
- 本地事务+最终一致性
对于订单系统,我们推荐使用Seata AT模式,它在性能和一致性之间取得了很好的平衡。
6. 常见问题排查
6.1 分片键为NULL
错误现象:执行SQL时报错"Sharding value cannot be null"
解决方案:
- 检查实体类分片键字段
- 确保插入数据时分片键有值
- 配置默认分片策略
6.2 跨库查询性能差
问题表现:多用户订单查询响应慢
优化方案:
- 使用ShardingSphere的绑定表功能
- 考虑使用广播表存储公共数据
- 对结果集进行应用层合并
6.3 主从同步延迟
现象:刚写入的数据查不到
解决方案:
- 对实时性要求高的查询强制走主库
- 配置适当的主从同步参数
- 使用ShardingSphere的hint强制路由
java复制HintManager.getInstance().setMasterRouteOnly();
7. 监控与运维建议
7.1 日志配置
建议开启ShardingSphere的SQL日志,但生产环境要注意日志量。
yaml复制logging:
level:
org.apache.shardingsphere: info
7.2 监控指标
关键监控指标包括:
- 分片SQL执行时间
- 连接池使用情况
- 主从延迟时间
- 分布式事务成功率
7.3 扩容规划
当现有分片不够时,需要考虑扩容:
- 预先规划好分片数量
- 使用一致性哈希算法减少数据迁移
- 考虑使用ShardingSphere的弹性伸缩功能
8. 项目总结
通过这个项目,我们成功实现了:
- 基于用户ID的水平分库
- 基于订单ID的水平分表
- 读写分离架构
- 分布式主键生成
实际压测结果显示,在相同硬件条件下:
- 写入TPS从1200提升到5800
- 查询平均响应时间从230ms降低到65ms
- 系统整体吞吐量提升约4.8倍
这个方案特别适合订单、交易类系统,但也需要注意:
- 分布式事务的处理复杂度
- 跨分片查询的性能问题
- 数据迁移和扩容的挑战
对于Java开发者来说,ShardingSphere最大的优势是几乎可以像使用普通JDBC一样开发分库分表应用,学习曲线平缓,社区活跃,是企业级分库分表方案的首选。