TiDB分布式数据库:Java应用接入、HTAP查询与MySQL迁移注意事项
TiDB分布式数据库:Java应用接入、HTAP查询与MySQL迁移注意事项
适读人群:Java后端工程师、DBA、架构师 | 阅读时长:约17分钟 | 技术栈:TiDB 7.x、Spring Boot、MyBatis-Plus
开篇故事
去年遇到了一个典型的MySQL单机撑不住的故事。客户的核心业务表数据量到了5亿行,写入QPS峰值8000,还要支持实时报表查询。单机MySQL在业务高峰期写入延迟飙升,报表查询直接影响OLTP性能,DBA苦不堪言。
当时摆在面前有几个选项:MySQL分库分表、迁移到TiDB、分离OLTP和OLAP(MySQL + ClickHouse双路写入)。
最终选了TiDB,主要原因:TiDB兼容MySQL协议,迁移改造量最小;同时TiDB的HTAP能力(TiKV + TiFlash)可以在一个系统里同时处理OLTP和OLAP,不需要维护两套数据同步。
迁移花了三个月,整体还算顺利,但也有几个坑值得记录。
一、核心问题:MySQL单机的扩展性瓶颈
MySQL主从架构到了极限之后,面临几个不可回避的问题:
二、原理深度解析
2.1 TiDB的三层架构
关键设计点:
- TiDB Server无状态,可以水平扩展
- TiKV用Raft协议保证多副本强一致性
- TiFlash实时从TiKV同步数据(延迟通常在毫秒级)
- 同一条SQL,优化器自动决定从TiKV还是TiFlash读取
2.2 HTAP查询路由
三、完整代码实现
3.1 Java连接配置(基本无需改动)
# TiDB使用MySQL协议,连接方式和MySQL完全一致
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://tidb-lb:4000/mydb?useSSL=false&serverTimezone=Asia/Shanghai&useServerPrepStmts=true&cachePrepStmts=true
username: root
password: secret
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# TiDB推荐配置
connection-test-query: SELECT 1注意:useServerPrepStmts=true在TiDB上能显著提升性能,因为TiDB支持服务端预处理。
3.2 MyBatis-Plus的TiDB适配
// MyBatis-Plus基本无需改动,但有些细节需要注意
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
// 普通查询:走TiKV
@Select("SELECT * FROM orders WHERE user_id = #{userId} ORDER BY created_at DESC LIMIT 10")
List<Order> findRecentOrders(@Param("userId") Long userId);
// OLAP查询:指定走TiFlash(通过SQL Hint)
@Select("SELECT /*+ READ_FROM_STORAGE(TIFLASH[orders]) */ " +
"region, sum(amount) as total, count(*) as cnt " +
"FROM orders " +
"WHERE created_at >= #{startDate} AND created_at < #{endDate} " +
"GROUP BY region")
List<RegionStats> getRegionStats(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
// 让优化器自动选择(推荐):通常大查询会自动选TiFlash
@Select("SELECT region, sum(amount), count(*) " +
"FROM orders " +
"WHERE created_at >= #{startDate} " +
"GROUP BY region")
List<RegionStats> getRegionStatsAuto(@Param("startDate") LocalDateTime startDate);
}3.3 HTAP混合查询示例
@Service
public class OrderAnalyticsService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 实时用户行为分析:OLAP + OLTP混合查询
* 这种场景TiDB的HTAP优势最明显
*/
public UserAnalyticsVO getUserAnalytics(Long userId) {
// OLTP查询:获取用户基本信息(走TiKV,快)
User user = userMapper.findById(userId);
// OLAP查询:统计用户历史数据(走TiFlash,不影响OLTP)
String sql = """
SELECT
count(*) as total_orders,
sum(amount) as total_amount,
avg(amount) as avg_amount,
max(created_at) as last_order_time
FROM orders
WHERE user_id = ?
""";
Map<String, Object> stats = jdbcTemplate.queryForMap(sql, userId);
return new UserAnalyticsVO(user, stats);
}
/**
* 写入操作(OLTP):走TiKV,支持事务
*/
@Transactional
public Order createOrder(OrderRequest request) {
// 这里的@Transactional和MySQL一样工作,TiDB支持标准事务
Order order = buildOrder(request);
orderMapper.insert(order);
inventoryMapper.deduct(request.getProductId(), request.getQuantity());
return order;
}
}3.4 迁移工具:使用TiDB Data Migration
# 使用TiDB DM (Data Migration) 从MySQL迁移数据
# 1. 配置源MySQL数据库
cat > source-mysql.yaml << EOF
source-id: "mysql-01"
enable-gtid: true
from:
host: "mysql-server"
user: "replication_user"
password: "secret"
port: 3306
EOF
# 2. 配置迁移任务
cat > migration-task.yaml << EOF
name: "migrate-to-tidb"
task-mode: "all" # full + incremental
target-database:
host: "tidb-server"
port: 4000
user: "root"
password: "secret"
mysql-instances:
- source-id: "mysql-01"
schema-name-mappings:
- "mydb -> mydb"
EOF
# 3. 启动迁移
tiup dmctl --master-addr dm-master:8261 operate-task start migration-task.yaml3.5 性能监控:观察查询路由
@Component
public class TiDBQueryMonitor {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 查看最近慢查询
*/
public void showSlowQueries() {
String sql = "SELECT * FROM information_schema.CLUSTER_SLOW_QUERY " +
"WHERE Time > NOW() - INTERVAL 1 HOUR " +
"ORDER BY Query_time DESC LIMIT 20";
List<Map<String, Object>> slowQueries = jdbcTemplate.queryForList(sql);
slowQueries.forEach(q -> log.info("慢查询: {}", q));
}
/**
* 查看某个表是否有TiFlash副本
*/
public void checkTiFlashReplica(String dbName, String tableName) {
String sql = "SELECT * FROM information_schema.TIFLASH_REPLICA " +
"WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, dbName, tableName);
if (result.isEmpty()) {
log.warn("表{}没有TiFlash副本,OLAP查询会走TiKV", tableName);
} else {
log.info("TiFlash副本状态: {}", result.get(0));
}
}
/**
* 为表创建TiFlash副本(OLAP优化)
*/
public void createTiFlashReplica(String tableName, int replicaCount) {
String sql = "ALTER TABLE " + tableName + " SET TIFLASH REPLICA " + replicaCount;
jdbcTemplate.execute(sql);
log.info("已为表{}创建{}个TiFlash副本", tableName, replicaCount);
}
}四、工程实践:MySQL到TiDB迁移要点
4.1 SQL兼容性检查
TiDB高度兼容MySQL,但有一些差异需要提前检查:
-- TiDB不支持的MySQL特性(常见):
-- 1. MyISAM引擎(TiDB只支持InnoDB语义)
-- 2. 全文索引(FULLTEXT INDEX)
-- 3. 空间索引(SPATIAL INDEX)
-- 4. 存储过程和函数(部分支持)
-- 5. 某些MySQL系统变量
-- 检查是否有全文索引
SELECT TABLE_NAME, INDEX_NAME, INDEX_TYPE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'mydb' AND INDEX_TYPE = 'FULLTEXT';
-- 检查使用MyISAM的表
SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'mydb' AND ENGINE = 'MyISAM';4.2 自增ID的差异
这是最容易踩的坑!
-- MySQL的AUTO_INCREMENT:严格连续递增,并发插入可能有间隙
-- TiDB的AUTO_INCREMENT:分布式环境下,ID不严格连续
-- TiDB推荐使用:AUTO_RANDOM(基于雪花算法,分布式友好)
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_RANDOM, -- 替代AUTO_INCREMENT
-- ...
PRIMARY KEY (id)
) SHARD_ROW_ID_BITS = 4;
-- 如果业务依赖严格连续ID,需要评估改造
-- 大多数情况下,ID只需要唯一,不需要严格连续五、踩坑实录
坑一:AUTO_INCREMENT在TiDB中的ID跳跃
我们迁移后,发现生成的ID不连续,有大量跳跃。业务代码有地方依赖"最新ID = max(id)"来判断最新记录,导致逻辑错误。
原因:TiDB的自增ID在分布式场景下,每个TiDB Server缓存一批ID(默认30000个),分配后顺序不严格。
解决方案:业务代码不依赖ID连续性,用created_at时间排序替代ID排序。
坑二:事务大小限制
TiDB默认单个事务最大100MB(可配置),如果有批量导入的大事务,会直接失败。
// 错误:一次性提交几万行数据在一个事务
@Transactional
public void importData(List<Order> orders) {
orderMapper.batchInsert(orders); // 如果orders超过几万行,可能超过事务大小限制
}
// 正确:分批提交
public void importDataSafely(List<Order> orders) {
List<List<Order>> batches = ListUtils.partition(orders, 1000); // 每批1000条
for (List<Order> batch : batches) {
importBatch(batch);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void importBatch(List<Order> batch) {
orderMapper.batchInsert(batch);
}坑三:TiFlash副本同步延迟影响查询一致性
TiFlash副本通过Raft Learner从TiKV同步数据,通常延迟在毫秒级,但偶尔会有秒级延迟。如果你的OLAP查询需要强一致性(刚写入就能查到),需要特别处理。
// 强一致性OLAP查询:读TiFlash但等待数据一致
// 通过SQL Hint设置
@Select("SELECT /*+ READ_FROM_STORAGE(TIFLASH[orders]) */ /*+ TIDB_CONSISTENT_READ */ ...")
// 但这会增加延迟,根据实际需求选择坑四:连接数过多导致TiDB OOM
TiDB Server的内存会因为大量并发查询而快速消耗。特别是复杂的OLAP查询,每条查询可能消耗几十到几百MB内存。
解决方案:限制应用连接池大小,对OLAP查询设置单独的连接池并限制大小。
六、总结与个人判断
TiDB是一个技术上非常成熟的分布式数据库,MySQL协议兼容性做得很好,迁移改造量相对较小。HTAP能力让你不需要维护独立的OLAP数据库,这对中等规模的企业用户很有吸引力。
但我要说几点实际感受:
第一,运维复杂度远高于MySQL。TiDB、TiKV、TiFlash、PD,多组件协同,版本升级、故障排查都需要专业知识。没有专职DBA的小团队,上TiDB要谨慎。
第二,资源消耗大。最小生产集群需要3 TiDB + 3 TiKV + 2 PD,如果要TiFlash,再加2-3个节点。硬件成本不低。
第三,真的适合MySQL分库分表迁移成本太高的场景。如果你的MySQL还扛得住,或者分库分表已经做好了,TiDB的收益有限。
我的建议:单机MySQL超过2TB、写入QPS超过5000、或者分库分表管理成本无法接受时,再认真评估TiDB。
