
凌晨三点,电商平台的数据库监控告警突然响起。你登录系统发现,订单查询接口响应时间从正常的 50ms 飙升到了 5 秒以上,数据库连接池频繁耗尽,后台管理系统几乎无法操作。
查看数据库指标,订单表数据量已经达到 1.2 亿行,单个表文件大小超过 100GB。每次简单的分页查询都需要扫描数十万行数据,索引维护成本越来越高,数据库服务器的 CPU 利用率长期维持在 95% 以上。
这不是虚构的场景,而是每个快速成长的电商平台都会面临的 "数据库瓶颈"。根据 MySQL 官方性能测试报告(https://dev.mysql.com/doc/refman/8.0/en/performance-schema.html),当单表数据量超过 1000 万行时,查询性能会出现显著下降。
分表分库技术是解决这一问题的核心方案,而 MyCat 作为国内最流行的分布式数据库中间件,凭借其简单易用、功能全面的特点,已成为众多企业的首选。本文将带你全面掌握 MyCat 在订单系统中的分表分库实战,从理论到实践,包含完整可运行代码和最佳实践。
MyCat 是一款基于 Java 开发的分布式数据库中间件,它模拟 MySQL 协议,实现了数据库的分表分库、读写分离、高可用等功能。应用程序无需修改代码,只需将连接指向 MyCat,即可实现对后端多个数据库的透明访问。

MyCat 的核心价值在于:
在使用 MyCat 之前,我们需要理解几个核心概念:
db1.order_0user_id阿里巴巴《Java 开发手册(嵩山版)》明确指出:"分库分表时,表名的命名最好能体现分片规则,如 t_order_00 到 t_order_15,便于阅读和理解"。
目前主流的分表分库中间件有 MyCat、ShardingSphere、TDDL 等,它们的对比:
特性 | MyCat | ShardingSphere-JDBC | ShardingSphere-Proxy |
|---|---|---|---|
部署方式 | 独立服务 | 嵌入应用 | 独立服务 |
协议支持 | MySQL | 多数据库 | 多数据库 |
学习成本 | 中等 | 较高 | 中等 |
性能 | 良好 | 优秀 | 良好 |
适用场景 | 中小团队、快速部署 | 开发能力强的团队 | 多语言支持场景 |
MyCat 的优势在于部署简单、运维方便,对应用无侵入,特别适合中小团队快速实现分表分库。
一个典型的电商订单表应包含以下字段:
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='订单表';
订单表的分片策略需要结合业务查询模式来设计。常见的查询场景:
基于这些场景,我们设计复合分片策略:

这种策略的优势:
单表数据量的合理阈值是分表分库设计的关键。根据测试和经验:
假设平台日均订单 10 万,每月约 300 万订单,按 4 个库分片,每个库每月约 75 万订单,正好在合理阈值内。
分表分库后,传统的自增 ID 无法保证全局唯一,需要全局 ID 生成策略。MyCat 支持多种全局 ID 生成方式:
对于订单 ID,推荐使用雪花算法,它生成的 64 位 ID 包含时间戳、机器 ID 等信息,既保证唯一性,又便于定位数据所在分片。
首先创建分库分表所需的数据库和表结构:
-- 创建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)
BEGIN
DECLARE 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))
BEGIN
SET @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;
tar -zxvf Mycat-server-1.6.7.6-release-20220524173824-linux.tar.gz -C /opt/echo 'export MYCAT_HOME=/opt/mycat' >> /etc/profile
echo 'export PATH=$PATH:$MYCAT_HOME/bin' >> /etc/profile
source /etc/profile
MyCat 的核心配置文件位于/opt/mycat/conf目录下,主要包括:
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&serverTimezone=Asia/Shanghai"
user="root" password="root">
<!-- 读库配置 -->
<readHost host="hostS1" url="jdbc:mysql://127.0.0.1:3307?useSSL=false&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.properties):
# 序列名称到数据库的映射
ORDER_ID=mycat_seq_db
# 启动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
<?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>
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:8066/ORDER_DB?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
max-lifetime: 1800000
mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml
global-config:
db-config:
id-type: INPUT # 手动输入ID,使用MyCat的全局序列
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
operationsSorter: method
packages-to-scan: com.jam.order.controller
logging:
level:
com.jam.order.mapper: debug
com.jam.order.service: info
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;
}
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");
}
}
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_tbl
WHERE 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_tbl
WHERE order_status = #{orderStatus}
AND create_time BETWEEN #{startTime} AND #{endTime}
</select>
</mapper>
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不能为空");
// 获取全局ID
Long 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 订单列表
*/
@Override
public 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 订单数量
*/
@Override
public 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);
}
}
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);
}
}
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);
}
}
使用 Swagger UI 进行测试,访问地址:http://localhost:8080/swagger-ui.html
{
"userId": 123456,
"totalAmount": 999.99,
"payAmount": 999.99,
"freight": 0.00,
"createTime": "2023-10-15T10:30:00"
}
/api/v1/orders/user/123456?startTime=2023-10-01T00:00:00&endTime=2023-10-31T23:59:59# 查看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;
使用 JMeter 进行性能测试,模拟 100 并发用户创建订单,观察系统响应时间和错误率。
预期结果:
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 属性说明:
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 属性说明:
全局表:在所有分片节点都存在的表,适用于数据量小、变动少的字典表。
配置:
<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>
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 {
@Override
public 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 基于 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
建议:
MyCat 和应用程序的连接池都需要优化:
<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>
应用连接池优化
spring:
datasource:
hikari:
maximum-pool-size: 20 # 根据并发量调整
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
max-lifetime: 1800000MyCat 环境下的 SQL 优化原则:
反例:
-- 不包含分片键,会导致全库扫描
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;
推荐的生产环境部署架构:

关键部署建议:
MyCat 提供了多种监控方式:
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
关键监控指标:
MyCat 状态检查脚本:
#!/bin/bash
# 检查MyCat是否运行
MYCAT_PID=$(ps -ef | grep mycat | grep -v grep | awk '{print $2}')
if [ -z "$MYCAT_PID" ]; then
echo "MyCat is not running, starting..."
/opt/mycat/bin/mycat start
sleep 5
# 再次检查
MYCAT_PID=$(ps -ef | grep mycat | grep -v grep | awk '{print $2}')
if [ -z "$MYCAT_PID" ]; then
echo "Failed to start MyCat"
# 发送告警
curl -X POST -d "MyCat启动失败" http://alert-service/alert
exit 1
else
echo "MyCat started successfully"
fi
else
echo "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; do
db_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.sql
if [ $? -eq 0 ]; then
echo "$db_name backup successful"
# 压缩备份文件
gzip $BACKUP_DIR/$DATE/$db_name.sql
else
echo "$db_name backup failed"
fi
done
# 保留最近30天的备份
find $BACKUP_DIR -type d -mtime +30 -exec rm -rf {} \;
问题:分表分库后,跨库事务难以保证 ACID 特性。
解决方案:
示例代码(基于本地消息表的最终一致性):
/**
* 创建订单并扣减库存(最终一致性实现)
*/
@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;
}
问题:随着业务增长,需要新增分片或调整分片规则。
解决方案:
数据迁移步骤:
问题:系统响应变慢,需要定位性能瓶颈。
排查步骤:
优化建议: