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

数据库模块检查与优化设计文档

概述

本文档分析 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周)

  1. 修复数据库连接配置
  2. 统一模型返回值格式
  3. 添加基础错误处理

阶段2: 功能增强(2-3周)

  1. 实现统一基础模型类
  2. 添加关联查询方法
  3. 优化查询缓存机制

阶段3: 性能优化(1-2周)

  1. 添加必要索引
  2. 实现事务支持
  3. 添加性能监控

阶段4: 测试与验证(1周)

  1. 编写单元测试
  2. 性能基准测试
  3. 生产环境验证