从 MySQL 到 TiDB:分布式数据库的无缝迁移与实战指南
当你的还在为 MySQL 分库分表的复杂运维头疼时,当你的业务数据量突破千万甚至亿级时,当你需要在保证高可用的同时实现弹性扩展时,一个名为 TiDB 的分布式 SQL 数据库正在悄然改变游戏规则。
本文将带你全面了解 TiDB—— 这个被称为 "MySQL 的分布式版本" 的新兴数据库,从底层架构到实战操作,从性能优化到最佳实践,让你不仅知道 TiDB 是什么,更能明白何时用、怎么用。无论你是数据库管理员、后端开发者还是架构师,读完本文都能掌握 TiDB 的核心知识点,并能在实际项目中灵活应用。
TiDB 是什么:分布式 SQL 的集大成者
TiDB 是由 PingCAP 公司开发的开源分布式 SQL 数据库,它结合了传统关系型数据库的易用性和 NoSQL 数据库的扩展性,旨在解决大规模数据场景下的存储和计算难题。
TiDB 的核心特性可以用 "NewSQL" 来概括:
- 像 MySQL 一样易用:支持 SQL、兼容 MySQL 协议
- 像 NoSQL 一样可扩展:水平扩展,无需分库分表
- 强一致性:支持 ACID 事务
- 高可用性:自动故障转移,无单点故障
如果你熟悉 MySQL,那么使用 TiDB 几乎没有学习成本;如果你正在为数据量增长带来的扩展性问题烦恼,TiDB 可能正是你寻找的解决方案。
TiDB 的核心架构:化繁为简的分布式设计
TiDB 采用了经典的 "计算 - 存储分离" 架构,将整个系统分为三个主要组件:
1. TiDB Server(计算层)
TiDB Server 是 TiDB 的 SQL 处理层,主要负责:
- 接收客户端的 SQL 请求
- SQL 解析、优化和执行
- 与存储层交互获取数据
- 维护会话状态
TiDB Server 是无状态的,这意味着你可以通过简单地增加 TiDB Server 节点来线性扩展计算能力。
2. PD Server(元数据层)
PD(Placement Driver)是 TiDB 的大脑,主要负责:
- 管理集群元数据
- 分配和调度数据分片(Region)
- 监控 TiKV 集群状态
- 选举 TiKV 的 Raft Leader
PD 采用 Raft 协议保证自身的高可用,通常部署奇数个节点(3 个或 5 个)。
3. TiKV Server(存储层)
TiKV 是 TiDB 的分布式存储引擎,基于 RocksDB 实现,主要负责:
- 数据的持久化存储
- 通过 Raft 协议保证数据一致性和高可用
- 按 Range 划分数据(Region)
- 提供分布式事务支持
TiKV 将数据按照 Key-Value 的形式存储,表中的一行数据会被编码为一个 Key-Value 对。
TiDB 的核心概念:理解分布式存储的基石
要真正掌握 TiDB,必须理解以下几个核心概念:
1. Region:数据的基本单位
TiKV 将数据按照 Key 的范围划分为多个 Region,每个 Region 的大小通常在 64MB 到 128MB 之间。可以把 Region 理解为 TiDB 中的 "数据分片"。
Region 具有以下特性:
- 每个 Region 由多个副本(通常 3 个),通过 Raft 协议保证一致性
- 每个 Region 有一个 Leader 副本,负责处理读写请求
- PD 负责 Region 的分裂和合并,以及副本的调度
2. Raft 协议:数据一致性的保障
TiKV 使用 Raft 协议来保证 Region 副本之间的数据一致性:
Raft 协议的核心流程:
- 客户端的所有读写请求都由 Leader 处理
- Leader 将修改操作记录为日志,并复制到所有 Follower
- 当大多数 Follower 确认收到日志后,Leader 提交日志并应用修改
- 如果 Leader 故障,Follower 会重新选举新的 Leader
3. 分布式事务:ACID 的分布式实现
TiDB 支持分布式事务,采用 Percolator 模型实现,保证 ACID 特性:
Percolator 模型的核心步骤:
- 选择一个主键作为 "协调者"(Primary Key)
- 对所有涉及的键进行预写(Pre-write),锁定这些键
- 如果所有预写都成功,提交主键
- 异步提交其他键(Secondary Key)
TiDB vs MySQL:为什么需要迁移?
虽然 TiDB 兼容 MySQL 协议,但两者在设计理念和适用场景上有很大差异:
特性 | MySQL | TiDB |
---|---|---|
架构 | 单体架构 | 分布式架构(计算 - 存储分离) |
扩展性 | 垂直扩展为主,水平扩展需分库分表 | 原生支持水平扩展 |
高可用 | 需借助外部工具(MGR、主从复制) | 内置高可用,自动故障转移 |
事务 | 单机事务 | 分布式事务(ACID) |
存储引擎 | InnoDB 等 | 基于 RocksDB 的 TiKV |
适合数据量 | GB 到 TB 级 | TB 到 PB 级 |
运维复杂度 | 中(分库分表后变高) | 低(自动管理) |
当你的业务面临以下挑战时,可能是时候考虑 TiDB 了:
- 数据量快速增长,单机 MySQL 难以承载
- 分库分表带来的运维复杂度越来越高
- 对高可用和灾备有严格要求
- 需要弹性扩展能力以应对业务波动
- 存在跨节点的复杂查询需求
环境搭建:从零开始部署 TiDB 集群
下面我们将一步步部署一个本地 TiDB 测试集群。
1. 安装 TiUP(TiDB 的包管理工具)
# 下载并安装TiUP
curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh# 重新加载环境变量
source .bash_profile# 确认安装成功
tiup --version
2. 部署本地测试集群
# 部署一个包含1个TiDB、1个PD和3个TiKV的集群
tiup playground v7.5.0 --db 1 --pd 1 --kv 3# 查看集群状态
tiup status
启动成功后,你将看到类似以下的输出:
CLUSTER START SUCCESSFULLY, Enjoy it ^-^
To connect TiDB: mysql --host 127.0.0.1 --port 4000 -u root
3. 连接 TiDB 集群
# 使用MySQL客户端连接TiDB
mysql -h 127.0.0.1 -P 4000 -u root
4. 访问 TiDB Dashboard
TiDB 提供了一个可视化的管理界面 Dashboard,默认地址为:
http://127.0.0.1:2379/dashboard
通过 Dashboard,你可以监控集群状态、查看慢查询、分析性能等。
数据库设计:TiDB 中的表结构最佳实践
虽然 TiDB 兼容 MySQL 的表结构设计,但为了充分发挥 TiDB 的性能,需要遵循一些特定的最佳实践。
1. 数据库和表的创建
-- 创建数据库
CREATE DATABASE IF NOT EXISTS ecommerce CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE ecommerce;-- 创建商品表(优化设计)
CREATE TABLE `product` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',`category_id` bigint NOT NULL COMMENT '分类ID',`name` varchar(255) NOT NULL COMMENT '商品名称',`price` decimal(10,2) NOT NULL COMMENT '商品价格',`stock` int NOT NULL DEFAULT 0 COMMENT '库存数量',`sales` int NOT NULL DEFAULT 0 COMMENT '销售数量',`status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-下架,1-上架',`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,KEY `idx_category_id` (`category_id`),KEY `idx_status_sales` (`status`,`sales`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';-- 创建订单表(优化设计)
CREATE TABLE `order` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',`order_no` varchar(64) NOT NULL COMMENT '订单编号',`user_id` bigint NOT NULL COMMENT '用户ID',`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',`status` tinyint NOT NULL COMMENT '状态:0-待支付,1-已支付,2-已取消,3-已完成',`payment_time` datetime DEFAULT NULL COMMENT '支付时间',`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,UNIQUE KEY `uk_order_no` (`order_no`),KEY `idx_user_id_created_at` (`user_id`,`created_at`),KEY `idx_status_created_at` (`status`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';-- 创建订单项表(优化设计)
CREATE TABLE `order_item` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单项ID',`order_id` bigint NOT NULL COMMENT '订单ID',`product_id` bigint NOT NULL COMMENT '商品ID',`quantity` int NOT NULL COMMENT '购买数量',`price` decimal(10,2) NOT NULL COMMENT '购买单价',`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,KEY `idx_order_id` (`order_id`),KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';
2. TiDB 表设计的关键差异
-
聚簇索引(CLUSTERED INDEX):
- TiDB 默认将主键作为聚簇索引,数据按照主键的顺序存储
- 这与 MySQL 的 InnoDB 引擎类似,但 TiDB 不支持非主键的聚簇索引
-
索引设计:
- 二级索引会存储主键信息,通过主键查找实际数据
- 联合索引的顺序非常重要,应将过滤性强的字段放在前面
-
自增 ID:
- TiDB 的自增 ID 是分布式生成的,可能存在间隙
- 对于高并发场景,建议使用雪花算法生成 ID
3. 分区表设计
TiDB 支持分区表,可以按照范围、列表或哈希进行分区:
-- 按时间范围分区的订单表
CREATE TABLE `order_history` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',`order_no` varchar(64) NOT NULL COMMENT '订单编号',`user_id` bigint NOT NULL COMMENT '用户ID',`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',`status` tinyint NOT NULL COMMENT '状态',`created_at` datetime NOT NULL COMMENT '创建时间',PRIMARY KEY (`id`,`created_at`) /*T![clustered_index] CLUSTERED */,UNIQUE KEY `uk_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(created_at)) (PARTITION p202301 VALUES LESS THAN (TO_DAYS('2023-02-01')),PARTITION p202302 VALUES LESS THAN (TO_DAYS('2023-03-01')),PARTITION p202303 VALUES LESS THAN (TO_DAYS('2023-04-01')),PARTITION p202304 VALUES LESS THAN (TO_DAYS('2023-05-01')),PARTITION p202305 VALUES LESS THAN (TO_DAYS('2023-06-01')),PARTITION p202306 VALUES LESS THAN (TO_DAYS('2023-07-01')),PARTITION pfuture VALUES LESS THAN MAXVALUE
) COMMENT='历史订单表';
分区表的优势:
- 提高查询性能,只扫描相关分区
- 方便数据管理,如按分区删除历史数据
- 均衡数据分布,避免单一 Region 过大
数据迁移:从 MySQL 到 TiDB 的无缝过渡
将数据从 MySQL 迁移到 TiDB 有多种方式,这里我们介绍两种常用方法:使用 TiDB Lightning 和使用 Dumpling+Lightning。
1. 使用 TiDB Lightning 迁移全量数据
TiDB Lightning 是一个快速导入大量数据到 TiDB 的工具,支持从 SQL 文件或 CSV 文件导入。
# 安装TiDB Lightning
tiup install lightning# 从MySQL导出数据
mysqldump -h 127.0.0.1 -P 3306 -u root -p --databases ecommerce > ecommerce.sql# 创建配置文件 lightning.toml
cat > lightning.toml << EOF
[lightning]
# 日志级别
log-level = "info"
# 并发数,根据CPU核心数调整
region-concurrency = 16
# 检查点目录
checkpoint-dir = "./checkpoint"[tikv-importer]
# 使用Local后端模式
backend = "local"
# 本地临时存储目录,需要较大的空间
sorted-kv-dir = "./sorted-kv"[mydumper]
# 数据来源目录
data-source-dir = "./ecommerce.sql"[tidb]
# 目标TiDB地址
host = "127.0.0.1"
port = 4000
user = "root"
password = ""
# 目标数据库名称
db-name = "ecommerce"
EOF# 执行导入
tiup lightning -config lightning.toml
2. 使用 Dumpling+Lightning 迁移(推荐)
Dumpling 是 TiDB 生态中的数据导出工具,比 mysqldump 更高效:
# 安装Dumpling
tiup install dumpling# 从MySQL导出数据
tiup dumpling -h 127.0.0.1 -P 3306 -u root -p -B ecommerce -f 'ecommerce.*' -o ./ecommerce_data# 使用Lightning导入(配置文件同上,只需修改data-source-dir)
tiup lightning -config lightning.toml
3. 增量数据同步
对于需要保持 MySQL 和 TiDB 数据同步的场景,可以使用 TiCDC:
# 安装TiCDC
tiup install cdc# 创建同步任务
tiup cdc cli changefeed create --server=http://127.0.0.1:8300 \--sink-uri="mysql://root@tcp(127.0.0.1:4000)/ecommerce" \--changefeed-id="mysql-to-tidb"
Java 实战:Spring Boot 集成 TiDB
TiDB 兼容 MySQL 协议,因此与 Java 应用的集成方式和 MySQL 基本相同。下面我们将实现一个 Spring Boot 应用连接 TiDB。
1. 项目依赖
<!-- pom.xml -->
<?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 https://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.example</groupId><artifactId>tidb-demo</artifactId><version>0.0.1-SNAPSHOT</version><name>tidb-demo</name><description>Demo project for Spring Boot with TiDB</description><properties><java.version>17</java.version></properties><dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 数据库 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>8.3.0</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.5</version></dependency><!-- 工具类 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.45</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>33.1.0-jre</version></dependency><!-- Swagger3 --><dependency><groupId>org.springdoc</groupId><artifactId>springdoc-openapi-starter-webmvc-ui</artifactId><version>2.2.0</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>
2. 配置文件
# application.yml
spring:datasource:url: jdbc:mysql://127.0.0.1:4000/ecommerce?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=trueusername: rootpassword:driver-class-name: com.mysql.cj.jdbc.Driverhikari:maximum-pool-size: 20minimum-idle: 5idle-timeout: 300000connection-timeout: 20000mybatis-plus:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.example.tidbdemo.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.slf4j.Slf4jImplspringdoc:api-docs:path: /api-docsswagger-ui:path: /swagger-ui.htmloperationsSorter: methodlogging:level:com.example.tidbdemo: infoorg.springframework.jdbc.core: debug
3. 实体类
package com.example.tidbdemo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;/*** 商品表实体类** @author ken*/
@Data
@TableName("product")
public class Product {/*** 商品ID*/@TableId(type = IdType.AUTO)private Long id;/*** 分类ID*/@TableField("category_id")private Long categoryId;/*** 商品名称*/@TableField("name")private String name;/*** 商品价格*/@TableField("price")private BigDecimal price;/*** 库存数量*/@TableField("stock")private Integer stock;/*** 销售数量*/@TableField("sales")private Integer sales;/*** 状态:0-下架,1-上架*/@TableField("status")private Integer status;/*** 创建时间*/@TableField("created_at")private LocalDateTime createdAt;/*** 更新时间*/@TableField("updated_at")private LocalDateTime updatedAt;
}
package com.example.tidbdemo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;/*** 订单表实体类** @author ken*/
@Data
@TableName("order")
public class Order {/*** 订单ID*/@TableId(type = IdType.AUTO)private Long id;/*** 订单编号*/@TableField("order_no")private String orderNo;/*** 用户ID*/@TableField("user_id")private Long userId;/*** 订单总金额*/@TableField("total_amount")private BigDecimal totalAmount;/*** 状态:0-待支付,1-已支付,2-已取消,3-已完成*/@TableField("status")private Integer status;/*** 支付时间*/@TableField("payment_time")private LocalDateTime paymentTime;/*** 创建时间*/@TableField("created_at")private LocalDateTime createdAt;/*** 更新时间*/@TableField("updated_at")private LocalDateTime updatedAt;
}
package com.example.tidbdemo.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import lombok.Data;/*** 订单项表实体类** @author ken*/
@Data
@TableName("order_item")
public class OrderItem {/*** 订单项ID*/@TableId(type = IdType.AUTO)private Long id;/*** 订单ID*/@TableField("order_id")private Long orderId;/*** 商品ID*/@TableField("product_id")private Long productId;/*** 购买数量*/@TableField("quantity")private Integer quantity;/*** 购买单价*/@TableField("price")private BigDecimal price;/*** 创建时间*/@TableField("created_at")private LocalDateTime createdAt;
}
4. Mapper 接口
package com.example.tidbdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.tidbdemo.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;/*** 商品Mapper接口** @author ken*/
@Mapper
public interface ProductMapper extends BaseMapper<Product> {/*** 扣减库存(带乐观锁)** @param id 商品ID* @param quantity 扣减数量* @param version 版本号(用于乐观锁)* @return 影响的行数*/int deductStock(@Param("id") Long id, @Param("quantity") Integer quantity, @Param("version") Integer version);
}
package com.example.tidbdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.tidbdemo.entity.Order;
import org.apache.ibatis.annotations.Mapper;/*** 订单Mapper接口** @author ken*/
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
package com.example.tidbdemo.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.tidbdemo.entity.OrderItem;
import org.apache.ibatis.annotations.Mapper;/*** 订单项Mapper接口** @author ken*/
@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
}
5. Service 层
package com.example.tidbdemo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.tidbdemo.entity.Product;/*** 商品服务接口** @author ken*/
public interface ProductService extends IService<Product> {/*** 根据ID查询商品** @param id 商品ID* @return 商品信息*/Product getById(Long id);/*** 扣减库存** @param id 商品ID* @param quantity 扣减数量* @return 是否成功*/boolean deductStock(Long id, Integer quantity);
}
package com.example.tidbdemo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.tidbdemo.entity.Product;
import com.example.tidbdemo.mapper.ProductMapper;
import com.example.tidbdemo.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;/*** 商品服务实现类** @author ken*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {private final ProductMapper productMapper;@Overridepublic Product getById(Long id) {if (ObjectUtils.isEmpty(id)) {log.error("商品ID不能为空");return null;}return productMapper.selectById(id);}@Override@Transactional(rollbackFor = Exception.class)public boolean deductStock(Long id, Integer quantity) {log.info("开始扣减库存,商品ID: {}, 数量: {}", id, quantity);// 参数校验if (ObjectUtils.isEmpty(id)) {log.error("商品ID不能为空");return false;}if (quantity == null || quantity <= 0) {log.error("扣减数量必须大于0");return false;}// 查询商品Product product = getById(id);if (ObjectUtils.isEmpty(product)) {log.error("商品不存在,ID: {}", id);return false;}// 检查库存if (product.getStock() < quantity) {log.error("库存不足,商品ID: {}, 可用库存: {}, 需扣减: {}", id, product.getStock(), quantity);return false;}// 扣减库存(使用乐观锁避免并发问题)int affectedRows = productMapper.deductStock(id, quantity, product.getVersion());if (affectedRows <= 0) {log.error("库存扣减失败,可能存在并发修改,商品ID: {}", id);return false;}log.info("库存扣减成功,商品ID: {}, 扣减数量: {}", id, quantity);return true;}
}
package com.example.tidbdemo.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.example.tidbdemo.entity.Order;
import com.example.tidbdemo.vo.CreateOrderVO;/*** 订单服务接口** @author ken*/
public interface OrderService extends IService<Order> {/*** 创建订单** @param createOrderVO 创建订单参数* @return 订单ID*/Long createOrder(CreateOrderVO createOrderVO);/*** 更新订单状态** @param orderId 订单ID* @param status 状态* @return 是否成功*/boolean updateOrderStatus(Long orderId, Integer status);
}
package com.example.tidbdemo.service.impl;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.tidbdemo.entity.Order;
import com.example.tidbdemo.entity.OrderItem;
import com.example.tidbdemo.entity.Product;
import com.example.tidbdemo.mapper.OrderItemMapper;
import com.example.tidbdemo.mapper.OrderMapper;
import com.example.tidbdemo.service.OrderService;
import com.example.tidbdemo.service.ProductService;
import com.example.tidbdemo.vo.CreateOrderVO;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;/*** 订单服务实现类** @author ken*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {private final OrderMapper orderMapper;private final OrderItemMapper orderItemMapper;private final ProductService productService;@Override@Transactional(rollbackFor = Exception.class)public Long createOrder(CreateOrderVO createOrderVO) {log.info("开始创建订单,参数: {}", createOrderVO);// 参数校验if (ObjectUtils.isEmpty(createOrderVO.getUserId())) {throw new IllegalArgumentException("用户ID不能为空");}if (ObjectUtils.isEmpty(createOrderVO.getItems()) || createOrderVO.getItems().isEmpty()) {throw new IllegalArgumentException("订单项不能为空");}// 1. 生成订单编号String orderNo = generateOrderNo();// 2. 计算总金额,检查并扣减库存BigDecimal totalAmount = BigDecimal.ZERO;List<OrderItem> orderItems = Lists.newArrayList();for (CreateOrderVO.OrderItemVO itemVO : createOrderVO.getItems()) {// 查询商品Product product = productService.getById(itemVO.getProductId());if (ObjectUtils.isEmpty(product)) {throw new RuntimeException("商品不存在,ID: " + itemVO.getProductId());}// 检查库存if (product.getStock() < itemVO.getQuantity()) {throw new RuntimeException("商品库存不足,ID: " + itemVO.getProductId());}// 扣减库存boolean deductResult = productService.deductStock(itemVO.getProductId(), itemVO.getQuantity());if (!deductResult) {throw new RuntimeException("商品库存扣减失败,ID: " + itemVO.getProductId());}// 计算金额BigDecimal itemAmount = product.getPrice().multiply(new BigDecimal(itemVO.getQuantity()));totalAmount = totalAmount.add(itemAmount);// 创建订单项OrderItem orderItem = new OrderItem();orderItem.setProductId(itemVO.getProductId());orderItem.setQuantity(itemVO.getQuantity());orderItem.setPrice(product.getPrice());orderItem.setCreatedAt(LocalDateTime.now());orderItems.add(orderItem);}// 3. 创建订单Order order = new Order();order.setOrderNo(orderNo);order.setUserId(createOrderVO.getUserId());order.setTotalAmount(totalAmount);order.setStatus(0); // 0-待支付order.setCreatedAt(LocalDateTime.now());order.setUpdatedAt(LocalDateTime.now());orderMapper.insert(order);log.info("订单创建成功,ID: {}", order.getId());// 4. 创建订单项for (OrderItem item : orderItems) {item.setOrderId(order.getId());orderItemMapper.insert(item);}log.info("订单项创建成功,数量: {}", orderItems.size());return order.getId();}@Override@Transactional(rollbackFor = Exception.class)public boolean updateOrderStatus(Long orderId, Integer status) {log.info("更新订单状态,订单ID: {}, 状态: {}", orderId, status);// 参数校验if (ObjectUtils.isEmpty(orderId)) {log.error("订单ID不能为空");return false;}if (ObjectUtils.isEmpty(status)) {log.error("状态不能为空");return false;}// 查询订单Order order = getById(orderId);if (ObjectUtils.isEmpty(order)) {log.error("订单不存在,ID: {}", orderId);return false;}// 更新状态Order updateOrder = new Order();updateOrder.setId(orderId);updateOrder.setStatus(status);updateOrder.setUpdatedAt(LocalDateTime.now());// 如果是支付状态,更新支付时间if (status == 1) {updateOrder.setPaymentTime(LocalDateTime.now());}int affectedRows = orderMapper.updateById(updateOrder);if (affectedRows <= 0) {log.error("订单状态更新失败,ID: {}", orderId);return false;}log.info("订单状态更新成功,ID: {}, 状态: {}", orderId, status);return true;}/*** 生成订单编号** @return 订单编号*/private String generateOrderNo() {// 订单编号规则:时间戳 + 随机数return System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8);}
}
6. Controller 层
package com.example.tidbdemo.controller;import com.example.tidbdemo.entity.Product;
import com.example.tidbdemo.service.ProductService;
import com.example.tidbdemo.vo.CommonResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;/*** 商品控制器** @author ken*/
@Slf4j
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
@Tag(name = "商品管理", description = "商品相关接口")
public class ProductController {private final ProductService productService;/*** 根据ID查询商品** @param id 商品ID* @return 商品信息*/@GetMapping("/{id}")@Operation(summary = "查询商品", description = "根据ID查询商品详情")public CommonResult<Product> getProductById(@Parameter(description = "商品ID", required = true) @PathVariable Long id) {Product product = productService.getById(id);return CommonResult.success(product);}/*** 创建商品** @param product 商品信息* @return 创建结果*/@PostMapping@Operation(summary = "创建商品", description = "创建新商品")public CommonResult<Long> createProduct(@RequestBody Product product) {boolean success = productService.save(product);if (success) {return CommonResult.success(product.getId());} else {return CommonResult.failed("创建商品失败");}}
}
package com.example.tidbdemo.controller;import com.example.tidbdemo.service.OrderService;
import com.example.tidbdemo.vo.CommonResult;
import com.example.tidbdemo.vo.CreateOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;/*** 订单控制器** @author ken*/
@Slf4j
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单相关接口")
public class OrderController {private final OrderService orderService;/*** 创建订单** @param createOrderVO 创建订单参数* @return 订单ID*/@PostMapping@Operation(summary = "创建订单", description = "创建新订单")public CommonResult<Long> createOrder(@RequestBody CreateOrderVO createOrderVO) {Long orderId = orderService.createOrder(createOrderVO);return CommonResult.success(orderId);}/*** 更新订单状态** @param orderId 订单ID* @param status 状态* @return 更新结果*/@PutMapping("/{orderId}/status")@Operation(summary = "更新订单状态", description = "更新订单状态")public CommonResult<Boolean> updateOrderStatus(@Parameter(description = "订单ID", required = true) @PathVariable Long orderId,@Parameter(description = "状态:0-待支付,1-已支付,2-已取消,3-已完成", required = true) @RequestParam Integer status) {boolean success = orderService.updateOrderStatus(orderId, status);return CommonResult.success(success);}
}
7. 通用 VO 和结果类
package com.example.tidbdemo.vo;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;/*** 创建订单请求VO** @author ken*/
@Data
@Schema(description = "创建订单请求参数")
public class CreateOrderVO {/*** 用户ID*/@Schema(description = "用户ID", required = true)private Long userId;/*** 订单项列表*/@Schema(description = "订单项列表", required = true)private List<OrderItemVO> items;/*** 订单项VO*/@Data@Schema(description = "订单项参数")public static class OrderItemVO {/*** 商品ID*/@Schema(description = "商品ID", required = true)private Long productId;/*** 购买数量*/@Schema(description = "购买数量", required = true, minimum = "1")private Integer quantity;}
}
package com.example.tidbdemo.vo;import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;/*** 通用返回结果** @author ken*/
@Data
@Schema(description = "通用返回结果")
public class CommonResult<T> {/*** 成功状态码*/public static final int SUCCESS_CODE = 200;/*** 失败状态码*/public static final int FAIL_CODE = 500;/*** 状态码*/@Schema(description = "状态码:200-成功,500-失败")private int code;/*** 消息*/@Schema(description = "返回消息")private String message;/*** 数据*/@Schema(description = "返回数据")private T data;/*** 成功返回** @param data 数据* @param <T> 数据类型* @return 通用返回结果*/public static <T> CommonResult<T> success(T data) {CommonResult<T> result = new CommonResult<>();result.setCode(SUCCESS_CODE);result.setMessage("操作成功");result.setData(data);return result;}/*** 失败返回** @param message 失败消息* @param <T> 数据类型* @return 通用返回结果*/public static <T> CommonResult<T> failed(String message) {CommonResult<T> result = new CommonResult<>();result.setCode(FAIL_CODE);result.setMessage(message);result.setData(null);return result;}
}
8. 启动类
package com.example.tidbdemo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** TiDB demo启动类** @author ken*/
@SpringBootApplication
@MapperScan("com.example.tidbdemo.mapper")
public class TidbDemoApplication {public static void main(String[] args) {SpringApplication.run(TidbDemoApplication.class, args);}
}
性能优化:让 TiDB 发挥最佳性能
TiDB 的性能优化涉及多个方面,从表设计到查询优化,再到集群配置,都有可以优化的空间。
1. 表设计优化
-
主键选择:
- 尽量使用自增 ID 作为主键,避免使用 UUID 等随机值
- 主键应具有单调性,便于 Region 分裂和数据均衡
-
索引优化:
- 只为频繁查询的字段创建索引
- 合理设计联合索引,将过滤性强的字段放在前面
- 避免创建过多索引,索引会影响写入性能
-
分区表使用:
- 对于大表(千万级以上),建议使用分区表
- 按时间或业务维度进行分区,使查询只涉及部分分区
2. SQL 查询优化
- 避免全表扫描:
- 确保查询条件包含索引字段
- 使用
EXPLAIN
分析查询计划,查看是否使用了索引
-- 分析查询计划
EXPLAIN SELECT * FROM `order` WHERE user_id = 123 AND created_at > '2023-01-01';
- 分页查询优化:
- 避免使用
LIMIT offset, row_count
进行深分页 - 采用 "上一页最后一条记录的 ID" 进行分页
- 避免使用
-- 不推荐:深分页效率低
SELECT * FROM `order` WHERE user_id = 123 ORDER BY created_at DESC LIMIT 100000, 10;-- 推荐:基于ID的分页
SELECT * FROM `order` WHERE user_id = 123 AND id < 1000000 ORDER BY created_at DESC LIMIT 10;
-
避免大事务:
- 将大事务拆分为多个小事务
- 长事务会占用锁资源,影响并发性能
-
批量操作优化:
- 使用批量插入代替单条插入
- 批量操作的大小建议控制在 1000 以内
-- 推荐:批量插入
INSERT INTO product (category_id, name, price, stock) VALUES
(1, '商品1', 99.99, 100),
(1, '商品2', 199.99, 50),
(2, '商品3', 299.99, 80);
3. 集群配置优化
-
TiKV 配置:
- 调整
storage.block-cache.capacity
设置合适的缓存大小 - 根据磁盘类型调整
raftstore.sync-log
(SSD 可设为 false)
- 调整
-
TiDB 配置:
- 调整
tidb_mem_quota_query
设置查询内存限制 - 根据 CPU 核心数调整
performance.max-procs
- 调整
-
PD 配置:
- 调整
replication.max-replicas
设置副本数量(默认 3 个) - 根据业务需求调整
schedule.leader-schedule-policy
- 调整
4. 读写分离
TiDB 支持通过 TiDB Server 实现读写分离:
- 可以将读请求路由到只读 TiDB 节点
- 写请求路由到主 TiDB 节点
# 读写分离配置示例
spring:shardingsphere:datasource:names: primary,replicaprimary:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://tidb-primary:4000/ecommerceusername: rootpassword:replica:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://tidb-replica:4000/ecommerceusername: rootpassword:rules:readwrite-splitting:data-sources:ecommerce:type: Staticprops:write-data-source-name: primaryread-data-source-names: replicaload-balancer-name: round_robinload-balancers:round_robin:type: RoundRobin
最佳实践:TiDB 在生产环境中的经验
1. 集群部署
- 生产环境建议至少部署 3 个 TiDB 节点、3 个 PD 节点和 3 个 TiKV 节点
- TiKV 节点应部署在独立的物理机或虚拟机上,避免资源竞争
- 建议使用 SSD 存储 TiKV 数据,提高 IO 性能
2. 备份与恢复
定期备份 TiDB 集群数据,以防数据丢失:
# 备份整个集群
tiup br backup full --pd "127.0.0.1:2379" --storage "local:///data/backup"# 恢复整个集群
tiup br restore full --pd "127.0.0.1:2379" --storage "local:///data/backup"
3. 监控与告警
- 部署 Prometheus 和 Grafana 监控 TiDB 集群
- 关注关键指标:QPS、延迟、Region 健康状态、磁盘使用率等
- 设置合理的告警阈值,及时发现和解决问题
4. 扩容与缩容
TiDB 支持在线扩容和缩容,不影响业务运行:
# 扩容TiKV节点
tiup cluster scale-out tidb-cluster scale-out.yaml# 缩容TiKV节点
tiup cluster scale-in tidb-cluster --node 192.168.1.10:20160
5. 版本升级
定期升级 TiDB 版本,获取新功能和性能优化:
# 升级集群到指定版本
tiup cluster upgrade tidb-cluster v7.5.0
总结:TiDB 的适用场景与未来展望
TiDB 作为一款优秀的分布式 SQL 数据库,正在被越来越多的企业采用。它的优势在于:
- 无缝迁移:兼容 MySQL 协议,现有 MySQL 应用几乎无需修改即可迁移
- 弹性扩展:支持水平扩展,轻松应对数据量增长
- 高可用性:内置高可用机制,自动故障转移,无单点故障
- 强一致性:支持分布式事务,保证数据一致性
TiDB 特别适合以下场景:
- 数据量快速增长,单机 MySQL 难以承载
- 需要弹性扩展能力,应对业务波动
- 对高可用和数据一致性有严格要求
- 希望避免分库分表带来的复杂性
当然,TiDB 也有其局限性,如在某些场景下性能可能不如优化良好的单机 MySQL,部署和维护相对复杂等。因此,在选择 TiDB 时,需要根据实际业务场景进行综合评估。