1. 这不是“配个Dockerfile”就完事的项目:它解决的是高并发场景下数据库的生死线问题
你有没有遇到过这样的情况:SpringBoot服务刚上线时响应飞快,QPS轻松破千;可半年后,单表数据量涨到2000万行,一个简单分页查询从50ms飙到3秒,DBA半夜打电话说主库CPU持续98%,从库延迟拉到15分钟?这不是代码写得烂,是架构没扛住——当业务增长撞上单库单表的物理天花板,分库分表和读写分离就不再是“可选项”,而是“保命项”。而今天这个标题里提到的组合:Docker + SpringBoot + ShardingJDBC + MybatisPlus,恰恰是一套在生产环境被反复验证、兼顾开发效率与运维弹性的轻量级落地方案。它不依赖昂贵的中间件集群,不强制重构所有DAO层,更不用动不动就上K8s Operator——而是用容器化封装复杂性,用ShardingJDBC做智能路由,用MybatisPlus保持CRUD手感,让团队在两周内就能把一个单体数据库平稳切分成多个物理库、多个逻辑表,同时把写操作锁死在主库、读请求自动打到从库。我带过的三个项目都走这条路:某电商订单中心日均写入80万单,某SaaS平台租户数据隔离要求严格,某IoT设备管理后台每秒接收2万条心跳上报。它们没用TiDB也没上OceanBase,就是靠这套组合拳活了下来。如果你正卡在“数据量一上来就崩”的临界点,又不想推倒重来,那这篇内容就是为你写的——它不讲抽象理论,只拆解真实部署中每个环节的取舍、参数背后的血泪教训,以及那些文档里绝不会写的“为什么必须这样配”。
2. 整体设计思路:为什么选这个技术栈?而不是其他方案?
2.1 不是为炫技,而是为“可控性”和“渐进式改造”让路
很多人一提分库分表就想到Mycat或ShardingSphere-Proxy,但实际落地时你会发现:Proxy层虽然对应用透明,却成了新的单点瓶颈和黑盒。我们曾在一个金融类项目里试过Proxy,结果发现SQL解析耗时占到整个请求的40%,慢查询日志根本看不出是应用层还是Proxy层的问题,排查一次线上抖动要拉上DBA、中间件组、应用组三方开会。而ShardingJDBC作为JDBC驱动层的增强,直接嵌在应用进程里,所有SQL路由、改写、归并都在内存完成,零网络开销。更重要的是——它支持分片策略热插拔。比如初期按用户ID取模分4库8表,后期发现热点用户集中,可以无缝切换成按用户ID哈希+时间范围双维度分片,只需改配置、重启服务,不用动一行SQL。这种灵活性,是任何代理层方案都给不了的。
2.2 Docker在这里不是“锦上添花”,而是解决环境一致性这个老大难
想象一下:开发在Mac上用HikariCP连接本地MySQL 8.0,测试在CentOS7跑MariaDB 10.3,预发环境用Docker Compose启了3个MySQL实例模拟主从,生产却是阿里云RDS主从+只读地址。光驱动版本兼容性就够喝一壶——MySQL Connector/J 8.0.28在Mac上正常,到了CentOS7却报Unknown system variable 'query_cache_size'。而Docker的价值在于:把数据库拓扑结构变成代码。我们定义一个docker-compose.yml,里面明确声明:
mysql-master:基于mysql:8.0.33镜像,挂载自定义my.cnf开启binlog、设置server-id=1;mysql-slave1:同样镜像,server-id=2,replicate-do-db=order_db;sharding-proxy(备用):虽然本次不用,但预留端口方便后续灰度;app-server:SpringBoot应用镜像,构建时注入application-sharding.yml。
这样,从开发写代码的第一天起,他面对的就是和生产完全一致的数据库拓扑。CI流水线里跑集成测试,用的也是同一份docker-compose.yml启动的环境。去年我们有个项目因测试环境没配GTID,导致主从切换后数据不一致,回滚花了6小时——这种坑,Docker能帮你提前踩平。
2.3 MybatisPlus不是“为了用而用”,它解决了分库分表中最痛的“分页与聚合”问题
ShardingJDBC官方文档里有一句大实话:“分页查询需谨慎”。为什么?因为LIMIT 10000,20这种偏移分页,在分库分表下会变成:每个分片查10020条,再合并后取20条,性能灾难。而MybatisPlus的Page<T>对象配合IPage接口,天然支持流式分页和优化分页插件。我们在MybatisPlusConfig里注册PaginationInnerInterceptor,它会自动将ORDER BY create_time LIMIT 10000,20重写为WHERE create_time > '2023-01-01' LIMIT 20,前提是你的分页条件里有索引字段。更关键的是,MybatisPlus的LambdaQueryWrapper能保证分片键(如user_id)在WHERE条件里被正确识别,避免全分片广播查询。这点,原生Mybatis的XML写法很难做到——你得手动在每个<if test="userId != null">AND user_id = #{userId}</if>里确保分片键存在,稍有遗漏就是性能雪崩。
3. 核心细节解析:配置文件里的每一行,都是踩过坑才定下来的
3.1 ShardingJDBC分片规则:别迷信“一致性哈希”,先搞懂你的数据分布
我们常看到教程里直接用ModShardingAlgorithm(取模算法),但实际业务中,取模最大的问题是扩容痛苦。比如按user_id % 4分4库,数据均匀;但要扩到8库,一半数据得迁移。而HashShardingAlgorithm(哈希)虽能缓解,但无法解决热点问题。我们最终采用的是复合分片策略:
- 库分片:用
user_id的MD5值前4位转十进制,再对库数取模。例如MD5("100001") = "b9a1...",取"b9a1"转十进制为47521,若4库则47521 % 4 = 1,路由到ds_1。 - 表分片:用
order_id的最后两位(如order_id=202310010001取01)对表数取模。
为什么这么设计?因为user_id是强业务主键,几乎每个查询都带,而order_id是全局唯一但查询频次低。这样,按用户查订单时,库路由精准(只查1个库),表路由也精准(只查1个表);而按订单号查时,虽要查全部库,但只查1个表,IO压力可控。计算过程如下:
// 库分片逻辑(简化版) public class UserIdDatabaseShardingAlgorithm implements StandardShardingAlgorithm<Long> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) { String userIdStr = String.valueOf(shardingValue.getValue()); String md5 = DigestUtils.md5Hex(userIdStr); int hash = Integer.parseInt(md5.substring(0, 4), 16); // 取前4字符转10进制 int dbIndex = hash % availableTargetNames.size(); // 假设availableTargetNames=["ds_0","ds_1","ds_2","ds_3"] return "ds_" + dbIndex; } }提示:MD5前4位足够分散,实测100万用户ID哈希后标准差<5%,比单纯取模更均衡。但千万别用
UUID.randomUUID().toString()做分片键——字符串太长,哈希碰撞概率飙升。
3.2 读写分离配置:不是配个master-slave就完事,得防“从库延迟陷阱”
ShardingJDBC的MasterSlaveDataSource默认策略是“写走主库,读走从库”,但问题来了:如果从库延迟10秒,用户刚下单成功,立刻刷新订单列表却看不到新订单,体验极差。我们的解法是强制读主库的三种场景:
- 事务内所有读操作:Spring的
@Transactional注解标记的方法,无论是否只读,一律走主库。配置里加transactional-read-write-splitting=true; - 特定方法名前缀:如
get*ByUserId、list*Latest这类语义上要求强一致的查询,在HintManager里手动指定master; - 从库延迟阈值监控:通过
SHOW SLAVE STATUS查Seconds_Behind_Master,当>3秒时,自动将该从库权重降为0。这需要自定义MasterSlaveLoadBalanceAlgorithm,代码里定期调用JDBC查从库状态并更新内存权重表。
实际配置片段:
spring: shardingsphere: rules: - !MASTER_SLAVE name: ds_ms_0 master-data-source-name: ds_0_master slave-data-source-names: [ds_0_slave1, ds_0_slave2] load-balancer-name: round_robin # 但实际用自定义算法 transactional-read-write-splitting: true # 关键!3.3 Docker网络与存储:别让容器重启后数据库变“裸奔”
新手常犯的错:docker run -d mysql:8.0,结果容器一重启,所有数据全丢。我们必须做三件事:
- 数据卷持久化:为每个MySQL实例创建独立命名卷,如
docker volume create mysql-master-data,挂载到容器内/var/lib/mysql; - 配置文件外置:
my.cnf不能写在镜像里,用-v /host/path/my.cnf:/etc/mysql/conf.d/my.cnf:ro方式挂载,且my.cnf里必须显式设置:[mysqld] server-id = 1 log-bin = mysql-bin binlog-format = ROW expire_logs_days = 7 max_binlog_size = 100M - 容器间网络隔离:用
docker network create sharding-net创建自定义桥接网络,所有MySQL和App容器都连此网络,并用服务名(如mysql-master)互相访问。这样,App里JDBC URL写jdbc:mysql://mysql-master:3306/order_db,而非172.17.0.2这种易变IP。
注意:MySQL 8.0默认启用
caching_sha2_password认证插件,而旧版JDBC驱动不支持。务必在my.cnf里加default_authentication_plugin=mysql_native_password,否则SpringBoot连不上会报Public Key Retrieval is not allowed。
4. 实操过程:从零开始搭建可运行的分库分表环境
4.1 环境准备:5分钟搞定本地验证环境
我们不需要先搭生产级集群,先用Docker Compose在本机跑通核心链路。创建docker-compose.yml:
version: '3.8' services: mysql-master: image: mysql:8.0.33 container_name: mysql-master restart: always environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: order_db volumes: - ./mysql/master/conf:/etc/mysql/conf.d:ro - ./mysql/master/data:/var/lib/mysql ports: - "3307:3306" networks: - sharding-net mysql-slave1: image: mysql:8.0.33 container_name: mysql-slave1 restart: always environment: MYSQL_ROOT_PASSWORD: root123 volumes: - ./mysql/slave1/conf:/etc/mysql/conf.d:ro - ./mysql/slave1/data:/var/lib/mysql ports: - "3308:3306" depends_on: - mysql-master networks: - sharding-net app-server: build: . container_name: app-server restart: always environment: SPRING_PROFILES_ACTIVE: sharding ports: - "8080:8080" depends_on: - mysql-master - mysql-slave1 networks: - sharding-net networks: sharding-net: driver: bridge关键点:
depends_on只控制启动顺序,不保证MySQL服务就绪。所以App启动类里加@PostConstruct方法,循环检测mysql-master:3306端口是否通,不通则Thread.sleep(1000)重试,最多30次;./mysql/master/conf/my.cnf和./mysql/slave1/conf/my.cnf内容不同:主库开log-bin,从库关log-bin但开relay-log;- App的Dockerfile里,
COPY target/*.jar app.jar后,加RUN chmod +x app.jar,否则容器启动报权限错误。
4.2 SpringBoot核心配置:application-sharding.yml逐行解读
这是整个项目的灵魂,我们拆解最易错的几处:
spring: datasource: dynamic: primary: master # 默认数据源名 strict: false # 防止找不到数据源时报错 datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://mysql-master:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: root123 slave1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://mysql-slave1:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: root123 shardingsphere: props: sql-show: true # 开发期打开,看SQL路由日志 query-with-cipher-column: false rules: - !SHARDING tables: t_order: actual-data-nodes: ds_${0..1}.t_order_${0..3} # 2库×4表=8个物理表 table-strategy: standard: sharding-column: order_id sharding-algorithm-name: t_order_table_inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_database_inline default-database-strategy: none: # 全局默认不分库,仅t_order表分 default-table-strategy: none: sharding-algorithms: t_order_database_inline: type: CLASS_BASED props: strategy: standard algorithmClassName: com.example.sharding.UserIdDatabaseShardingAlgorithm t_order_table_inline: type: INLINE props: algorithm-expression: t_order_${order_id % 4} # 简单取模,表数固定为4 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 123重点说明:
actual-data-nodes: ds_${0..1}.t_order_${0..3}:这是ShardingJDBC的EL表达式,生成8个节点:ds_0.t_order_0,ds_0.t_order_1...ds_1.t_order_3。注意${0..1}是闭区间,不是{0,1};table-strategy和database-strategy必须同时存在,否则分片失效;key-generators里worker-id不能为0,否则Snowflake生成ID全为0。我们用123是因测试环境单机,生产会按机器IP哈希算;sql-show: true在开发期必开,它会在日志里打印Actual SQL: ds_0 ::: SELECT * FROM t_order_2 WHERE user_id = ?,这是验证分片是否生效的唯一证据。
4.3 MybatisPlus集成:让分页和批量插入不翻车
MybatisPlus的IService接口默认方法(如list()、page())能自动适配分片,但有两个深坑:
- 批量插入:
saveBatch(list, 1000)默认会把1000条数据全发到同一个分片,造成单点写入瓶颈。必须用InsertBatchSomeColumn自定义插入器,按user_id分组,每组发往对应分片。代码示例:@Bean public MybatisPlusPropertiesCustomizer mybatisPlusPropertiesCustomizer() { return properties -> { properties.getGlobalConfig().getDbConfig().setInsertStrategy(FieldStrategy.NOT_NULL); }; } // 在Service实现类里 public boolean saveOrderBatch(List<Order> orders) { // 按user_id分组 Map<Long, List<Order>> grouped = orders.stream() .collect(Collectors.groupingBy(Order::getUserId)); for (List<Order> group : grouped.values()) { baseMapper.insertBatch(group); // 此时group内所有order.userId相同,路由到同库同表 } return true; } - 分页查询:
page(new Page<>(1, 10), wrapper)在分库分表下会触发COUNT(*)广播查询,极慢。我们改用IPage<Order> page = baseMapper.selectPage(page, wrapper),并在wrapper里强制加上orderBy("create_time", false),利用索引覆盖避免全表扫描。实测1000万数据,COUNT(*)要8秒,加索引后SELECT只要120ms。
4.4 启动与验证:三步确认分片真正生效
- 启动容器:
docker-compose up -d,等所有容器状态为healthy; - 插入测试数据:用Postman调用
POST http://localhost:8080/order/create,传{"userId":100001,"orderId":"202310010001","amount":99.9}; - 验证分片效果:
- 进入
mysql-master容器:docker exec -it mysql-master mysql -uroot -proot123 order_db; - 查
t_order_1表(因202310010001 % 4 = 1):SELECT * FROM t_order_1;—— 应有数据; - 查
t_order_0表:SELECT * FROM t_order_0;—— 应为空; - 查日志:
docker logs app-server | grep "Actual SQL",应看到ds_0 ::: INSERT INTO t_order_1 (...); - 模拟从库延迟:在
mysql-slave1里执行STOP SLAVE;,然后调用查询接口,观察是否自动切到主库(日志里应出现master ::: SELECT)。
- 进入
实操心得:第一次验证时,90%的人卡在
Actual SQL没打印出来。原因八成是sql-show: true没写在shardingsphere节点下,而是错写在spring根节点;或者application-sharding.yml没被Spring Profiles激活,检查启动日志是否有The following profiles are active: sharding。
5. 常见问题与排查技巧实录:那些让老手也挠头的“幽灵Bug”
5.1 分片键未命中:查询变全库扫描,CPU瞬间100%
现象:日志里大量Broadcast SQL: SELECT * FROM t_order WHERE status = ?,QPS超500时MySQL CPU飙到95%。
排查步骤:
- 检查
wrapper构造:QueryWrapper<Order> w = new QueryWrapper<>(); w.eq("status", "paid");—— 错!没带分片键user_id; - 正确写法:
w.eq("user_id", 100001).eq("status", "paid");或用@ShardingKey注解标记DTO字段; - 更彻底的方案:在
ShardingSphere配置里加default-database-strategy: none,强制所有表必须显式指定分片策略,否则启动报错。
根治技巧:写个单元测试,用ShardingSphereTestUtil模拟SQL解析:
@Test public void testShardingRoute() { String sql = "SELECT * FROM t_order WHERE user_id = 100001 AND status = 'paid'"; Collection<RouteUnit> routeUnits = shardingSphereDataSource.getRouteResult(sql); assertEquals(1, routeUnits.size()); // 必须只路由到1个节点 }5.2 时间类型字段乱码:create_time存进去是2023-10-01 00:00:00,查出来变1970-01-01 08:00:00
原因:MySQL 8.0的TIMESTAMP类型受time_zone影响,而Docker容器默认UTC时区,SpringBoot JDBC URL里serverTimezone=Asia/Shanghai只影响JDBC层,MySQL服务本身仍是UTC。
解决方案:
- 在
my.cnf里加:[mysqld] default-time-zone = '+08:00' - 在SpringBoot的
application-sharding.yml里,JDBC URL补充useTimezone=true:url: jdbc:mysql://mysql-master:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&useTimezone=true - Java实体类用
LocalDateTime而非Date,MybatisPlus自动处理时区转换。
注意:
useTimezone=true必须和serverTimezone配对,否则报错The server time zone value 'UTC' is unrecognized。
5.3 Docker容器内存溢出:app-server频繁OOMKilled
现象:docker ps看到app-server状态为Up 2 minutes (unhealthy),docker logs app-server末尾是Killed process 123 (java) total-vm:4567890kB, anon-rss:1234567kB, file-rss:0kB。
根因:Java进程在容器里无法感知cgroup内存限制,默认按宿主机内存设堆大小。一台32G宿主机,Java-Xmx可能设到16G,但容器只分配2G,必然OOM。
解决:
- 启动命令加JVM参数:
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -jar app.jar; - 或在Dockerfile里:
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" CMD java $JAVA_OPTS -jar app.jar - 验证:容器内执行
java -XX:+PrintFlagsFinal -version | grep MaxRAMPercentage,应显示75.0。
5.4 从库延迟导致数据不一致:用户下单后查不到,客服电话被打爆
现象:mysql-slave1的Seconds_Behind_Master长期>5秒,app-server日志里仍有slave1 ::: SELECT。
排查:
- 检查
MasterSlaveLoadBalanceAlgorithm是否生效:在算法类里加log.info("Current slave weight: {}", weights),确认权重没被设为0; - 检查
transactional-read-write-splitting是否为true,且方法确实被@Transactional包裹; - 最致命的坑:Spring的
@Transactional默认只对public方法生效。如果createOrder()是private或package-private,事务不开启,读操作仍走从库。
终极方案:在application-sharding.yml里加熔断配置:
spring: shardingsphere: props: sql-show: false max-connections-size-per-query: 1 # 单查询最大连接数,防从库打满 rules: - !MASTER_SLAVE name: ds_ms_0 master-data-source-name: ds_0_master slave-data-source-names: [ds_0_slave1] load-balancer-name: round_robin transactional-read-write-splitting: true # 新增熔断 circuit-breaker: enabled: true max-failures: 3 failure-threshold: 0.5 delay: 60000这样,当从库连续3次查询失败(如超时),自动熔断1分钟,所有读请求切主库。
5.5 分布式ID重复:两个用户同时下单,生成了相同的order_id
现象:数据库唯一索引报Duplicate entry '202310010001' for key 'PRIMARY'。
原因:Snowflake算法依赖机器时间,Docker容器重启后系统时间可能回拨,或多个容器worker-id相同。
解决方案:
- 强制
worker-id唯一:在Docker Compose里为每个App容器设不同环境变量:app-server-1: environment: WORKER_ID: 1 app-server-2: environment: WORKER_ID: 2 - Java代码里读取:
@Value("${worker-id:1}") private long workerId; @Bean public KeyGenerator snowflakeKeyGenerator() { SnowflakeKeyGenerateAlgorithm algorithm = new SnowflakeKeyGenerateAlgorithm(); Properties props = new Properties(); props.setProperty("worker-id", String.valueOf(workerId)); algorithm.setProps(props); return algorithm; } - 兜底方案:在MySQL表里加
UNIQUE KEY idx_order_no (order_no),应用层捕获SQLIntegrityConstraintViolationException,重试生成新ID。
实操心得:我们在线上加了监控告警——当
INSERT返回affectedRows=0(即唯一键冲突)时,立即推送企业微信告警。三个月内只触发过2次,都是因运维误操作导致worker-id重复。
6. 性能压测与调优:从500QPS到5000QPS的实测记录
6.1 基准测试:单库单表 vs 分库分表的真实差距
我们用JMeter对同一业务接口(根据user_id查订单列表)做对比:
| 场景 | 数据量 | 并发用户 | 平均响应时间 | 95%响应时间 | 错误率 |
|---|---|---|---|---|---|
| 单库单表(t_order) | 500万行 | 200 | 128ms | 210ms | 0% |
| 分库分表(2库×4表) | 500万行(均分) | 200 | 42ms | 68ms | 0% |
| 分库分表(2库×4表) | 500万行 | 1000 | 185ms | 320ms | 0.3% |
关键发现:分库分表在200并发时优势明显(响应时间降67%),但到1000并发时错误率上升——查日志发现是MySQL连接池耗尽。
6.2 连接池调优:HikariCP的5个致命参数
默认HikariCP配置(maximum-pool-size=10)在分库分表下是灾难。因为每个分片都要独立连接池,2库×4表=8个数据源,总连接数达80,远超MySQL默认max_connections=151。我们最终配置:
spring: datasource: dynamic: hikari: maximum-pool-size: 20 # 每个数据源最多20连接 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 leak-detection-threshold: 60000 # 60秒未关闭连接即告警maximum-pool-size=20:经压测,20是单数据源最优值,再高MySQL线程调度开销剧增;leak-detection-threshold=60000:必须开!我们曾因MybatisPlus的LambdaQueryWrapper未关闭Stream,导致连接泄漏,3小时后连接池耗尽;max-lifetime=1800000(30分钟):防止MySQL的wait_timeout(默认8小时)杀死空闲连接,造成Connection reset异常。
6.3 SQL优化:分库分表后,索引策略必须重写
分库分表后,EXPLAIN结果和单库完全不同。我们发现两个高频问题:
- 跨分片
ORDER BY失效:SELECT * FROM t_order ORDER BY create_time DESC LIMIT 10,ShardingJDBC会查所有分片,再内存排序。解决方案:强制加WHERE user_id IN (100001,100002)限定分片,或改用create_time分片; - 联合索引失效:单库时
INDEX idx_user_status (user_id, status)高效,但分库后user_id是分片键,status是普通字段,查询WHERE status='paid'仍要全分片扫描。对策:建INDEX idx_status (status),虽不能消除广播,但减少IO。
提示:用
shardingsphere-sql-parser工具解析SQL路由路径,比肉眼猜准得多。命令:java -jar sql-parser-cli.jar "SELECT * FROM t_order WHERE user_id=100001",输出[ds_0.t_order_1]即正确。
6.4 容器资源分配:CPU和内存的黄金比例
在4核8G的测试服务器上,我们尝试不同分配:
app-server:--cpus=2 --memory=4g→ QPS 3200,CPU使用率78%;app-server:--cpus=3 --memory=6g→ QPS 3800,但MySQL从库CPU达92%,成为瓶颈;- 最终方案:
app-server:--cpus=2.5 --memory=4.5g,mysql-master:--cpus=1.5 --memory=3g,mysql-slave1:--cpus=1 --memory=2g。
理由:Java应用是CPU密集型,MySQL是IO密集型,资源要按实际负载配。用docker stats实时监控,确保无容器%MEM持续>90%。
7. 生产上线 checklist:一份让运维同事对你刮目相看的清单
7.1 上线前必须验证的10件事
- ✅分片键覆盖率:检查所有查询接口的
WHERE条件,确保95%以上带user_id; - ✅从库延迟监控:Prometheus已接入
mysql_slave_status_seconds_behind_master指标,告警阈值设为10秒; - ✅连接池健康检查:
/actuator/health返回hikari: { "status": "UP", "pool": "HikariPool-1" }; - ✅SQL路由日志:
grep "Actual SQL" logs/app.log | head -20,确认无Broadcast SQL; - ✅分布式ID唯一性:用
SELECT COUNT(*), order_id FROM t_order GROUP BY order_id HAVING COUNT(*) > 1查全库,结果为空; - ✅事务一致性:模拟主库宕机,执行
INSERT后立刻SELECT,验证是否自动切从库且数据可见(需transactional-read-write-splitting=false测试); - ✅Docker重启恢复:
docker restart app-server mysql-master mysql-slave1,5分钟内服务自动恢复; - ✅备份策略:
mysqldump脚本已配置--single-transaction --routines --triggers,每天凌晨2点全量备份; - ✅回滚预案:
application-sharding.yml备份为application-sharding-bak.yml,一键切回单库模式; - ✅压测报告:JMeter报告已邮件发送至CTO、DBA、运维负责人,QPS达标且错误率<0.1%。
7.2 上线后第一周的盯盘重点
- 第1天:紧盯
docker stats,确认无OOMKilled;查app.log,确认Actual SQL路由正常; - 第3天:分析MySQL慢查询日志,
pt-query-digest生成报告,重点关注Rows_examined>1000的SQL; - 第5天:用
shardingsphere-agent采集SQL执行路径,导出route_trace.json,验证分片策略无