从 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 模型的核心步骤:

  1. 选择一个主键作为 "协调者"(Primary Key)
  2. 对所有涉及的键进行预写(Pre-write),锁定这些键
  3. 如果所有预写都成功,提交主键
  4. 异步提交其他键(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 表设计的关键差异

  1. 聚簇索引(CLUSTERED INDEX)

    • TiDB 默认将主键作为聚簇索引,数据按照主键的顺序存储
    • 这与 MySQL 的 InnoDB 引擎类似,但 TiDB 不支持非主键的聚簇索引
  2. 索引设计

    • 二级索引会存储主键信息,通过主键查找实际数据
    • 联合索引的顺序非常重要,应将过滤性强的字段放在前面
  3. 自增 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. 表设计优化

  1. 主键选择

    • 尽量使用自增 ID 作为主键,避免使用 UUID 等随机值
    • 主键应具有单调性,便于 Region 分裂和数据均衡
  2. 索引优化

    • 只为频繁查询的字段创建索引
    • 合理设计联合索引,将过滤性强的字段放在前面
    • 避免创建过多索引,索引会影响写入性能
  3. 分区表使用

    • 对于大表(千万级以上),建议使用分区表
    • 按时间或业务维度进行分区,使查询只涉及部分分区

2. SQL 查询优化

  1. 避免全表扫描

    • 确保查询条件包含索引字段
    • 使用EXPLAIN分析查询计划,查看是否使用了索引

    -- 分析查询计划
    EXPLAIN SELECT * FROM order WHERE user_id = 123 AND created_at > '2023-01-01';

  2. 分页查询优化

    • 避免使用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;

  3. 避免大事务

    • 将大事务拆分为多个小事务
    • 长事务会占用锁资源,影响并发性能
  4. 批量操作优化

    • 使用批量插入代替单条插入
    • 批量操作的大小建议控制在 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. 集群配置优化

  1. TiKV 配置

    • 调整storage.block-cache.capacity设置合适的缓存大小
    • 根据磁盘类型调整raftstore.sync-log(SSD 可设为 false)
  2. TiDB 配置

    • 调整tidb_mem_quota_query设置查询内存限制
    • 根据 CPU 核心数调整performance.max-procs
  3. 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 数据库,正在被越来越多的企业采用。它的优势在于:

  1. 无缝迁移:兼容 MySQL 协议,现有 MySQL 应用几乎无需修改即可迁移
  2. 弹性扩展:支持水平扩展,轻松应对数据量增长
  3. 高可用性:内置高可用机制,自动故障转移,无单点故障
  4. 强一致性:支持分布式事务,保证数据一致性

TiDB 特别适合以下场景:

  • 数据量快速增长,单机 MySQL 难以承载
  • 需要弹性扩展能力,应对业务波动
  • 对高可用和数据一致性有严格要求
  • 希望避免分库分表带来的复杂性

当然,TiDB 也有其局限性,如在某些场景下性能可能不如优化良好的单机 MySQL,部署和维护相对复杂等。因此,在选择 TiDB 时,需要根据实际业务场景进行综合评估。

相关推荐
axban2 小时前
QT M/V架构开发实战:QStandardItemModel介绍
开发语言·数据库·qt
没学上了2 小时前
数据库建立库-Qt
数据库
我是zxb3 小时前
EasyExcel:快速读写Excel的工具类
数据库·oracle·excel
代码不停3 小时前
MySQL联合查询
java·数据库·mysql
沐浴露z3 小时前
Redis内存回收:过期策略与淘汰策略
数据库·redis·缓存
宴之敖者、3 小时前
MySQL——数据库基础
数据库·mysql
张3蜂3 小时前
MongoDB BI Connector 详细介绍与使用指南(手动安装方式,CentOS 7 + MongoDB 5.0.5)
数据库·mongodb·centos
春时似衿里4 小时前
jmeter配置数据库连接步骤
数据库·jmeter
喵喵爱自由4 小时前
Ubuntu 24.04 Server 版系统安装及配置
数据库·ubuntu