Skip to content

RFC: TRANSACTION_CACHE_SAFETY 事务缓存安全方案提案 #22

@adaex

Description

@adaex

问题描述

当前 MysqlCache 在事务场景下存在缓存一致性风险:写操作会立即清理缓存,但如果事务回滚,缓存与数据库状态就会不一致。

问题举例

// 风险场景:事务回滚导致缓存不一致
await mysqlBin.io.transaction(async (trx) => {
  await User.updateById('user001', { name: '新名字' }, trx)  // 缓存立即清理
  
  // 此时其他请求可能读取到旧数据并缓存
  // const user = await User.getById('user001')  // 缓存了旧数据
  
  throw new Error('业务异常')  // 事务自动回滚,但缓存已被污染
})

改动思路

直接在事务对象上标记 __transactionCacheMode 属性来选择缓存策略:

  • legacy:立即清理缓存(默认,保持现有行为)
  • safe:延迟清理缓存(事务提交后才清理)

具体实现

MysqlCache 核心逻辑

// src/services/MysqlCache.ts
private isTransactionSafe(trx?: CoaMysql.Transaction): boolean {
  return (trx as any)?.__transactionCacheMode === 'safe'
}

private async handleCacheClear(trx: CoaMysql.Transaction | undefined, ids: string[], dataList: any[]) {
  if (trx && this.isTransactionSafe(trx)) {
    // 安全模式:注册事务提交后清理
    this.registerCacheClearOnCommit(trx, ids, dataList)
  } else {
    // 传统模式:立即清理
    await this.deleteCache(ids, dataList)
  }
}

private registerCacheClearOnCommit(trx: CoaMysql.Transaction, ids: string[], dataList: any[]) {
  if (!(trx as any).__cacheClearTasks) {
    (trx as any).__cacheClearTasks = []
    
    // 劫持事务提交方法实现延迟清理缓存
    // TODO: 考虑查询 Knex 是否提供更优雅的事务钩子方案
    const originalCommit = trx.commit.bind(trx)
    trx.commit = async () => {
      const result = await originalCommit()
      // 事务提交成功后清理缓存
      const tasks = (trx as any).__cacheClearTasks || []
      for (const task of tasks) {
        await this.deleteCache(task.ids, task.dataList)
      }
      return result
    }
  }
  
  // 添加缓存清理任务
  (trx as any).__cacheClearTasks.push({ ids, dataList })
}

// 所有写操作方法都调用 handleCacheClear
async updateById(id: string, data: any, trx?: CoaMysql.Transaction) {
  const dataList = await this.getCacheChangedDataList([id], data, trx)
  const result = await super.updateById(id, data, trx)
  if (result) await this.handleCacheClear(trx, [id], dataList)
  return result
}

4. 使用示例

// 安全模式(优先在有问题的场景使用)
await mysqlBin.io.transaction(async (trx) => {
  // 标记事务为安全模式
  Object.assign(trx, { __transactionCacheMode: 'safe' })
  
  await User.updateById('user001', { name: '张三' }, trx)
  await Order.insert({ userId: 'user001', amount: 100 }, trx)
  // 事务提交后才清理缓存,保证一致性
})

// 传统模式(现有业务保持不变)
await mysqlBin.io.transaction(async (trx) => {
  await User.updateById('user002', { name: '李四' }, trx)
  // 缓存立即清理,保持原有行为
})

实现要求

  1. 确保零侵入性:现有代码无需任何修改
  2. 确保向后兼容性:原有 mysqlBin.io.transaction() 行为不变
  3. 只需要在需要安全模式时手动标记 Object.assign(trx, { __transactionCacheMode: 'safe' })

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions