1. 项目背景与需求分析
在新能源车快速普及的当下,家用智能充电桩管理系统面临着前所未有的数据压力。我们团队最近接手了一个覆盖10万+充电桩的后台系统改造项目,原单库架构在高峰期经常出现查询超时和写入阻塞。经过压力测试发现,当并发用户超过5000时,响应时间从平均200ms飙升到5s以上,核心交易表的数据量在6个月内突破了3亿条。
这个SpringBoot微服务架构的系统需要处理三类典型负载:
- 高频写入:充电启停事件(峰值5000+ TPS)
- 实时查询:用户余额/状态(99%请求需<100ms响应)
- 统计分析:区域用电量报表(每日千万级聚合计算)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分库分表方案设计
2.1 分片策略选型对比
我们测试了三种主流分片方案:
| 策略类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 哈希取模 | 分布均匀 | 扩容困难 | 无明显热点的数据 |
| 范围分片 | 易于扩容 | 可能产生热点 | 有时间序列特征的数据 |
| 一致性哈希 | 扩容影响小 | 实现复杂 | 需要动态扩容的场景 |
最终选择按用户ID哈希分库+按时间范围分表的混合策略:
- 用户维度分库:16个物理库(user_id % 16)
- 时间维度分表:按月分表(charge_record_202307)
关键决策点:充电记录具有强用户关联性,同时有明显的时间冷热特征(最近3个月数据访问量占90%)
2.2 分片键设计陷阱
初期我们犯了个典型错误——使用充电桩设备ID作为分片键,导致两个严重问题:
- 家庭用户的多桩数据分散在不同库
- 企业用户的数十个桩全部分到同一库
修正后的分片键组合:
sql复制-- 分库键:用户ID哈希
sharding_key = CRC32(user_id) & 0x0F
-- 分表键:事件时间(精确到月)
table_suffix = DATE_FORMAT(event_time, '%Y%m')
3. SpringBoot集成ShardingSphere实战
3.1 依赖配置关键点
在pom.xml中需要特别注意版本兼容性:
xml复制<!-- 必须保持版本一致 -->
<sharding-sphere.version>5.3.2</sharding-sphere.version>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
3.2 分片规则配置示例
这是我们的实际生产配置(application-sharding.yml):
yaml复制spring:
shardingsphere:
datasource:
names: ds0,ds1,...,ds15
# 每个数据源配置略...
sharding:
tables:
charge_record:
actual-data-nodes: ds$->{0..15}.charge_record_$->{202301..202312}
database-strategy:
standard:
sharding-column: user_id
precise-algorithm-class-name: com.xxx.config.UserIdHashAlgorithm
table-strategy:
standard:
sharding-column: event_time
precise-algorithm-class-name: com.xxx.config.MonthRangeAlgorithm
3.3 自定义分片算法实现
用户ID哈希分库算法核心代码:
java复制public class UserIdHashAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
int hash = Math.abs(shardingValue.getValue().hashCode());
int dbIndex = hash % availableTargetNames.size();
return "ds" + dbIndex;
}
}
4. 数据迁移与脚本实践
4.1 在线双写迁移方案
我们采用灰度迁移策略确保零停机:
- 新老库双写(通过Spring AOP拦截)
- 增量数据对比(使用Alibaba DataX)
- 历史数据分批迁移(pt-archiver工具)
关键迁移脚本片段:
bash复制#!/bin/bash
# 分批次迁移脚本示例
for i in {0..15}; do
pt-archiver \
--source h=old_host,D=old_db,t=charge_record \
--dest h=new_host,D=new_db_$i,t=charge_record \
--where "user_id%16=$i" \
--limit 1000 \
--commit-each
done
4.2 分表维护自动化
创建动态表维护Job:
java复制@Scheduled(cron = "0 0 1 1 * ?") // 每月1号执行
public void autoCreateNextMonthTable() {
String nextMonth = LocalDate.now().plusMonths(1)
.format(DateTimeFormatter.ofPattern("yyyyMM"));
jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS charge_record_"
+ nextMonth + " LIKE charge_record_template");
}
5. 性能优化关键指标
分库分表前后的性能对比:
| 指标 | 分片前 | 分片后 | 提升幅度 |
|---|---|---|---|
| 写入TPS | 1200 | 8500 | 7.1倍 |
| 查询P99 | 780ms | 65ms | 12倍 |
| 锁等待时间 | 4.2s | 0.3s | 14倍 |
特别提醒三个性能陷阱:
- 避免跨分片JOIN(改为应用层聚合)
- 分布式事务控制在5个分片内
- 分页查询必须带分片键
6. 监控与异常处理
6.1 关键监控项配置
我们在Grafana中配置了这些核心看板:
- 分片倾斜率监控(标准差>15%触发告警)
- 慢查询分片TOP10
- 分布式事务成功率
PromQL示例:
promql复制# 计算分片数据量差异率
stddev(shardingsphere_sharding_table_rows)
/ avg(shardingsphere_sharding_table_rows) > 0.15
6.2 典型故障处理案例
曾遇到过分片键更新导致的"数据幽灵"问题:
- 现象:用户修改手机号后,历史订单消失
- 根因:手机号作为分片键被修改
- 解决方案:
- 业务上禁止更新分片键
- 必须更新时采用"先删后插"模式
7. 扩展思考与优化方向
在实际运行三个月后,我们又发现了新的优化点:
- 冷热数据分离:将6个月前的数据自动归档到ClickHouse
- 弹性分片:基于K8s的动态数据源扩容
- 本地缓存优化:使用Caffeine做分片级缓存
这个充电桩项目让我深刻体会到:分库分表不是简单的技术选型,而是需要持续优化的系统工程。特别是在处理用户地理位置分片时,我们最终放弃了纯粹的地理哈希算法,改为"地域+用户ID"的复合分片策略,这使得区域运营报表的生成效率提升了8倍。
