TypeScript环境下如何为JSON文件创建更新队列规避read/write错误
TypeScript 实现JSON文件读写队列解决方案
核心思路是通过单例异步任务队列串行化所有对同一份JSON文件的读写操作,彻底消除并发读写导致的竞态、半写入等错误。
完整实现代码
import * as fs from 'fs/promises' import * as path from 'path' class JsonDBQueue { // 全局单例映射,不同JSON文件对应独立队列互不干扰 private static instanceMap: Map<string, JsonDBQueue> = new Map() // 任务链,初始为已完成的Promise private taskChain: Promise<void> = Promise.resolve() private readonly dbFilePath: string private constructor(filePath: string) { this.dbFilePath = path.resolve(filePath) } // 获取对应文件的唯一队列实例 public static getInstance(filePath: string): JsonDBQueue { if (!JsonDBQueue.instanceMap.has(filePath)) { JsonDBQueue.instanceMap.set(filePath, new JsonDBQueue(filePath)) } return JsonDBQueue.instanceMap.get(filePath)! } // 通用入队方法,所有读写操作都需要通过该方法加入执行队列 private enqueue<T>(task: () => Promise<T>): Promise<T> { return new Promise((resolve, reject) => { // 将当前任务追加到队列末尾,保证串行执行 this.taskChain = this.taskChain .then(async () => { const result = await task() resolve(result) }) .catch((err) => { reject(err) }) }) } // 读取JSON文件内容 public read<T = unknown>(): Promise<T> { return this.enqueue(async () => { const rawContent = await fs.readFile(this.dbFilePath, 'utf-8') return JSON.parse(rawContent) as T }) } // 写入内容到JSON文件,先写临时文件再重命名,避免进程崩溃损坏原文件 public write<T>(data: T): Promise<void> { return this.enqueue(async () => { const content = JSON.stringify(data, null, 2) const tempFilePath = `${this.dbFilePath}.tmp` await fs.writeFile(tempFilePath, content, 'utf-8') await fs.rename(tempFilePath, this.dbFilePath) }) } // 原子更新操作,将读-改-写封装为单个原子任务,避免中间插入其他操作 public update<T>(updater: (oldData: T) => T | Promise<T>): Promise<T> { return this.enqueue(async () => { const oldData = await this.read<T>() const newData = await updater(oldData) await this.write(newData) return newData }) } }
使用示例
// 初始化目标JSON文件的队列实例 const userDB = JsonDBQueue.getInstance('./data/users.json') // 1. 直接写入初始数据 await userDB.write({ users: [] }) // 2. 读取JSON数据 const dbData = await userDB.read<{ users: Array<{ id: number; name: string }> }>() // 3. 原子更新数据,并发调用也不会出现冲突 const updateResult = await userDB.update<{ users: Array<{ id: number; name: string }> }>((oldData) => { oldData.users.push({ id: Date.now(), name: '测试用户' }) return oldData }) // 并发调用测试:所有任务会按调用顺序串行执行,不会出现读写错乱 Promise.all([ userDB.update(old => { old.users.push({ id: 1, name: '用户A' }); return old }), userDB.update(old => { old.users.push({ id: 2, name: '用户B' }); return old }), userDB.read() ]).then(([res1, res2, finalData]) => { // finalData必然包含id为1和2的两个用户 console.log(finalData.users.length) // 输出2 })
核心特性说明
- 单例映射保证同一个JSON文件只会对应一个队列实例,避免多处初始化导致的并发问题
- 写入逻辑做了异常兼容,即使写入中途进程崩溃,也不会损坏原有JSON文件
- 所有操作自动串行执行,不需要手动加锁,适配Node.js单线程事件循环特性
- update方法封装原子操作,避免读和写之间插入其他操作导致的数据不一致
内容的提问来源于stack exchange,提问作者Grupo RXCO
相关产品推荐
相关产品推荐

