You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
16 KiB
16 KiB
数据库模块检查与优化设计文档
概述
本文档分析 Koa3 项目的数据库模块存在的问题,并提供优化方案。通过深入分析代码结构、模型设计、查询缓存和错误处理机制,识别潜在问题并提出改进建议。
技术栈分析
- 数据库: SQLite3
- ORM框架: Knex.js
- 缓存机制: 内存缓存(自定义实现)
- 项目类型: 后端应用(Node.js + Koa3)
架构分析
当前架构结构
graph TB
A[应用层] --> B[模型层]
B --> C[数据库连接层]
C --> D[SQLite数据库]
B --> E[查询缓存层]
E --> F[内存缓存]
C --> G[Knex QueryBuilder]
G --> H[迁移系统]
G --> I[种子数据]
数据模型关系图
erDiagram
Users {
int id PK
string username
string email UK
string password
string role
string phone
int age
string name
text bio
string avatar
string status
timestamp created_at
timestamp updated_at
}
Articles {
int id PK
string title
text content
string author
string category
string tags
string keywords
string description
string status
timestamp published_at
int view_count
string featured_image
text excerpt
int reading_time
string meta_title
text meta_description
string slug UK
timestamp created_at
timestamp updated_at
}
Bookmarks {
int id PK
int user_id FK
string title
string url
text description
timestamp created_at
timestamp updated_at
}
SiteConfig {
int id PK
string key UK
text value
timestamp created_at
timestamp updated_at
}
Contacts {
int id PK
string name
string email
string subject
text message
string ip_address
text user_agent
string status
timestamp created_at
timestamp updated_at
}
Users ||--o{ Bookmarks : "拥有"
问题识别与分析
1. 数据库连接问题
问题描述
- 连接池配置不合理,SQLite设置为最大1个连接,在高并发场景下可能成为瓶颈
- 缺少连接重试机制和错误恢复策略
- 没有健康检查机制
影响评估
- 性能影响: 高并发场景下连接竞争导致性能下降
- 稳定性风险: 连接异常时缺少恢复机制
2. 模型设计问题
问题描述
- 模型方法返回值不一致,部分返回数组,部分返回对象
- 缺少统一的错误处理机制
- 模型之间缺少关联查询方法
- 批量操作支持不足
影响评估
- 开发效率: 不一致的API增加开发复杂度
- 维护成本: 缺少统一规范导致维护困难
3. 查询缓存问题
问题描述
- 缓存键生成策略不合理,可能产生冲突
- 缺少缓存失效策略和一致性保证
- 没有缓存命中率监控
影响评估
- 数据一致性: 缓存与数据库数据不同步
- 内存泄漏: 缓存无限增长可能导致内存问题
4. 事务处理问题
问题描述
- 模型方法缺少事务支持
- 没有原子操作保证
- 复杂业务逻辑缺少事务包装
影响评估
- 数据完整性: 并发操作可能导致数据不一致
- 业务逻辑: 复杂操作缺少原子性保证
5. 索引优化问题
问题描述
- 部分查询缺少合适的索引
- 复合索引设计不合理
- 缺少查询性能监控
影响评估
- 查询性能: 缺少索引导致查询缓慢
- 扩展性: 数据量增长时性能急剧下降
优化方案设计
1. 数据库连接优化
连接池配置改进
// knexfile.mjs 优化配置
export default {
development: {
client: "sqlite3",
connection: {
filename: "./database/development.sqlite3",
},
pool: {
min: 1,
max: 3, // 适当增加连接数
acquireTimeoutMillis: 60000,
createTimeoutMillis: 30000,
destroyTimeoutMillis: 5000,
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
createRetryIntervalMillis: 200,
afterCreate: (conn, done) => {
conn.run("PRAGMA journal_mode = WAL", done)
conn.run("PRAGMA synchronous = NORMAL", done)
conn.run("PRAGMA cache_size = 1000", done)
conn.run("PRAGMA temp_store = MEMORY", done)
},
},
}
}
健康检查机制
// db/index.js 添加健康检查
export const checkHealth = async () => {
try {
await db.raw("SELECT 1")
return { status: "healthy", timestamp: new Date() }
} catch (error) {
return { status: "unhealthy", error: error.message, timestamp: new Date() }
}
}
2. 模型设计优化
统一基础模型类
// db/models/BaseModel.js
class BaseModel {
static get tableName() {
throw new Error("tableName must be defined")
}
static async findById(id) {
const result = await db(this.tableName).where("id", id).first()
return result || null
}
static async findAll(options = {}) {
const { page = 1, limit = 10, orderBy = "id", order = "desc" } = options
const offset = (page - 1) * limit
return db(this.tableName)
.orderBy(orderBy, order)
.limit(limit)
.offset(offset)
}
static async create(data) {
const [result] = await db(this.tableName)
.insert({
...data,
created_at: db.fn.now(),
updated_at: db.fn.now(),
})
.returning("*")
return result
}
static async update(id, data) {
const [result] = await db(this.tableName)
.where("id", id)
.update({
...data,
updated_at: db.fn.now(),
})
.returning("*")
return result
}
static async delete(id) {
return db(this.tableName).where("id", id).del()
}
static async count(conditions = {}) {
const result = await db(this.tableName).where(conditions).count("id as count").first()
return parseInt(result.count)
}
}
关联查询方法
// 扩展模型关联查询
class ArticleModel extends BaseModel {
static get tableName() { return "articles" }
// 获取作者相关文章
static async findByAuthorWithProfile(author) {
return db(this.tableName)
.select("articles.*", "users.name as author_name", "users.avatar as author_avatar")
.leftJoin("users", "articles.author", "users.username")
.where("articles.author", author)
.where("articles.status", "published")
}
}
class BookmarkModel extends BaseModel {
static get tableName() { return "bookmarks" }
// 获取用户书签(包含用户信息)
static async findByUserWithProfile(userId) {
return db(this.tableName)
.select("bookmarks.*", "users.username", "users.name")
.leftJoin("users", "bookmarks.user_id", "users.id")
.where("bookmarks.user_id", userId)
}
}
3. 查询缓存优化
改进缓存键生成策略
// db/index.js 缓存优化
const getCacheKeyForBuilder = (builder) => {
if (builder._customCacheKey) return String(builder._customCacheKey)
// 改进键生成策略
const sql = builder.toString()
const tableName = builder._single.table || 'unknown'
const hash = require('crypto').createHash('md5').update(sql).digest('hex')
return `${tableName}:${hash}`
}
// 添加缓存统计
export const getCacheStats = () => {
let valid = 0
let expired = 0
let totalSize = 0
for (const [key, entry] of queryCache.entries()) {
if (isExpired(entry)) {
expired++
} else {
valid++
totalSize += JSON.stringify(entry.value).length
}
}
return {
totalKeys: queryCache.size,
validKeys: valid,
expiredKeys: expired,
totalSize,
hitRate: valid / (valid + expired) || 0
}
}
缓存一致性策略
// 数据变更时自动清理相关缓存
buildKnex.QueryBuilder.extend("invalidateCache", function() {
const tableName = this._single.table
if (tableName) {
DbQueryCache.clearByPrefix(`${tableName}:`)
}
return this
})
// 在模型的 CUD 操作后自动清理缓存
class BaseModel {
static async create(data) {
const result = await db(this.tableName).insert(data).returning("*")
await db(this.tableName).invalidateCache()
return result[0]
}
static async update(id, data) {
const result = await db(this.tableName)
.where("id", id)
.update(data)
.returning("*")
await db(this.tableName).invalidateCache()
return result[0]
}
static async delete(id) {
const result = await db(this.tableName).where("id", id).del()
await db(this.tableName).invalidateCache()
return result
}
}
4. 事务处理优化
事务工具函数
// db/transaction.js
export const withTransaction = async (callback) => {
const trx = await db.transaction()
try {
const result = await callback(trx)
await trx.commit()
return result
} catch (error) {
await trx.rollback()
throw error
}
}
// 使用示例
export const createUserWithProfile = async (userData, profileData) => {
return withTransaction(async (trx) => {
const [user] = await trx("users").insert(userData).returning("*")
const [profile] = await trx("user_profiles")
.insert({ ...profileData, user_id: user.id })
.returning("*")
return { user, profile }
})
}
批量操作优化
// 批量插入优化
class BaseModel {
static async createMany(dataArray, batchSize = 100) {
const results = []
for (let i = 0; i < dataArray.length; i += batchSize) {
const batch = dataArray.slice(i, i + batchSize)
const batchResults = await db(this.tableName)
.insert(batch)
.returning("*")
results.push(...batchResults)
}
await db(this.tableName).invalidateCache()
return results
}
static async updateMany(conditions, data) {
const result = await db(this.tableName)
.where(conditions)
.update({ ...data, updated_at: db.fn.now() })
await db(this.tableName).invalidateCache()
return result
}
}
5. 索引优化建议
添加必要索引
// 新增迁移文件:add_performance_indexes.mjs
export const up = async (knex) => {
// 用户表索引
await knex.schema.alterTable("users", (table) => {
table.index(["email"])
table.index(["username"])
table.index(["status", "created_at"])
})
// 文章表索引
await knex.schema.alterTable("articles", (table) => {
table.index(["author", "status"])
table.index(["category", "published_at"])
table.index(["status", "view_count"])
table.index(["tags"]) // 用于标签搜索
})
// 书签表索引
await knex.schema.alterTable("bookmarks", (table) => {
table.index(["user_id", "created_at"])
table.index(["url"]) // 用于URL查重
})
// 联系人表索引
await knex.schema.alterTable("contacts", (table) => {
table.index(["email", "created_at"])
table.index(["status", "created_at"])
})
}
6. 错误处理优化
统一错误处理机制
// db/errors.js
export class DatabaseError extends Error {
constructor(message, code, originalError) {
super(message)
this.name = "DatabaseError"
this.code = code
this.originalError = originalError
}
}
export const handleDatabaseError = (error) => {
if (error.code === "SQLITE_CONSTRAINT") {
return new DatabaseError("数据约束违反", "CONSTRAINT_VIOLATION", error)
}
if (error.code === "SQLITE_BUSY") {
return new DatabaseError("数据库忙,请稍后重试", "DATABASE_BUSY", error)
}
return new DatabaseError("数据库操作失败", "DATABASE_ERROR", error)
}
// 在模型中使用
class BaseModel {
static async findById(id) {
try {
return await db(this.tableName).where("id", id).first()
} catch (error) {
throw handleDatabaseError(error)
}
}
}
7. 性能监控优化
查询性能监控
// db/monitor.js
const queryStats = new Map()
export const logQuery = (sql, duration) => {
const key = sql.split(' ')[0].toUpperCase() // SELECT, INSERT, UPDATE, DELETE
if (!queryStats.has(key)) {
queryStats.set(key, { count: 0, totalTime: 0, avgTime: 0 })
}
const stats = queryStats.get(key)
stats.count++
stats.totalTime += duration
stats.avgTime = stats.totalTime / stats.count
}
export const getQueryStats = () => {
return Object.fromEntries(queryStats)
}
// 在 knex 配置中添加查询日志
export default {
development: {
// ... 其他配置
log: {
warn(message) {
console.warn(message)
},
error(message) {
console.error(message)
},
deprecate(message) {
console.log(message)
},
debug(message) {
if (message.sql) {
const duration = message.bindings ? message.duration : 0
logQuery(message.sql, duration)
}
},
},
}
}
测试策略
单元测试框架
// tests/models/BaseModel.test.js
import { expect } from 'chai'
import { BaseModel } from '../src/db/models/BaseModel.js'
describe('BaseModel', () => {
it('应该正确创建记录', async () => {
const data = { name: 'test' }
const result = await TestModel.create(data)
expect(result).to.have.property('id')
expect(result.name).to.equal('test')
})
it('应该正确处理事务', async () => {
await expect(
withTransaction(async (trx) => {
await trx('test_table').insert({ name: 'test' })
throw new Error('回滚测试')
})
).to.be.rejected
const count = await TestModel.count()
expect(count).to.equal(0)
})
})
性能测试
// tests/performance/cache.test.js
describe('缓存性能测试', () => {
it('缓存命中率应该大于80%', async () => {
// 执行大量查询
for (let i = 0; i < 1000; i++) {
await ArticleModel.findById(1).cache(60000)
}
const stats = getCacheStats()
expect(stats.hitRate).to.be.greaterThan(0.8)
})
})
迁移计划
阶段1: 基础优化(1-2周)
- 修复数据库连接配置
- 统一模型返回值格式
- 添加基础错误处理
阶段2: 功能增强(2-3周)
- 实现统一基础模型类
- 添加关联查询方法
- 优化查询缓存机制
阶段3: 性能优化(1-2周)
- 添加必要索引
- 实现事务支持
- 添加性能监控
阶段4: 测试与验证(1周)
- 编写单元测试
- 性能基准测试
- 生产环境验证