当前位置: 首页 > ai >正文

万字长文详解 MyCat 分表分库:从 0 到 1 构建高可用订单系统

引言:

  • 本文总字数:约 10500 字
  • 建议阅读时间:45 分钟

当订单表突破千万级,数据库的 "生死抉择"

凌晨三点,电商平台的数据库监控告警突然响起。你登录系统发现,订单查询接口响应时间从正常的 50ms 飙升到了 5 秒以上,数据库连接池频繁耗尽,后台管理系统几乎无法操作。

查看数据库指标,订单表数据量已经达到 1.2 亿行,单个表文件大小超过 100GB。每次简单的分页查询都需要扫描数十万行数据,索引维护成本越来越高,数据库服务器的 CPU 利用率长期维持在 95% 以上。

这不是虚构的场景,而是每个快速成长的电商平台都会面临的 "数据库瓶颈"。根据 MySQL 官方性能测试报告(https://dev.mysql.com/doc/refman/8.0/en/performance-schema.html),当单表数据量超过 1000 万行时,查询性能会出现显著下降。

分表分库技术是解决这一问题的核心方案,而 MyCat 作为国内最流行的分布式数据库中间件,凭借其简单易用、功能全面的特点,已成为众多企业的首选。本文将带你全面掌握 MyCat 在订单系统中的分表分库实战,从理论到实践,包含完整可运行代码和最佳实践。

一、MyCat 核心原理:分布式数据库的 "智能路由器"

1.1 什么是 MyCat?

MyCat 是一款基于 Java 开发的分布式数据库中间件,它模拟 MySQL 协议,实现了数据库的分表分库、读写分离、高可用等功能。应用程序无需修改代码,只需将连接指向 MyCat,即可实现对后端多个数据库的透明访问。

MyCat 的核心价值在于:

  • 对应用透明:应用程序几乎无需修改
  • 功能全面:支持分表分库、读写分离、数据迁移等
  • 性能优异:经过大量企业验证的高性能中间件
  • 社区活跃:国内使用最广泛的分布式数据库中间件之一

1.2 MyCat 核心概念

在使用 MyCat 之前,我们需要理解几个核心概念:

  • 逻辑库(schema):MyCat 中定义的数据库,对应应用程序看到的数据库
  • 逻辑表(table):MyCat 中定义的表,对应应用程序看到的表
  • 数据节点(dataNode):实际存储数据的数据库和表,如db1.order_0
  • 分片键(shardingKey):用于分片的字段,如user_id
  • 分片规则(rule):数据如何分配到不同节点的规则
  • 全局序列(global sequence):生成全局唯一 ID 的机制

阿里巴巴《Java 开发手册(嵩山版)》明确指出:"分库分表时,表名的命名最好能体现分片规则,如 t_order_00 到 t_order_15,便于阅读和理解"。

1.3 MyCat 与其他中间件的对比

目前主流的分表分库中间件有 MyCat、ShardingSphere、TDDL 等,它们的对比:

特性MyCatShardingSphere-JDBCShardingSphere-Proxy
部署方式独立服务嵌入应用独立服务
协议支持MySQL多数据库多数据库
学习成本中等较高中等
性能良好优秀良好
适用场景中小团队、快速部署开发能力强的团队多语言支持场景

MyCat 的优势在于部署简单、运维方便,对应用无侵入,特别适合中小团队快速实现分表分库。

二、订单系统分表分库设计:从业务到技术的映射

2.1 订单表结构设计

一个典型的电商订单表应包含以下字段:

CREATE TABLE `order_tbl` (`id` bigint NOT NULL COMMENT '订单ID',`order_no` varchar(64) NOT NULL COMMENT '订单编号',`user_id` bigint NOT NULL COMMENT '用户ID',`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',`pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额',`freight` decimal(10,2) NOT NULL COMMENT '运费',`order_status` tinyint NOT NULL COMMENT '订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消',`payment_time` datetime DEFAULT NULL COMMENT '支付时间',`delivery_time` datetime DEFAULT NULL COMMENT '发货时间',`receive_time` datetime DEFAULT NULL COMMENT '确认收货时间',`comment_time` datetime DEFAULT NULL COMMENT '评价时间',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_user_id` (`user_id`),KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

2.2 分片策略设计

订单表的分片策略需要结合业务查询模式来设计。常见的查询场景:

  1. 根据用户 ID 查询订单列表
  2. 根据订单号查询订单详情
  3. 根据时间范围查询订单

基于这些场景,我们设计复合分片策略

  • 分库策略:按用户 ID 范围分片,共 4 个库
  • 分表策略:在每个库内按订单创建时间(月份)分片

这种策略的优势:

  • 满足按用户 ID 查询的高效性,避免跨库查询
  • 符合订单的时间特性,便于历史数据归档
  • 数据分布均匀,避免热点问题

2.3 分片粒度确定

单表数据量的合理阈值是分表分库设计的关键。根据测试和经验:

  • 对于订单表这种读写频繁、索引较多的表,单表数据量建议控制在 500 万以内
  • 若查询简单且索引优化良好,可放宽到 1000 万

假设平台日均订单 10 万,每月约 300 万订单,按 4 个库分片,每个库每月约 75 万订单,正好在合理阈值内。

2.4 全局 ID 生成策略

分表分库后,传统的自增 ID 无法保证全局唯一,需要全局 ID 生成策略。MyCat 支持多种全局 ID 生成方式:

  1. 本地文件方式:基于本地文件的自增 ID,性能高但不适合分布式部署
  2. 数据库方式:基于数据库的自增 ID,可靠性高但性能一般
  3. 时间戳方式:包含时间戳的 ID,有序且包含时间信息
  4. 分布式 ID 生成器:如雪花算法(Snowflake)

对于订单 ID,推荐使用雪花算法,它生成的 64 位 ID 包含时间戳、机器 ID 等信息,既保证唯一性,又便于定位数据所在分片。

三、MyCat 环境搭建:从安装到配置

3.1 环境准备

3.1.1 软件版本选择
  • JDK:17.0.9
  • MyCat:1.6.7.6(最新稳定版)
  • MySQL:8.0.34
  • Spring Boot:3.2.0
  • MyBatis-Plus:3.5.5
  • Lombok:1.18.30
  • Swagger3:2.1.0
  • commons-lang3:3.14.0
3.1.2 MySQL 数据库初始化

首先创建分库分表所需的数据库和表结构:

-- 创建4个订单库
CREATE DATABASE IF NOT EXISTS order_db_0 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_1 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_2 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS order_db_3 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 创建存储过程批量创建表
DELIMITER $$
CREATE PROCEDURE create_order_tables(IN db_suffix INT)
BEGINDECLARE month_str VARCHAR(6);SET month_str = '202310';CALL create_order_table_by_month(db_suffix, month_str);SET month_str = '202311';CALL create_order_table_by_month(db_suffix, month_str);SET month_str = '202312';CALL create_order_table_by_month(db_suffix, month_str);
END$$CREATE PROCEDURE create_order_table_by_month(IN db_suffix INT, IN month_str VARCHAR(6))
BEGINSET @db_name = CONCAT('order_db_', db_suffix);SET @table_name = CONCAT('order_tbl_', month_str);SET @sql = CONCAT('CREATE TABLE IF NOT EXISTS ', @db_name, '.', @table_name, ' (id bigint NOT NULL COMMENT \'订单ID\',order_no varchar(64) NOT NULL COMMENT \'订单编号\',user_id bigint NOT NULL COMMENT \'用户ID\',total_amount decimal(10,2) NOT NULL COMMENT \'订单总金额\',pay_amount decimal(10,2) NOT NULL COMMENT \'实付金额\',freight decimal(10,2) NOT NULL COMMENT \'运费\',order_status tinyint NOT NULL COMMENT \'订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消\',payment_time datetime DEFAULT NULL COMMENT \'支付时间\',delivery_time datetime DEFAULT NULL COMMENT \'发货时间\',receive_time datetime DEFAULT NULL COMMENT \'确认收货时间\',comment_time datetime DEFAULT NULL COMMENT \'评价时间\',create_time datetime NOT NULL COMMENT \'创建时间\',update_time datetime NOT NULL COMMENT \'更新时间\',PRIMARY KEY (id),KEY idx_user_id (user_id),KEY idx_create_time (create_time)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=\'订单表\'');PREPARE stmt FROM @sql;EXECUTE stmt;DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;-- 为每个库创建表
CALL create_order_tables(0);
CALL create_order_tables(1);
CALL create_order_tables(2);
CALL create_order_tables(3);-- 创建全局序列表(用于MyCat生成全局ID)
CREATE DATABASE IF NOT EXISTS mycat_seq_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mycat_seq_db;
CREATE TABLE IF NOT EXISTS MYCAT_SEQUENCE (name VARCHAR(50) NOT NULL COMMENT '序列名称',current_value BIGINT NOT NULL COMMENT '当前值',increment INT NOT NULL DEFAULT 100 COMMENT '步长',PRIMARY KEY (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MyCat全局序列表';-- 初始化订单ID序列
INSERT INTO MYCAT_SEQUENCE (name, current_value, increment) 
VALUES ('ORDER_ID', 1000000, 100) ON DUPLICATE KEY UPDATE current_value = 1000000;

3.2 MyCat 安装与配置

3.2.1 MyCat 安装
  1. 下载 MyCat:从 MyCat 官方网站(| MYCAT官方网站—中国开源分布式数据库中间件)下载 1.6.7.6 版本
  2. 解压安装包:tar -zxvf Mycat-server-1.6.7.6-release-20220524173824-linux.tar.gz -C /opt/
  3. 配置环境变量:
echo 'export MYCAT_HOME=/opt/mycat' >> /etc/profile
echo 'export PATH=$PATH:$MYCAT_HOME/bin' >> /etc/profile
source /etc/profile
3.2.2 MyCat 核心配置

MyCat 的核心配置文件位于/opt/mycat/conf目录下,主要包括:

  1. schema.xml:定义逻辑库、逻辑表、数据节点等
  2. rule.xml:定义分片规则
  3. server.xml:定义用户、权限等

schema.xml 配置:

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/"><!-- 定义逻辑库 --><schema name="ORDER_DB" checkSQLschema="false" sqlMaxLimit="100"><!-- 定义订单逻辑表 --><table name="order_tbl" dataNode="dn$0-3" rule="order_table_rule" primaryKey="id"autoIncrement="true"></table><!-- 订单详情表,与订单表使用相同分片规则 --><table name="order_item_tbl" dataNode="dn$0-3" rule="order_table_rule" primaryKey="id"autoIncrement="true"></table><!-- 字典表,作为全局表在所有节点都存在 --><table name="dict_tbl" dataNode="dn$0-3" type="global" primaryKey="id"></table></schema><!-- 定义数据节点 --><dataNode name="dn0" dataHost="localhost1" database="order_db_0" /><dataNode name="dn1" dataHost="localhost1" database="order_db_1" /><dataNode name="dn2" dataHost="localhost1" database="order_db_2" /><dataNode name="dn3" dataHost="localhost1" database="order_db_3" /><!-- 定义数据主机 --><dataHost name="localhost1" maxCon="1000" minCon="10" balance="1"writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100"><heartbeat>select 1</heartbeat><!-- 写库配置 --><writeHost host="hostM1" url="jdbc:mysql://127.0.0.1:3306?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="root"><!-- 读库配置 --><readHost host="hostS1" url="jdbc:mysql://127.0.0.1:3307?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="root" /></writeHost></dataHost>
</mycat:schema>

rule.xml 配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/"><!-- 订单表分片规则 --><tableRule name="order_table_rule"><rule><!-- 分库字段 --><columns>user_id</columns><!-- 分库规则 --><algorithm>order_db_inline</algorithm></rule><!-- 分表规则 --><childTableRule name="order_item_tbl" joinKey="order_id" parentKey="id"><rule><columns>create_time</columns><algorithm>order_table_inline</algorithm></rule></childTableRule></tableRule><!-- 分库算法:按user_id范围分片 --><function name="order_db_inline" class="io.mycat.route.function.AutoPartitionByLong"><property name="mapFile">autopartition-long.txt</property><property name="defaultNode">0</property></function><!-- 分表算法:按create_time月份分片 --><function name="order_table_inline" class="io.mycat.route.function.PartitionByMonth"><property name="dateFormat">yyyyMM</property><property name="sBeginDate">202310</property><property name="sEndDate">202412</property><property name="sPartionDay">1</property></function>
</mycat:rule>

创建autopartition-long.txt文件(位于 conf 目录):

# user_id范围到数据节点的映射
0-25000000=0
25000001-50000000=1
50000001-75000000=2
75000001-100000000=3

server.xml 配置(主要部分):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/"><!-- 全局序列配置 --><system><property name="sequnceHandlerType">1</property> <!-- 1表示使用数据库方式生成序列 --><property name="defaultSqlParser">druidparser</property><property name="charset">utf8mb4</property></system><!-- 用户配置 --><user name="root" defaultAccount="true"><property name="password">123456</property><property name="schemas">ORDER_DB</property></user><user name="user"><property name="password">user</property><property name="schemas">ORDER_DB</property><property name="readOnly">true</property></user>
</mycat:server>

序列配置(sequence_db_conf.properti

# 序列名称到数据库的映射
ORDER_ID=mycat_seq_db
3.2.3 启动 MyCat
# 启动MyCat
mycat start# 查看状态
mycat status# 查看日志
tail -f /opt/mycat/logs/wrapper.log

MyCat 启动后,默认监听 8066 端口(数据端口)和 9066 端口(管理端口)。

可以通过 MySQL 客户端连接 MyCat 进行测试:

mysql -h 127.0.0.1 -P 8066 -u root -p123456

四、订单系统开发:基于 MyCat 的实战代码

4.1 项目初始化

4.1.1 Maven 依赖配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version><relativePath/></parent><groupId>com.jam.order</groupId><artifactId>mycat-order-demo</artifactId><version>1.0.0</version><properties><java.version>17</java.version><mybatis-plus.version>3.5.5</mybatis-plus.version><lombok.version>1.18.30</lombok.version><commons-lang3.version>3.14.0</commons-lang3.version><springdoc.version>2.1.0</springdoc.version></properties><dependencies><!-- Spring Boot 核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 数据库驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- MyBatis-Plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>${mybatis-plus.version}</version></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><scope>provided</scope></dependency><!-- 工具类 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>${commons-lang3.version}</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-core</artifactId><version>6.1.2</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>${springdoc.version}</version></dependency><!-- 测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build>
</project>
4.1.2 应用配置文件
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:8066/ORDER_DB?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456hikari:maximum-pool-size: 20minimum-idle: 5idle-timeout: 300000connection-timeout: 20000max-lifetime: 1800000mybatis-plus:mapper-locations: classpath*:mapper/**/*.xmlglobal-config:db-config:id-type: INPUT  # 手动输入ID,使用MyCat的全局序列logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.slf4j.Slf4jImplspringdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.htmloperationsSorter: methodpackages-to-scan: com.jam.order.controllerlogging:level:com.jam.order.mapper: debugcom.jam.order.service: info

4.2 核心代码实现

4.2.1 实体类
package com.jam.order.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;import java.math.BigDecimal;
import java.time.LocalDateTime;/*** 订单实体类** @author 果酱*/
@Data
@TableName("order_tbl")
@Schema(description = "订单信息")
public class Order {/*** 订单ID*/@TableId(type = IdType.INPUT)@Schema(description = "订单ID")private Long id;/*** 订单编号*/@Schema(description = "订单编号")private String orderNo;/*** 用户ID*/@Schema(description = "用户ID")private Long userId;/*** 订单总金额*/@Schema(description = "订单总金额")private BigDecimal totalAmount;/*** 实付金额*/@Schema(description = "实付金额")private BigDecimal payAmount;/*** 运费*/@Schema(description = "运费")private BigDecimal freight;/*** 订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消*/@Schema(description = "订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消")private Integer orderStatus;/*** 支付时间*/@Schema(description = "支付时间")private LocalDateTime paymentTime;/*** 发货时间*/@Schema(description = "发货时间")private LocalDateTime deliveryTime;/*** 确认收货时间*/@Schema(description = "确认收货时间")private LocalDateTime receiveTime;/*** 评价时间*/@Schema(description = "评价时间")private LocalDateTime commentTime;/*** 创建时间*/@Schema(description = "创建时间")private LocalDateTime createTime;/*** 更新时间*/@Schema(description = "更新时间")private LocalDateTime updateTime;
}
4.2.2 MyCat 全局 ID 工具类
package com.jam.order.util;import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;import java.util.Objects;/*** MyCat全局ID工具类** @author 果酱*/
@Slf4j
@Component
public class MyCatSequenceUtil {private final JdbcTemplate jdbcTemplate;public MyCatSequenceUtil(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** 获取MyCat全局序列** @param sequenceName 序列名称* @return 全局唯一ID*/public Long getSequenceId(String sequenceName) {Objects.requireNonNull(sequenceName, "序列名称不能为空");try {// 调用MyCat的序列函数String sql = "SELECT next value for MYCATSEQ_" + sequenceName;Long result = jdbcTemplate.queryForObject(sql, Long.class);return Objects.requireNonNullElse(result, 0L);} catch (Exception e) {log.error("获取MyCat全局序列失败,序列名称:{},异常信息:{}", sequenceName, ExceptionUtils.getStackTrace(e));throw new RuntimeException("获取全局ID失败", e);}}/*** 获取订单ID** @return 订单ID*/public Long getOrderId() {return getSequenceId("ORDER_ID");}
}
4.2.3 Mapper 接口
package com.jam.order.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.order.entity.Order;
import org.apache.ibatis.annotations.Param;import java.time.LocalDateTime;
import java.util.List;/*** 订单Mapper接口** @author 果酱*/
public interface OrderMapper extends BaseMapper<Order> {/*** 根据用户ID和时间范围查询订单** @param userId 用户ID* @param startTime 开始时间* @param endTime 结束时间* @return 订单列表*/List<Order> selectByUserIdAndTimeRange(@Param("userId") Long userId,@Param("startTime") LocalDateTime startTime,@Param("endTime") LocalDateTime endTime);/*** 根据订单状态和时间范围统计订单数量** @param orderStatus 订单状态* @param startTime 开始时间* @param endTime 结束时间* @return 订单数量*/Long countByStatusAndTimeRange(@Param("orderStatus") Integer orderStatus,@Param("startTime") LocalDateTime startTime,@Param("endTime") LocalDateTime endTime);
}

对应的 Mapper XML 文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.order.mapper.OrderMapper"><select id="selectByUserIdAndTimeRange" resultType="com.jam.order.entity.Order">SELECT * FROM order_tblWHERE user_id = #{userId}AND create_time BETWEEN #{startTime} AND #{endTime}ORDER BY create_time DESC</select><select id="countByStatusAndTimeRange" resultType="java.lang.Long">SELECT COUNT(*) FROM order_tblWHERE order_status = #{orderStatus}AND create_time BETWEEN #{startTime} AND #{endTime}</select>
</mapper>
4.2.4 Service 层
package com.jam.order.service;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.order.entity.Order;
import com.jam.order.mapper.OrderMapper;
import com.jam.order.util.MyCatSequenceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;/*** 订单服务实现类** @author 果酱*/
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {private final MyCatSequenceUtil sequenceUtil;public OrderServiceImpl(MyCatSequenceUtil sequenceUtil) {this.sequenceUtil = sequenceUtil;}/*** 创建订单** @param order 订单信息* @return 订单ID*/@Override@Transactional(rollbackFor = Exception.class)public Long createOrder(Order order) {// 参数校验Objects.requireNonNull(order, "订单信息不能为空");Objects.requireNonNull(order.getUserId(), "用户ID不能为空");// 获取全局IDLong orderId = sequenceUtil.getOrderId();order.setId(orderId);// 生成订单编号String orderNo = generateOrderNo(order.getUserId());order.setOrderNo(orderNo);// 设置默认值LocalDateTime now = LocalDateTime.now();order.setCreateTime(now);order.setUpdateTime(now);order.setOrderStatus(0); // 默认为待付款状态// 保存订单boolean saveResult = save(order);if (!saveResult) {log.error("创建订单失败,订单信息:{}", order);throw new RuntimeException("创建订单失败");}log.info("创建订单成功,订单ID:{},订单编号:{}", order.getId(), order.getOrderNo());return order.getId();}/*** 生成订单编号* 规则:年月日时分秒 + 用户ID后4位 + 随机数** @param userId 用户ID* @return 订单编号*/private String generateOrderNo(Long userId) {StringBuilder orderNo = new StringBuilder();// 年月日时分秒(14位)orderNo.append(LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));// 用户ID后4位String userIdStr = String.valueOf(userId);if (userIdStr.length() >= 4) {orderNo.append(userIdStr.substring(userIdStr.length() - 4));} else {orderNo.append(StringUtils.leftPad(userIdStr, 4, '0'));}// 3位随机数orderNo.append(StringUtils.leftPad(String.valueOf((int) (Math.random() * 1000)), 3, '0'));return orderNo.toString();}/*** 根据用户ID查询订单列表** @param userId 用户ID* @param startTime 开始时间* @param endTime 结束时间* @return 订单列表*/@Overridepublic List<Order> getOrdersByUserId(Long userId, LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(userId, "用户ID不能为空");Objects.requireNonNull(startTime, "开始时间不能为空");Objects.requireNonNull(endTime, "结束时间不能为空");log.info("查询用户订单,用户ID:{},时间范围:{}至{}", userId, startTime, endTime);List<Order> orders = baseMapper.selectByUserIdAndTimeRange(userId, startTime, endTime);if (CollectionUtils.isEmpty(orders)) {log.info("未查询到用户订单,用户ID:{}", userId);return List.of();}return orders;}/*** 更新订单状态** @param orderId 订单ID* @param orderStatus 订单状态* @return 是否更新成功*/@Override@Transactional(rollbackFor = Exception.class)public boolean updateOrderStatus(Long orderId, Integer orderStatus) {Objects.requireNonNull(orderId, "订单ID不能为空");Objects.requireNonNull(orderStatus, "订单状态不能为空");// 验证订单状态是否合法if (orderStatus < 0 || orderStatus > 4) {log.error("订单状态不合法:{}", orderStatus);throw new IllegalArgumentException("订单状态不合法");}Order order = new Order();order.setId(orderId);order.setOrderStatus(orderStatus);order.setUpdateTime(LocalDateTime.now());// 根据状态更新对应的时间switch (orderStatus) {case 1: // 待发货,说明已支付order.setPaymentTime(LocalDateTime.now());break;case 2: // 已发货order.setDeliveryTime(LocalDateTime.now());break;case 3: // 已完成order.setReceiveTime(LocalDateTime.now());break;case 4: // 已取消break;default:// 无需处理}boolean updateResult = updateById(order);log.info("更新订单状态,订单ID:{},新状态:{},结果:{}", orderId, orderStatus, updateResult);return updateResult;}/*** 统计指定状态的订单数量** @param orderStatus 订单状态* @param startTime 开始时间* @param endTime 结束时间* @return 订单数量*/@Overridepublic Long countOrdersByStatus(Integer orderStatus, LocalDateTime startTime, LocalDateTime endTime) {Objects.requireNonNull(orderStatus, "订单状态不能为空");Objects.requireNonNull(startTime, "开始时间不能为空");Objects.requireNonNull(endTime, "结束时间不能为空");log.info("统计订单数量,状态:{},时间范围:{}至{}", orderStatus, startTime, endTime);return baseMapper.countByStatusAndTimeRange(orderStatus, startTime, endTime);}
}
4.2.5 Controller 层
package com.jam.order.controller;import com.jam.order.entity.Order;
import com.jam.order.service.OrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;/*** 订单控制器** @author 果酱*/
@Slf4j
@RestController
@RequestMapping("/api/v1/orders")
@Tag(name = "订单管理", description = "订单相关的CRUD操作")
public class OrderController {private final OrderService orderService;public OrderController(OrderService orderService) {this.orderService = orderService;}/*** 创建订单** @param order 订单信息* @return 订单ID*/@PostMapping@Operation(summary = "创建订单", description = "创建新的订单")public ResponseEntity<Long> createOrder(@RequestBody Order order) {Long orderId = orderService.createOrder(order);return ResponseEntity.ok(orderId);}/*** 查询订单详情** @param orderId 订单ID* @return 订单详情*/@GetMapping("/{orderId}")@Operation(summary = "查询订单详情", description = "根据订单ID查询订单详情")public ResponseEntity<Order> getOrderDetail(@Parameter(description = "订单ID", required = true)@PathVariable Long orderId) {Order order = orderService.getById(orderId);return ResponseEntity.ok(order);}/*** 根据用户ID查询订单列表** @param userId 用户ID* @param startTime 开始时间* @param endTime 结束时间* @return 订单列表*/@GetMapping("/user/{userId}")@Operation(summary = "查询用户订单", description = "根据用户ID和时间范围查询订单列表")public ResponseEntity<List<Order>> getOrdersByUserId(@Parameter(description = "用户ID", required = true)@PathVariable Long userId,@Parameter(description = "开始时间", required = true)@RequestParam LocalDateTime startTime,@Parameter(description = "结束时间", required = true)@RequestParam LocalDateTime endTime) {List<Order> orders = orderService.getOrdersByUserId(userId, startTime, endTime);return ResponseEntity.ok(orders);}/*** 更新订单状态** @param orderId 订单ID* @param orderStatus 订单状态* @return 是否更新成功*/@PutMapping("/{orderId}/status")@Operation(summary = "更新订单状态", description = "根据订单ID更新订单状态")public ResponseEntity<Boolean> updateOrderStatus(@Parameter(description = "订单ID", required = true)@PathVariable Long orderId,@Parameter(description = "订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消", required = true)@RequestParam Integer orderStatus) {boolean result = orderService.updateOrderStatus(orderId, orderStatus);return ResponseEntity.ok(result);}/*** 统计指定状态的订单数量** @param orderStatus 订单状态* @param startTime 开始时间* @param endTime 结束时间* @return 订单数量*/@GetMapping("/count")@Operation(summary = "统计订单数量", description = "统计指定状态和时间范围内的订单数量")public ResponseEntity<Long> countOrdersByStatus(@Parameter(description = "订单状态:0-待付款,1-待发货,2-已发货,3-已完成,4-已取消", required = true)@RequestParam Integer orderStatus,@Parameter(description = "开始时间", required = true)@RequestParam LocalDateTime startTime,@Parameter(description = "结束时间", required = true)@RequestParam LocalDateTime endTime) {Long count = orderService.countOrdersByStatus(orderStatus, startTime, endTime);return ResponseEntity.ok(count);}
}
4.2.6 启动类
package com.jam.order;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** 订单服务启动类** @author 果酱*/
@SpringBootApplication
@MapperScan("com.jam.order.mapper")
public class OrderApplication {public static void main(String[] args) {SpringApplication.run(OrderApplication.class, args);}
}

4.3 测试验证

4.3.1 功能测试

使用 Swagger UI 进行测试,访问地址:http://localhost:8080/swagger-ui.html

  1. 创建订单测试:
{"userId": 123456,"totalAmount": 999.99,"payAmount": 999.99,"freight": 0.00,"createTime": "2023-10-15T10:30:00"
}
  1. 查询订单测试:
    访问接口/api/v1/orders/user/123456?startTime=2023-10-01T00:00:00&endTime=2023-10-31T23:59:59

  2. 验证数据分布:
    登录对应的数据库查看数据是否正确分片:

# 查看user_id=123456应该被分到哪个库
# 根据autopartition-long.txt配置,0-25000000=0,所以应该在order_db_0
mysql -h 127.0.0.1 -P 3306 -u root -p order_db_0
select * from order_tbl_202310 where user_id=123456;
4.3.2 性能测试

使用 JMeter 进行性能测试,模拟 100 并发用户创建订单,观察系统响应时间和错误率。

预期结果:

  • 平均响应时间 < 500ms
  • 95% 响应时间 < 1000ms
  • 错误率 = 0

五、MyCat 高级特性:提升系统可用性与性能

5.1 读写分离

MyCat 支持读写分离,将读操作路由到从库,写操作路由到主库,提高系统吞吐量。

配置方式:在 schema.xml 中配置 readHost:

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="1"writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100"><heartbeat>select 1</heartbeat><!-- 写库 --><writeHost host="hostM1" url="jdbc:mysql://127.0.0.1:3306" user="root" password="root"><!-- 读库1 --><readHost host="hostS1" url="jdbc:mysql://127.0.0.1:3307" user="root" password="root" /><!-- 读库2 --><readHost host="hostS2" url="jdbc:mysql://127.0.0.1:3308" user="root" password="root" /></writeHost>
</dataHost>

balance 属性说明:

  • 0:不开启读写分离,所有操作都走写库
  • 1:全部的 readHost 与 stand by writeHost 参与 select 语句的负载均衡
  • 2:所有读操作随机在 writeHost 和 readHost 上分发
  • 3:所有读请求随机分发到 writeHost 对应的 readHost 上

5.2 高可用配置

MyCat 支持数据库高可用,当主库宕机时自动切换到备用主库。

<dataHost name="localhost1" maxCon="1000" minCon="10" balance="1"writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"  slaveThreshold="100"><heartbeat>select 1</heartbeat><!-- 主库1 --><writeHost host="hostM1" url="jdbc:mysql://127.0.0.1:3306" user="root" password="root"><readHost host="hostS1" url="jdbc:mysql://127.0.0.1:3307" user="root" password="root" /></writeHost><!-- 备用主库2 --><writeHost host="hostM2" url="jdbc:mysql://127.0.0.1:3309" user="root" password="root"><readHost host="hostS2" url="jdbc:mysql://127.0.0.1:3310" user="root" password="root" /></writeHost>
</dataHost>

switchType 属性说明:

  • -1:不自动切换
  • 1:默认值,自动切换
  • 2:基于 MySQL 主从同步的状态决定是否切换

5.3 全局表与 ER 表

全局表:在所有分片节点都存在的表,适用于数据量小、变动少的字典表。

配置:

<table name="dict_tbl" dataNode="dn$0-3" type="global" primaryKey="id" />

ER 表:基于 ER 关系的子表,与主表使用相同的分片规则,避免跨库关联查询。

配置:

<table name="order_tbl" dataNode="dn$0-3" rule="order_table_rule" primaryKey="id"><childTable name="order_item_tbl" joinKey="order_id" parentKey="id" />
</table>

5.4 SQL 拦截与改写

MyCat 支持对 SQL 进行拦截和改写,实现自定义的 SQL 处理逻辑。

配置方式:在 server.xml 中添加:

<system><property name="sqlInterceptor">com.jam.order.interceptor.MySqlInterceptor</property>
</system>

实现自定义拦截器:

package com.jam.order.interceptor;import io.mycat.MycatException;
import io.mycat.interceptor.SQLInterceptor;
import io.mycat.server.parser.ServerParse;
import io.mycat.session.MycatSession;/*** 自定义SQL拦截器** @author 果酱*/
public class MySqlInterceptor implements SQLInterceptor {@Overridepublic String interceptSQL(String sql, MycatSession session, int sqlType) throws MycatException {// 记录慢查询if (sqlType == ServerParse.SELECT) {// 可以在这里记录SQL,或者对SQL进行改写System.out.println("拦截到查询SQL: " + sql);}// 禁止全表扫描if (sqlType == ServerParse.SELECT && sql.contains("*") && !sql.contains("where")) {throw new MycatException("禁止全表扫描的SQL: " + sql);}return sql;}
}

六、性能优化:让 MyCat 发挥最佳性能

6.1 JVM 参数优化

MyCat 基于 Java 开发,合理的 JVM 参数设置对性能至关重要。

修改/opt/mycat/conf/wrapper.conf

# JVM参数设置
wrapper.java.additional.10=-Xms2G
wrapper.java.additional.11=-Xmx2G
wrapper.java.additional.12=-XX:MetaspaceSize=256m
wrapper.java.additional.13=-XX:MaxMetaspaceSize=512m
wrapper.java.additional.14=-XX:+UseG1GC
wrapper.java.additional.15=-XX:G1HeapRegionSize=16m
wrapper.java.additional.16=-XX:G1ReservePercent=25
wrapper.java.additional.17=-XX:InitiatingHeapOccupancyPercent=30
wrapper.java.additional.18=-XX:+HeapDumpOnOutOfMemoryError
wrapper.java.additional.19=-XX:HeapDumpPath=/opt/mycat/logs/heapdump.hprof

建议:

  • 堆内存设置为物理内存的 1/4 到 1/2
  • 使用 G1GC 收集器,适合大堆内存
  • 配置适当的日志和监控参数

6.2 连接池优化

MyCat 和应用程序的连接池都需要优化:

  1. MyCat 连接池优化(server.xml):
<system><property name="processorBufferPoolType">0</property><property name="processorExecutor">64</property><property name="maxStringLiteralLength">65535</property><property name="sequnceHandlerType">1</property><property name="backSocketNoDelay">1</property><property name="frontSocketNoDelay">1</property><property name="processorCheckPeriod">1000</property><property name="dataNodeIdleCheckPeriod">300000</property><property name="dataNodeHeartbeatPeriod">60000</property>
</system>
  1. 应用连接池优化
spring:datasource:hikari:maximum-pool-size: 20  # 根据并发量调整minimum-idle: 5idle-timeout: 300000connection-timeout: 20000max-lifetime: 1800000

6.3 SQL 优化

MyCat 环境下的 SQL 优化原则:

  1. 避免全表扫描:查询必须包含分片键
  2. 控制结果集大小:分页查询必须使用 LIMIT
  3. 优化 JOIN 操作:尽量使用 ER 表避免跨库 JOIN
  4. 避免复杂 SQL:复杂逻辑尽量在应用层实现
  5. 合理使用索引:索引必须包含分片键

反例:

-- 不包含分片键,会导致全库扫描
SELECT * FROM order_tbl WHERE order_status = 1;-- 没有分页,可能返回大量数据
SELECT * FROM order_tbl WHERE user_id = 123456;

正例:

-- 包含分片键user_id
SELECT * FROM order_tbl WHERE user_id = 123456 AND order_status = 1;-- 使用分页查询
SELECT * FROM order_tbl WHERE user_id = 123456 LIMIT 0, 20;

七、生产环境部署与监控

7.1 部署架构

推荐的生产环境部署架构:

关键部署建议:

  • MyCat 集群化部署,避免单点故障
  • 数据库采用主从架构,确保数据安全
  • 使用 Keepalived 实现 MyCat 高可用
  • 所有服务器分离部署,避免资源竞争

7.2 监控配置

MyCat 提供了多种监控方式:

  1. MyCat 监控中心
# 部署MyCat-web监控
wget http://dl.mycat.org.cn/mycat-web-1.0/Mycat-web-1.0-SNAPSHOT-20170102153329-linux.tar.gz
tar -zxvf Mycat-web-1.0-SNAPSHOT-20170102153329-linux.tar.gz
cd mycat-web
./start.sh

访问监控界面:http://localhost:8082/mycat

  1. Prometheus + Grafana 监控
  • 部署 MyCat 的 Prometheus exporter
  • 配置 Prometheus 收集指标
  • 使用 Grafana 创建监控面板

关键监控指标:

  • 吞吐量:QPS、TPS
  • 响应时间:平均响应时间、95% 响应时间
  • 连接数:当前连接数、最大连接数
  • 错误率:SQL 错误率、连接错误率
  • 后端数据库状态:主从同步延迟、连接数

7.3 运维脚本

MyCat 状态检查脚本

#!/bin/bash
# 检查MyCat是否运行
MYCAT_PID=$(ps -ef | grep mycat | grep -v grep | awk '{print $2}')
if [ -z "$MYCAT_PID" ]; thenecho "MyCat is not running, starting..."/opt/mycat/bin/mycat startsleep 5# 再次检查MYCAT_PID=$(ps -ef | grep mycat | grep -v grep | awk '{print $2}')if [ -z "$MYCAT_PID" ]; thenecho "Failed to start MyCat"# 发送告警curl -X POST -d "MyCat启动失败" http://alert-service/alertexit 1elseecho "MyCat started successfully"fi
elseecho "MyCat is running with PID: $MYCAT_PID"
fi

数据备份脚本

#!/bin/bash
# 备份所有分片数据库
BACKUP_DIR="/data/backup/mysql"
DATE=$(date +%Y%m%d%H%M%S)
DB_SUFFIXES="0 1 2 3"# 创建备份目录
mkdir -p $BACKUP_DIR/$DATE# 备份每个数据库
for suffix in $DB_SUFFIXES; dodb_name="order_db_$suffix"echo "Backing up $db_name..."mysqldump -h 127.0.0.1 -u root -p'root' -R -E --single-transaction $db_name > $BACKUP_DIR/$DATE/$db_name.sqlif [ $? -eq 0 ]; thenecho "$db_name backup successful"# 压缩备份文件gzip $BACKUP_DIR/$DATE/$db_name.sqlelseecho "$db_name backup failed"fi
done# 保留最近30天的备份
find $BACKUP_DIR -type d -mtime +30 -exec rm -rf {} \;

八、常见问题与解决方案

8.1 数据一致性问题

问题:分表分库后,跨库事务难以保证 ACID 特性。

解决方案

  1. 尽量避免跨库事务,通过业务设计将相关数据放在同一个分片
  2. 采用最终一致性方案,如本地消息表、事务消息
  3. 对于强一致性需求,可使用分布式事务框架如 Seata

示例代码(基于本地消息表的最终一致性):

/*** 创建订单并扣减库存(最终一致性实现)*/
@Transactional(rollbackFor = Exception.class)
public Long createOrderWithStock(Order order, List<OrderItem> items) {// 1. 创建订单Long orderId = createOrder(order);// 2. 记录本地消息for (OrderItem item : items) {LocalMessage message = new LocalMessage();message.setMessageId(UUID.randomUUID().toString());message.setBusinessType("STOCK_DEDUCT");message.setBusinessId(item.getProductId().toString());message.setContent(JSON.toJSONString(item));message.setStatus(0); // 待发送message.setCreateTime(LocalDateTime.now());localMessageMapper.insert(message);}// 3. 异步发送消息扣减库存stockMessageSender.sendStockDeductMessage(orderId, items);return orderId;
}

8.2 数据迁移与扩容

问题:随着业务增长,需要新增分片或调整分片规则。

解决方案

  1. 使用 MyCat 的 dataDiff 和 dataMigrate 工具进行数据迁移
  2. 采用 "双写" 策略:同时写入旧分片和新分片,验证无误后切换
  3. 迁移过程中通过监控确保数据一致性

数据迁移步骤:

  1. 准备新的分片环境
  2. 配置双写机制,同时写入新旧分片
  3. 使用工具同步历史数据
  4. 验证数据一致性
  5. 切换读流量到新分片
  6. 停止双写,只写入新分片

8.3 性能瓶颈排查

问题:系统响应变慢,需要定位性能瓶颈。

排查步骤

  1. 查看 MyCat 日志,分析慢 SQL
  2. 检查后端数据库性能,是否有慢查询
  3. 监控系统资源:CPU、内存、磁盘 IO、网络
  4. 检查连接池状态,是否有连接耗尽
  5. 分析 MyCat 监控指标,定位瓶颈环节

优化建议:

  • 为频繁查询添加缓存
  • 优化 SQL,避免全表扫描
  • 增加从库分担读压力
  • 调整 JVM 参数,避免频繁 GC
  • 对热点数据进行特殊处理

九、参考

  1. MySQL 性能阈值:参考 MySQL 官方文档(https://dev.mysql.com/doc/refman/8.0/en/table-size-limit.html)
  2. MyCat 配置与使用:参考 MyCat 官方文档(http://www.mycat.org.cn/documentation/)
  3. 分布式事务解决方案:参考《分布式服务架构:原理、设计与实战》(李艳鹏著)
  4. 连接池配置:参考 HikariCP 官方文档(https://github.com/brettwooldridge/HikariCP)
http://www.xdnf.cn/news/20250.html

相关文章:

  • 能发弹幕的简单视频网站
  • 计算机网络:调制解调器
  • Docker-volume数据卷
  • 为什么固态硬盘断电后数据还能保存不丢失?
  • 【LeetCode热题100道笔记】二叉树展开为链表
  • 激光频率梳 3D 轮廓测量 - 油路板的凹槽深度和平面度测量
  • Spring核心-Bean周期
  • ElmentUI之DateTimePicker 日期时间选择器
  • 避免使用非const全局变量:C++中的最佳实践 (C++ Core Guidelines)
  • SQLSERVER数据备份
  • Java8 Comparator接口 和 List Steam 排序使用案例
  • 人工智能在医学图像中的应用:从机器学习到深度学习
  • 技术方案详解:如何安全移动已安装软件?
  • C语言精讲(视频教程)
  • 打包 Uniapp
  • Redisson分布式锁:看门狗机制与续期原理
  • nginx安装部署(备忘)
  • ecplise配置maven插件
  • 【知识点讲解】稀疏注意力与LSH技术:从基础到前沿的完整指南
  • MHA高可用架构
  • 多线程(六) ~ 定时器与锁
  • 驱动开发系列71 - GLSL编译器实现 - 指令选择
  • python 逻辑运算练习题
  • HttpClient、OkHttp 和 WebClient
  • 贪心算法应用:交易费优化问题详解
  • OpenLayers常用控件 -- 章节七:测量工具控件教程
  • 《sklearn机器学习——聚类性能指标》Fowlkes-Mallows 得分
  • Java学习笔记二(类)
  • 【3D图像算法技术】如何在Blender中对复杂物体进行有效减面?
  • 【EXPLAIN详解:MySQL查询优化师的显微镜】