当你的还在为 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=true
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.tidbdemo.entity
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
logging:
level:
com.example.tidbdemo: info
org.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;
@Override
public 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 * FROMorder
WHERE user_id = 123 AND created_at > '2023-01-01'; -
分页查询优化 :
- 避免使用
LIMIT offset, row_count
进行深分页 - 采用 "上一页最后一条记录的 ID" 进行分页
-- 不推荐:深分页效率低
SELECT * FROMorder
WHERE user_id = 123 ORDER BY created_at DESC LIMIT 100000, 10;-- 推荐:基于ID的分页
SELECT * FROMorder
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,replica
primary:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://tidb-primary:4000/ecommerce
username: root
password:
replica:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://tidb-replica:4000/ecommerce
username: root
password:
rules:
readwrite-splitting:
data-sources:
ecommerce:
type: Static
props:
write-data-source-name: primary
read-data-source-names: replica
load-balancer-name: round_robin
load-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 时,需要根据实际业务场景进行综合评估。