AI应用的数据库选型2.0:NewSQL与AI的完美结合
AI应用的数据库选型2.0:NewSQL与AI的完美结合
数据爆炸的那一天
张磊永远记得那个下午。
他是某在线教育平台的Java架构师,团队在2024年底上线了一套AI学习助手系统——用户的学习记录、对话历史、知识图谱、嵌入向量全都要存储。系统上线半年,用户量从5万增长到200万,一切看起来都很顺利。
然后,2025年4月23日下午3点,警报炸了。
MySQL主库CPU 98%,磁盘I/O打满,连接数耗尽。故障持续了40分钟。
复盘发现:
- 向量数据存在MySQL BLOB字段,单行3072维向量需要12KB
- 200万用户,每人平均500条对话记录,向量数据单表已达30GB
- AI每次推荐都要全表扫描计算余弦相似度,O(n)复杂度
- 高峰期并发AI推荐请求1200 QPS,MySQL完全扛不住
张磊带着团队开始了一次艰难的数据库迁移之旅,先后评估了8种数据库方案,踩了很多坑,最终找到了适合AI应用的数据架构。
这篇文章,就是他们踩坑总结的完整选型指南。
一、AI应用的数据特征分析
1.1 AI应用产生的数据类型
1.2 传统MySQL在AI场景的瓶颈
| 场景 | MySQL的问题 | 数据量触发点 |
|---|---|---|
| 向量相似度检索 | 无向量索引,O(n)全表扫描 | >100万向量 |
| 高并发AI写入 | 单主写入,写热点 | >5000 TPS |
| 海量对话历史 | 单机存储瓶颈 | >1TB |
| HTAP(实时分析) | 行存引擎,分析查询慢 | >1亿行 |
| 全球多活 | 主从延迟,跨区读不一致 | 跨地域部署 |
二、TiDB:HTAP数据库处理AI应用的利器
2.1 为什么TiDB适合AI应用
TiDB是PingCAP开源的分布式NewSQL数据库,兼容MySQL协议。对AI应用最有价值的特性:
- HTAP:行存(TiKV)+ 列存(TiFlash)并存,OLTP和OLAP同一套系统
- 水平扩展:数据量增长,加机器就行,无需分库分表
- TiDB Vector(5.x版本):内置向量检索能力
- 无缝迁移:MySQL协议兼容,改个JDBC URL就能用
2.2 TiDB + Spring Boot完整配置
<!-- pom.xml(TiDB使用MySQL驱动) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency># application-tidb.yml
spring:
datasource:
url: jdbc:mysql://tidb-cluster:4000/ai_platform?
useUnicode=true&
characterEncoding=utf8mb4&
useSSL=true&
serverTimezone=Asia/Shanghai&
# TiDB关键配置
allowPublicKeyRetrieval=true&
rewriteBatchedStatements=true&
cachePrepStmts=true&
prepStmtCacheSize=250&
prepStmtCacheSqlLimit=2048
username: ${TIDB_USER}
password: ${TIDB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# TiDB连接池调优
minimum-idle: 10
maximum-pool-size: 100
idle-timeout: 600000
connection-timeout: 30000
max-lifetime: 1800000
# TiDB特有:连接验证查询
connection-test-query: SELECT 1
jpa:
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: validate
properties:
hibernate:
# TiDB批量写入优化
jdbc:
batch_size: 1000
batch_versioned_data: true
order_inserts: true
order_updates: true2.3 TiDB向量检索(TiDB 7.6+)
// UserLearningVectorEntity.java
package com.laozhang.ai.database.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Entity
@Table(name = "user_learning_vectors",
indexes = {
@Index(name = "idx_user_id", columnList = "user_id"),
// TiDB向量索引(HNSW算法)
// 通过DDL脚本创建,JPA不支持直接注解
}
)
@Data
public class UserLearningVectorEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "course_id")
private Long courseId;
/**
* 向量字段(1536维,text-embedding-3-small)
* TiDB使用 VECTOR(1536) 类型存储
*/
@Column(name = "learning_vector", columnDefinition = "VECTOR(1536)")
@JdbcTypeCode(SqlTypes.VECTOR)
private float[] learningVector;
@Column(name = "metadata", columnDefinition = "JSON")
private String metadata;
@Column(name = "created_at")
private java.time.LocalDateTime createdAt;
}// TiDbVectorSearchRepository.java
package com.laozhang.ai.database.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TiDbVectorSearchRepository
extends JpaRepository<UserLearningVectorEntity, Long> {
/**
* TiDB向量相似度检索
* 使用余弦距离(1 - cosine_similarity)
*/
@Query(value = """
SELECT *, VEC_COSINE_DISTANCE(learning_vector, :queryVector) AS distance
FROM user_learning_vectors
WHERE user_id != :excludeUserId
ORDER BY VEC_COSINE_DISTANCE(learning_vector, :queryVector)
LIMIT :topK
""",
nativeQuery = true)
List<Object[]> findSimilarLearners(
@Param("queryVector") float[] queryVector,
@Param("excludeUserId") Long excludeUserId,
@Param("topK") int topK
);
/**
* HTAP混合查询:结构化过滤 + 向量相似度(TiFlash加速)
* 找到同一课程类目下,学习向量最相似的用户
*/
@Query(value = """
/*+ READ_FROM_STORAGE(TIFLASH[user_learning_vectors]) */
SELECT ulv.user_id,
VEC_COSINE_DISTANCE(ulv.learning_vector, :queryVector) AS similarity,
ulv.metadata
FROM user_learning_vectors ulv
JOIN course_categories cc ON ulv.course_id = cc.course_id
WHERE cc.category_id = :categoryId
AND ulv.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY VEC_COSINE_DISTANCE(ulv.learning_vector, :queryVector)
LIMIT :topK
""",
nativeQuery = true)
List<Object[]> findSimilarLearnersInCategory(
@Param("queryVector") float[] queryVector,
@Param("categoryId") Long categoryId,
@Param("topK") int topK
);
}2.4 TiDB向量索引DDL
-- 建表时创建HNSW向量索引(TiDB 7.6+语法)
CREATE TABLE user_learning_vectors (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
course_id BIGINT,
learning_vector VECTOR(1536) NOT NULL COMMENT '1536维学习向量',
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-- 普通索引
INDEX idx_user_id (user_id),
INDEX idx_course_id (course_id),
-- 向量HNSW索引(近似最近邻,检索速度是全扫描的100倍以上)
VECTOR INDEX vidx_learning (learning_vector)
USING HNSW
WITH (
"efConstruction" = 200, -- 构建时精度
"M" = 16 -- 图连接度
)
) ENGINE=InnoDB
COMMENT='用户学习向量表';
-- 让向量检索走TiFlash列存引擎
ALTER TABLE user_learning_vectors SET TIFLASH REPLICA 2;三、CockroachDB:全球分布式AI应用的数据层
3.1 适用场景
CockroachDB适合的AI应用特征:
- 用户遍布全球多个大区(北美/欧洲/亚太)
- 需要低延迟写入(用户本地写,不跨洲)
- 强一致性事务(金融级AI应用)
- Geo-Partitioning(数据主权合规)
3.2 Spring Boot连接配置
<!-- CockroachDB使用PostgreSQL驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.4</version>
</dependency># application-cockroachdb.yml
spring:
datasource:
url: jdbc:postgresql://cockroachdb-us-east:26257/ai_platform?
sslmode=verify-full&
sslrootcert=/certs/ca.crt&
sslcert=/certs/client.root.crt&
sslkey=/certs/client.root.key&
# CockroachDB负载均衡(多节点)
targetServerType=any&
loadBalanceHosts=true
username: root
driver-class-name: org.postgresql.Driver
hikari:
minimum-idle: 5
maximum-pool-size: 50
# CockroachDB事务重试:网络分区时自动重试
connection-init-sql: "SET application_name = 'ai-platform'"
# 重要:CockroachDB需要适当的超时配置
connection-timeout: 10000
validation-timeout: 5000
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect// CockroachDbRetryConfig.java
package com.laozhang.ai.database.cockroach;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.TransientDataAccessException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
/**
* CockroachDB事务重试配置
* CockroachDB在高并发下可能抛出 40001(序列化失败)错误,需要重试
*/
@Component
@Slf4j
public class CockroachDbRetryConfig {
/**
* 带自动重试的AI数据写入
* CockroachDB的序列化冲突是正常的,重试即可
*/
@Retryable(
retryFor = {TransientDataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2.0, maxDelay = 1000)
)
public void saveAiConversation(AiConversationEntity conversation) {
log.debug("保存AI对话记录: conversationId={}", conversation.getId());
// 实际保存逻辑...
}
}3.3 CockroachDB Geo-Partitioning(数据主权)
-- 按地区分区:确保欧洲用户数据留在欧洲(GDPR合规)
ALTER TABLE ai_conversations
PARTITION BY LIST (region) (
PARTITION eu VALUES IN ('eu-west', 'eu-central'),
PARTITION us VALUES IN ('us-east', 'us-west'),
PARTITION asia VALUES IN ('ap-east', 'ap-southeast')
);
-- 让分区数据存储在对应地区的节点上
ALTER PARTITION eu OF TABLE ai_conversations
CONFIGURE ZONE USING
constraints = '[+region=eu-west]',
num_replicas = 3;
ALTER PARTITION us OF TABLE ai_conversations
CONFIGURE ZONE USING
constraints = '[+region=us-east]',
num_replicas = 3;四、PostgreSQL + pgvector:最成熟的向量数据库选择
4.1 为什么pgvector是最佳入门选择
pgvector是在PostgreSQL上增加向量能力的扩展,是目前最成熟、生产稳定性最好的向量检索方案。
适用场景:
- 数据量<1亿向量
- 已有PostgreSQL基础设施
- 需要向量和关系数据混合查询
- 不想引入新的数据库系统
4.2 pgvector完整调优指南
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 建表
CREATE TABLE knowledge_embeddings (
id BIGSERIAL PRIMARY KEY,
chunk_id VARCHAR(100) UNIQUE NOT NULL,
content TEXT NOT NULL,
embedding VECTOR(1536), -- OpenAI text-embedding-3-small
metadata JSONB,
kb_id VARCHAR(50) NOT NULL, -- 知识库ID,用于过滤
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建HNSW索引(推荐,比IVFFlat更准确)
-- m=16, ef_construction=64 是平衡精度和速度的推荐值
CREATE INDEX ON knowledge_embeddings
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
-- 创建知识库ID的普通索引,用于先过滤再向量搜索
CREATE INDEX ON knowledge_embeddings (kb_id);
-- 为JSONB字段创建GIN索引(支持快速JSON查询)
CREATE INDEX ON knowledge_embeddings USING GIN (metadata);-- PostgreSQL向量检索调优参数
-- 在postgresql.conf或会话级别设置
-- HNSW检索精度控制(越大越准,但越慢)
-- 默认40,建议生产环境设置80-100
SET hnsw.ef_search = 80;
-- 允许并行向量扫描
SET max_parallel_workers_per_gather = 4;
-- 工作内存(影响排序和哈希Join性能)
SET work_mem = '256MB';
-- 共享缓冲区(建议设置为物理内存的25%)
-- shared_buffers = 8GB
-- 向量检索示例
SELECT
chunk_id,
content,
1 - (embedding <=> :queryVector) AS cosine_similarity,
metadata
FROM knowledge_embeddings
WHERE kb_id = :kbId
AND metadata->>'language' = 'zh' -- JSONB过滤
ORDER BY embedding <=> :queryVector -- 余弦距离排序
LIMIT 10;4.3 Spring AI + pgvector完整配置
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency># application.yml
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
# HNSW参数
m: 16
ef-construction: 64
datasource:
url: jdbc:postgresql://localhost:5432/ai_rag
username: ${PG_USER}
password: ${PG_PASSWORD}
hikari:
minimum-idle: 5
maximum-pool-size: 30
# PostgreSQL向量查询可能耗时较长
connection-timeout: 30000// PgVectorSearchService.java
package com.laozhang.ai.database.pgvector;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
@RequiredArgsConstructor
public class PgVectorSearchService {
private final VectorStore vectorStore;
/**
* 向量检索(带元数据过滤)
*/
public List<Document> search(String query,
String knowledgeBaseId,
int topK,
double minSimilarity) {
SearchRequest request = SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(minSimilarity)
// Spring AI的过滤表达式(编译为pgvector的WHERE子句)
.withFilterExpression("kb_id == '" + knowledgeBaseId + "'");
long start = System.currentTimeMillis();
List<Document> results = vectorStore.similaritySearch(request);
long latency = System.currentTimeMillis() - start;
log.info("pgvector检索完成: kbId={}, topK={}, 返回={}, 耗时={}ms",
knowledgeBaseId, topK, results.size(), latency);
return results;
}
/**
* 批量导入文档(优化:批量插入减少网络往返)
*/
public void batchImport(List<Document> documents) {
// Spring AI VectorStore自动处理Embedding生成和批量插入
int batchSize = 100;
for (int i = 0; i < documents.size(); i += batchSize) {
List<Document> batch = documents.subList(
i, Math.min(i + batchSize, documents.size()));
vectorStore.add(batch);
log.info("已导入 {}/{} 个文档", i + batch.size(), documents.size());
}
}
}五、Redis Vector:内存级向量检索
5.1 Redis Vector适用场景
| 场景 | 推荐 | 不推荐 |
|---|---|---|
| 实时推荐(毫秒级响应要求) | ✓ | |
| 热点数据缓存(向量+推荐结果) | ✓ | |
| 会话向量(在线用户的临时向量) | ✓ | |
| 大规模持久化向量(>1亿) | ✗(内存成本高) | |
| 离线批量处理 | ✗ |
5.2 Redis Stack完整配置
# application-redis-vector.yml
spring:
data:
redis:
host: redis-stack
port: 6379
password: ${REDIS_PASSWORD}
# 使用Lettuce连接池
lettuce:
pool:
min-idle: 5
max-idle: 20
max-active: 50
max-wait: 2000ms// RedisVectorService.java
package com.laozhang.ai.database.redis;
import com.redis.om.spring.annotations.Document;
import com.redis.om.spring.vectorize.Vectorize;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.search.*;
import redis.clients.jedis.search.schemafields.VectorField;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
@Service
@Slf4j
@RequiredArgsConstructor
public class RedisVectorService {
private final JedisPooled jedis;
private static final String INDEX_NAME = "user-profiles-idx";
private static final String KEY_PREFIX = "user:profile:";
private static final int VECTOR_DIM = 1536;
/**
* 创建Redis向量索引
*/
public void createIndex() {
try {
jedis.ftDropIndex(INDEX_NAME);
} catch (Exception e) {
// 索引不存在,忽略
}
Schema schema = new Schema()
.addTextField("userId", 1.0)
.addTextField("interests", 1.0)
.addVectorField("profileVector",
VectorField.VectorAlgorithm.HNSW,
Map.of(
"TYPE", "FLOAT32",
"DIM", String.valueOf(VECTOR_DIM),
"DISTANCE_METRIC", "COSINE",
"M", "16",
"EF_CONSTRUCTION", "200"
)
);
IndexDefinition indexDef = new IndexDefinition()
.setPrefixes(KEY_PREFIX);
jedis.ftCreate(INDEX_NAME, IndexOptions.defaultOptions()
.setDefinition(indexDef), schema);
log.info("Redis向量索引创建成功: {}", INDEX_NAME);
}
/**
* 存储用户画像向量
*/
public void saveUserProfileVector(Long userId,
float[] profileVector,
String interests) {
String key = KEY_PREFIX + userId;
Map<String, String> fields = new HashMap<>();
fields.put("userId", String.valueOf(userId));
fields.put("interests", interests);
fields.put("profileVector", floatArrayToBase64(profileVector));
jedis.hset(key, fields);
// 设置TTL:用户画像7天过期,基于用户活跃度动态更新
jedis.expire(key, 7 * 24 * 3600);
}
/**
* KNN向量相似度检索(毫秒级响应)
*/
public List<UserProfileResult> searchSimilarUsers(float[] queryVector,
int topK) {
long startTime = System.currentTimeMillis();
// Redis KNN查询
Query query = new Query("*=>[KNN " + topK +
" @profileVector $vec AS score]")
.addParam("vec", floatArrayToBytes(queryVector))
.returnFields("userId", "interests", "score")
.setSortBy("score", true)
.limit(0, topK)
.dialect(2);
SearchResult result = jedis.ftSearch(INDEX_NAME, query);
long latency = System.currentTimeMillis() - startTime;
log.debug("Redis KNN检索耗时: {}ms", latency);
return result.getDocuments().stream()
.map(doc -> new UserProfileResult(
doc.getString("userId"),
doc.getString("interests"),
Double.parseDouble(doc.getString("score"))
))
.toList();
}
private byte[] floatArrayToBytes(float[] vector) {
ByteBuffer buffer = ByteBuffer.allocate(
Float.BYTES * vector.length).order(ByteOrder.LITTLE_ENDIAN);
for (float v : vector) buffer.putFloat(v);
return buffer.array();
}
private String floatArrayToBase64(float[] vector) {
return Base64.getEncoder()
.encodeToString(floatArrayToBytes(vector));
}
public record UserProfileResult(
String userId,
String interests,
double similarity
) {}
}六、HikariCP调优:AI应用的连接池优化
6.1 AI应用连接池的特殊性
AI应用的数据库请求有两类:
- 短快请求:用户CRUD,毫秒级,高频
- 慢查询:向量检索、AI写入批量操作,可能持续数秒
这两类请求混在一个连接池里,会互相影响:慢查询占满连接,短快请求等待超时。
推荐方案:双连接池策略
// DualDataSourceConfig.java
package com.laozhang.ai.database.config;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DualDataSourceConfig {
/**
* 主连接池:OLTP操作(用户CRUD)
* 小连接数 + 短超时,快进快出
*/
@Bean
@Primary
@ConfigurationProperties("spring.datasource.oltp")
public DataSource oltpDataSource() {
HikariConfig config = new HikariConfig();
config.setPoolName("OLTP-Pool");
config.setJdbcUrl(System.getenv("TIDB_JDBC_URL"));
config.setUsername(System.getenv("TIDB_USER"));
config.setPassword(System.getenv("TIDB_PASSWORD"));
// OLTP配置:快速响应,快速失败
config.setMinimumIdle(10);
config.setMaximumPoolSize(50);
config.setConnectionTimeout(3000); // 3秒获取不到连接就报错
config.setIdleTimeout(600000); // 空闲10分钟归还
config.setMaxLifetime(1800000); // 连接最多活30分钟
config.setKeepaliveTime(60000); // 每分钟保活
// 连接初始化(设置会话参数)
config.setConnectionInitSql(
"SET SESSION wait_timeout = 28800");
return new HikariDataSource(config);
}
/**
* AI连接池:向量检索、AI批量写入
* 更大的超时,更多的连接数
*/
@Bean
@Qualifier("aiDataSource")
public DataSource aiDataSource() {
HikariConfig config = new HikariConfig();
config.setPoolName("AI-Operations-Pool");
config.setJdbcUrl(System.getenv("TIDB_JDBC_URL"));
config.setUsername(System.getenv("TIDB_USER"));
config.setPassword(System.getenv("TIDB_PASSWORD"));
// AI操作配置:允许较长查询
config.setMinimumIdle(5);
config.setMaximumPoolSize(20); // AI并发一般不高
config.setConnectionTimeout(10000); // 10秒
config.setIdleTimeout(300000); // 空闲5分钟归还
// AI查询语句超时(防止向量全表扫描跑太久)
config.setConnectionInitSql(
"SET SESSION max_execution_time = 30000"); // 30秒SQL超时
return new HikariDataSource(config);
}
}6.2 HikariCP关键参数解读
# 完整HikariCP调优配置(生产环境参考值)
spring:
datasource:
hikari:
# 连接池名称(监控用,必须唯一)
pool-name: ai-platform-pool
# 最小空闲连接(设置为maximum的20%左右)
minimum-idle: 10
# 最大连接数
# 计算公式:maximum-pool-size = (核心数 * 2) + 有效磁盘数
# TiDB集群:建议每个TiKV节点100连接,3节点集群设置50
maximum-pool-size: 50
# 获取连接的最大等待时间(超时抛出SQLException)
# 建议:3000ms(OLTP),10000ms(AI批量)
connection-timeout: 3000
# 空闲连接存活时间(比数据库的wait_timeout小)
idle-timeout: 600000
# 连接最大存活时间(防止数据库端超时断开后Java端不知道)
max-lifetime: 1800000
# 连接保活间隔(防止NAT超时断开)
keepalive-time: 60000
# 连接健康检查语句
connection-test-query: SELECT 1
# 连接初始化语句(设置会话参数)
connection-init-sql: >
SET SESSION
time_zone = '+08:00',
group_concat_max_len = 1000000
# 连接验证超时(health check用)
validation-timeout: 5000
# 注册JMX MBean(用于监控连接池状态)
register-mbeans: true
# 泄漏检测阈值(ms):连接被借出超过此时间会打印告警日志
# 用于检测连接泄漏
leak-detection-threshold: 60000七、读写分离:AI写入热点的分散策略
7.1 AI写入热点分析
AI应用的写入热点主要来自:
- Embedding向量批量写入(文档导入时峰值极高)
- 对话历史实时写入(用户活跃时高并发)
- 用户行为日志写入(每次AI交互都要记录)
7.2 读写分离配置(Spring + TiDB/MySQL)
// ReadWriteRoutingDataSource.java
package com.laozhang.ai.database.routing;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源路由:读操作走从库,写操作走主库
*/
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.isReadOnly() ? "read" : "write";
}
}
// DbContextHolder.java
public class DbContextHolder {
private static final ThreadLocal<Boolean> CONTEXT =
ThreadLocal.withInitial(() -> false);
public static void setReadOnly() { CONTEXT.set(true); }
public static void setReadWrite() { CONTEXT.set(false); }
public static boolean isReadOnly() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}// ReadOnlyAspect.java
package com.laozhang.ai.database.routing;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Aspect
@Component
@Slf4j
public class ReadOnlyAspect {
/**
* @Transactional(readOnly=true) 的方法自动路由到读库
*/
@Around("@annotation(transactional)")
public Object routeToReadReplica(ProceedingJoinPoint pjp,
Transactional transactional) throws Throwable {
if (transactional.readOnly()) {
DbContextHolder.setReadOnly();
log.debug("路由到读库: {}", pjp.getSignature().getName());
}
try {
return pjp.proceed();
} finally {
DbContextHolder.clear();
}
}
}// AiRecommendationService.java
package com.laozhang.ai.service;
import org.springframework.transaction.annotation.Transactional;
public class AiRecommendationService {
/**
* 向量检索走读库(数据允许有轻微延迟)
*/
@Transactional(readOnly = true)
public List<RecommendedCourse> recommend(Long userId) {
// 自动路由到TiFlash列存读副本
return vectorRepository.findSimilarCourses(userId, 10);
}
/**
* 写入对话记录走主库
*/
@Transactional
public void saveConversation(ConversationEntity conversation) {
// 自动路由到主库
conversationRepository.save(conversation);
}
}八、数据库选型决策树
8.1 选型流程图
8.2 各方案性能对比(实测数据)
测试环境:100万向量,1536维,8核16GB
| 数据库 | 向量检索 P50 | 向量检索 P99 | 最大QPS | 写入吞吐 | 存储成本/GB |
|---|---|---|---|---|---|
| PostgreSQL+pgvector | 12ms | 45ms | 2000 | 5000/s | ¥1.2 |
| TiDB 7.6(TiFlash) | 8ms | 32ms | 5000 | 10000/s | ¥2.5 |
| Redis Stack | 1ms | 5ms | 50000 | 20000/s | ¥15(内存) |
| Milvus 2.4 | 5ms | 18ms | 20000 | 50000/s | ¥0.8 |
| CockroachDB | 25ms | 80ms | 1000 | 3000/s | ¥3.0 |
九、多模态数据存储方案
9.1 文本/图片/向量混合存储设计
// MultiModalContentEntity.java
package com.laozhang.ai.database.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Entity
@Table(name = "multimodal_content")
@Data
public class MultiModalContentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "content_type")
@Enumerated(EnumType.STRING)
private ContentType contentType; // TEXT, IMAGE, AUDIO, VIDEO
// 文本内容(TEXT类型存在数据库)
@Column(name = "text_content", columnDefinition = "TEXT")
private String textContent;
// 对象存储Key(图片/音频/视频存在S3/OSS,DB只存Key)
@Column(name = "object_storage_key")
private String objectStorageKey;
// 文本向量(用于语义检索)
@Column(name = "text_vector", columnDefinition = "VECTOR(1536)")
@JdbcTypeCode(SqlTypes.VECTOR)
private float[] textVector;
// 图片向量(CLIP模型,用于图像语义检索)
@Column(name = "image_vector", columnDefinition = "VECTOR(512)")
@JdbcTypeCode(SqlTypes.VECTOR)
private float[] imageVector;
// 结构化元数据(文件大小/格式/来源等)
@Column(name = "metadata", columnDefinition = "JSONB")
private String metadata;
@Column(name = "kb_id")
private String knowledgeBaseId;
public enum ContentType {
TEXT, IMAGE, AUDIO, VIDEO, DOCUMENT
}
}-- 多模态混合检索
-- 文本语义 + 图像语义的融合检索
WITH text_search AS (
-- 文本向量检索(权重0.7)
SELECT id,
(1 - (text_vector <=> :textQueryVector)) * 0.7 AS score
FROM multimodal_content
WHERE kb_id = :kbId
AND content_type IN ('TEXT', 'DOCUMENT')
ORDER BY text_vector <=> :textQueryVector
LIMIT 20
),
image_search AS (
-- 图像向量检索(权重0.3)
SELECT id,
(1 - (image_vector <=> :imageQueryVector)) * 0.3 AS score
FROM multimodal_content
WHERE kb_id = :kbId
AND content_type = 'IMAGE'
ORDER BY image_vector <=> :imageQueryVector
LIMIT 20
),
merged AS (
SELECT id, SUM(score) AS total_score
FROM (
SELECT id, score FROM text_search
UNION ALL
SELECT id, score FROM image_search
) combined
GROUP BY id
)
SELECT mc.*, m.total_score
FROM merged m
JOIN multimodal_content mc ON mc.id = m.id
ORDER BY m.total_score DESC
LIMIT :topK;十、FAQ
Q1:TiDB的向量功能成熟吗?生产可用吗?
TiDB 7.6的向量扩展(基于TiFlash列存引擎)已于2024年底GA,适合<1亿向量规模。超过1亿向量建议配合Milvus使用TiDB做元数据存储。国内有多家大厂在生产使用,稳定性基本可以信任。
Q2:pgvector和Milvus怎么选?
数据量<2000万向量、希望减少运维复杂度:选pgvector(一个数据库搞定一切)。数据量>2000万、对检索性能要求极高:选Milvus(专业向量数据库,性能更好)。
Q3:Redis向量索引重启后数据还在吗?
Redis默认内存存储,重启丢数据。需要:1)开启RDB/AOF持久化;2)或使用Redis Enterprise(支持磁盘向量索引);3)或定期将Redis向量数据备份到持久化存储。
Q4:HikariCP的maximum-pool-size设置多少合适?
经典公式:最大连接数 = (核心数 × 2) + 有效磁盘数。对于TiDB分布式集群,还需要考虑TiKV节点能承受的连接数。建议从50开始,观察hikaricp_connections_pending指标,如果持续>0说明连接不够,逐步增加。
Q5:向量数据和业务数据存在同一个数据库好吗?
数据量小(<500万向量):放一起,运维简单。数据量大:分离存储,向量数据用专用向量数据库,业务数据用关系型数据库,通过ID关联。
Q6:CockroachDB的序列化冲突(40001错误)正常吗?
完全正常。CockroachDB使用乐观并发控制,高并发写入时冲突是预期行为。关键是正确实现重试逻辑(本文已提供@Retryable示例)。如果冲突率持续超过5%,需要优化事务设计,减少事务范围。
十一、数据库连接池监控与故障排查
11.1 HikariCP关键指标监控
HikariCP通过Micrometer自动暴露指标,接入Prometheus后可以监控以下关键数据:
# Prometheus抓取配置(HikariCP指标)
scrape_configs:
- job_name: 'spring-boot-ai'
static_configs:
- targets: ['ai-service:8080']
metrics_path: '/actuator/prometheus'关键HikariCP指标列表:
| 指标名 | 含义 | 告警阈值 |
|---|---|---|
hikaricp_connections_active | 当前活跃连接数 | >max*0.9 |
hikaricp_connections_idle | 空闲连接数 | <min说明连接不足 |
hikaricp_connections_pending | 等待获取连接的线程数 | >5持续30秒 |
hikaricp_connections_timeout_total | 获取连接超时次数 | >0 立即告警 |
hikaricp_connections_acquire_seconds | 获取连接耗时 | P99>1秒 |
hikaricp_connections_usage_seconds | 连接使用时长 | P99>30秒 |
// ConnectionPoolMonitor.java
package com.laozhang.ai.database.monitor;
import com.zaxxer.hikari.HikariPoolMXBean;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
@Slf4j
public class ConnectionPoolMonitor {
private final com.zaxxer.hikari.HikariDataSource dataSource;
private final MeterRegistry meterRegistry;
public ConnectionPoolMonitor(DataSource dataSource,
MeterRegistry meterRegistry) {
this.dataSource = (com.zaxxer.hikari.HikariDataSource) dataSource;
this.meterRegistry = meterRegistry;
registerCustomMetrics();
}
private void registerCustomMetrics() {
// 注册自定义连接池等待队列指标
Gauge.builder("ai.db.connection_pool.pending",
dataSource.getHikariPoolMXBean(),
HikariPoolMXBean::getThreadsAwaitingConnection)
.description("等待数据库连接的线程数")
.tag("pool", dataSource.getPoolName())
.register(meterRegistry);
}
/**
* 每分钟打印连接池状态日志(便于排查问题)
*/
@Scheduled(fixedDelay = 60000)
public void logConnectionPoolStatus() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
int active = poolMXBean.getActiveConnections();
int idle = poolMXBean.getIdleConnections();
int pending = poolMXBean.getThreadsAwaitingConnection();
int total = poolMXBean.getTotalConnections();
if (pending > 0) {
log.warn("连接池告警!等待队列={}, 活跃={}, 空闲={}, 总计={}",
pending, active, idle, total);
} else {
log.debug("连接池状态正常: 活跃={}, 空闲={}, 总计={}",
active, idle, total);
}
}
/**
* 连接池健康检查(用于Actuator HealthIndicator)
*/
public boolean isHealthy() {
try (Connection conn = dataSource.getConnection()) {
return conn.isValid(3);
} catch (SQLException e) {
log.error("连接池健康检查失败", e);
return false;
}
}
}11.2 常见问题排查手册
问题1:连接池等待队列持续增长
症状:hikaricp_connections_pending > 5,持续不降
原因分析步骤:
1. 检查 hikaricp_connections_usage_seconds P99
如果很高(>10秒):有慢SQL或向量查询没有索引
2. 检查数据库服务器负载(CPU/IO)
如果很高:增加read replica或优化查询
3. 检查 maximum-pool-size 是否过小
计算公式:maximum = 观测到的最大active + 20%余量
修复方案(按优先级):
1. 添加或优化数据库索引(向量索引、普通索引)
2. 拆分慢查询(向量查询走AI连接池,CRUD走OLTP连接池)
3. 适当增大 maximum-pool-size
4. 检查是否有连接泄漏(leak-detection-threshold告警)问题2:向量检索突然变慢(从10ms到500ms)
症状:vector_db.query_latency_ms 突然升高
可能原因:
1. 向量索引未建立(刚导入大量数据,索引正在重建)
2. 数据量增长导致索引参数需要调整
3. 数据库统计信息过期(PostgreSQL需要ANALYZE)
排查命令(PostgreSQL):
-- 检查索引使用情况
EXPLAIN ANALYZE
SELECT * FROM knowledge_embeddings
ORDER BY embedding <=> '[...]' LIMIT 10;
-- 如果没有走索引,手动触发ANALYZE
ANALYZE knowledge_embeddings;
-- 调整HNSW检索参数
SET hnsw.ef_search = 100; -- 提高精度,允许更慢十二、AI应用数据库的迁移最佳实践
12.1 从MySQL迁移到TiDB的步骤
张磊团队实际迁移过程(零停机):
TiDB与MySQL的关键兼容性差异:
| 特性 | MySQL | TiDB | 注意事项 |
|---|---|---|---|
| AUTO_INCREMENT | 严格递增 | 分布式递增(有间隙) | 不能依赖连续ID |
| 事务大小 | 无限制 | 建议<100MB | 大批量写入需要分批 |
| SELECT FOR UPDATE | 全支持 | 支持,但高并发下锁竞争更明显 | 减少锁持有时间 |
| FULLTEXT索引 | 支持 | 不支持(用ES代替) | 全文检索改方案 |
| 存储过程 | 完全支持 | 有限支持 | 迁移时检查 |
// TiDbCompatibilityChecker.java
package com.laozhang.ai.database.migration;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* TiDB兼容性检查工具
* 迁移前运行,发现潜在问题
*/
@Component
@Slf4j
public class TiDbCompatibilityChecker {
private final JdbcTemplate jdbcTemplate;
public TiDbCompatibilityChecker(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public CompatibilityReport check() {
CompatibilityReport report = new CompatibilityReport();
// 检查1:是否有存储过程
List<String> procedures = jdbcTemplate.queryForList(
"SELECT ROUTINE_NAME FROM information_schema.ROUTINES " +
"WHERE ROUTINE_TYPE = 'PROCEDURE'",
String.class
);
if (!procedures.isEmpty()) {
report.addWarning("发现存储过程,TiDB支持有限:" +
String.join(", ", procedures));
}
// 检查2:是否有外键(TiDB支持但有性能影响)
List<String> foreignKeys = jdbcTemplate.queryForList(
"SELECT TABLE_NAME FROM information_schema.TABLE_CONSTRAINTS " +
"WHERE CONSTRAINT_TYPE = 'FOREIGN KEY'",
String.class
);
if (!foreignKeys.isEmpty()) {
report.addInfo("发现外键约束,TiDB支持但高并发下可能有性能影响:" +
String.join(", ", foreignKeys));
}
// 检查3:是否有FULLTEXT索引
List<String> fulltextIndexes = jdbcTemplate.queryForList(
"SELECT TABLE_NAME FROM information_schema.STATISTICS " +
"WHERE INDEX_TYPE = 'FULLTEXT'",
String.class
);
if (!fulltextIndexes.isEmpty()) {
report.addError("发现FULLTEXT索引,TiDB不支持,需要迁移到Elasticsearch:" +
String.join(", ", fulltextIndexes));
}
log.info("TiDB兼容性检查完成: {} 个错误, {} 个警告, {} 个提示",
report.errorCount(), report.warningCount(), report.infoCount());
return report;
}
public record CompatibilityReport(
java.util.List<String> errors,
java.util.List<String> warnings,
java.util.List<String> infos
) {
public CompatibilityReport() {
this(new java.util.ArrayList<>(),
new java.util.ArrayList<>(),
new java.util.ArrayList<>());
}
public void addError(String msg) { errors.add(msg); }
public void addWarning(String msg) { warnings.add(msg); }
public void addInfo(String msg) { infos.add(msg); }
public int errorCount() { return errors.size(); }
public int warningCount() { return warnings.size(); }
public int infoCount() { return infos.size(); }
public boolean hasErrors() { return !errors.isEmpty(); }
}
}总结
张磊团队的迁移最终方案:
- TiDB:用户业务数据 + AI向量数据(100万以内)
- PostgreSQL + pgvector:知识库向量(200万,成本敏感)
- Redis Vector:实时推荐热数据缓存(毫秒级响应)
- 对象存储(OSS):文档/图片/音频原始文件
选型原则:
- 先解决数据量和QPS要求,再考虑功能
- 优先选择开发团队熟悉的技术栈,降低运维风险
- 不同场景用不同的数据库,不要用一种方案解决所有问题
- 从简单开始(pgvector),随规模增长再换(Milvus)
- 连接池监控是AI应用数据层稳定性的第一道防线
