IndexedDB 的事务（Transaction）：ACID 特性与并发控制机制

admin
Wed, 10 Dec 2025 07:59:33 +0000
Javascript, acid, indexeddb, transaction

各位开发者，大家好！ 今天，我们将深入探讨 Web 平台上一个至关重要的客户端数据存储技术——IndexedDB，特别是它的核心机制：事务（Transaction）。作为一名编程专家，我深知数据完整性、并发控制对于任何应用程序的重要性，无论它运行在服务器还是浏览器。IndexedDB 的事务，正是其保障这些特性的基石。 本次讲座，我将带大家全面剖析 IndexedDB 事务的 ACID 特性，以及它如何实现并发控制。我们将通过丰富的代码示例，从理论到实践，层层递进，确保大家不仅理解其工作原理，更能掌握在实际项目中构建健壮、高效的数据存储方案。 IndexedDB 基础回顾：理解事务的上下文 在深入事务之前，我们先快速回顾一下 IndexedDB 的基本概念，这将帮助我们更好地理解事务在其体系结构中的定位。 IndexedDB 是一个强大的客户端存储解决方案，它是一个低级的 API，用于在用户的浏览器中存储大量结构化数据。它不是关系型数据库，而是一个基于对象的 NoSQL 存储。 核心概念： 数据库（Database）： 通过名称和版本号进行识别。 对象存储（Object Store）： …

继续阅读“IndexedDB 的事务（Transaction）：ACID 特性与并发控制机制”

The post IndexedDB 的事务（Transaction）：ACID 特性与并发控制机制 first appeared on 智猿学院-前后端，数据库，人工智能，云计算等领域前沿技术讲座.

各位开发者，大家好！

今天，我们将深入探讨 Web 平台上一个至关重要的客户端数据存储技术——IndexedDB，特别是它的核心机制：事务（Transaction）。作为一名编程专家，我深知数据完整性、并发控制对于任何应用程序的重要性，无论它运行在服务器还是浏览器。IndexedDB 的事务，正是其保障这些特性的基石。

本次讲座，我将带大家全面剖析 IndexedDB 事务的 ACID 特性，以及它如何实现并发控制。我们将通过丰富的代码示例，从理论到实践，层层递进，确保大家不仅理解其工作原理，更能掌握在实际项目中构建健壮、高效的数据存储方案。

IndexedDB 基础回顾：理解事务的上下文

在深入事务之前，我们先快速回顾一下 IndexedDB 的基本概念，这将帮助我们更好地理解事务在其体系结构中的定位。

IndexedDB 是一个强大的客户端存储解决方案，它是一个低级的 API，用于在用户的浏览器中存储大量结构化数据。它不是关系型数据库，而是一个基于对象的 NoSQL 存储。

核心概念：

数据库（Database）： 通过名称和版本号进行识别。

对象存储（Object Store）： 类似于关系型数据库中的表，但存储的是 JavaScript 对象。每个对象存储都有一个名称，并且可以选择指定一个 keyPath 或 autoIncrement。

键（Key）： 每个存储在对象存储中的对象都必须有一个唯一的键。这个键可以是对象自身的某个属性（keyPath），也可以由 IndexedDB 自动生成（autoIncrement）。

索引（Index）： 允许你根据对象存储中除主键之外的其他属性进行高效查询。

连接数据库与版本管理：

要使用 IndexedDB，首先需要打开一个数据库连接。版本管理是 IndexedDB 的一个关键特性，它通过 onupgradeneeded 事件来处理数据库的创建和结构升级。

// 假设我们有一个名为 'myDatabase' 的数据库
const DB_NAME = 'myDatabase';
const DB_VERSION = 1; // 数据库版本号

let db; // 用于存储数据库实例

function openDatabase() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onerror = (event) => {
            console.error("数据库打开失败:", event.target.errorCode);
            reject(new Error("数据库打开失败"));
        };

        request.onsuccess = (event) => {
            db = event.target.result;
            console.log("数据库打开成功");
            resolve(db);
        };

        // onupgradeneeded 事件只在数据库版本号发生变化时（或首次创建时）触发
        request.onupgradeneeded = (event) => {
            console.log("数据库升级或首次创建");
            const dbUpgrade = event.target.result;

            // 如果 'users' 对象存储不存在，则创建它
            if (!dbUpgrade.objectStoreNames.contains('users')) {
                const userStore = dbUpgrade.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
                // 创建索引，方便根据 'email' 字段查询用户
                userStore.createIndex('emailIndex', 'email', { unique: true });
                console.log("创建 'users' 对象存储和 'emailIndex' 索引");
            }

            // 如果 'products' 对象存储不存在，则创建它
            if (!dbUpgrade.objectStoreNames.contains('products')) {
                const productStore = dbUpgrade.createObjectStore('products', { keyPath: 'productId' });
                productStore.createIndex('nameIndex', 'name', { unique: false });
                console.log("创建 'products' 对象存储和 'nameIndex' 索引");
            }

            // 可以在这里处理更多版本升级逻辑
            // 例如，从旧版本迁移数据
            // ...
        };
    });
}

// 示例调用
// openDatabase()
//     .then(database => {
//         console.log("数据库准备就绪:", database);
//         // 可以在这里开始执行事务操作
//     })
//     .catch(error => {
//         console.error("数据库初始化失败:", error);
//     });

onupgradeneeded 是一个特殊的事件，它在 IDBOpenDBRequest 成功打开数据库之前触发，并且提供了一个 IDBVersionChangeEvent 对象。这个事件的 target.result 是一个 IDBDatabase 实例，但它处于一种特殊的 versionchange 事务模式下，拥有独占访问权限，允许我们创建、删除对象存储和索引。这是我们进行数据库结构管理的关键点。

IndexedDB 事务的核心：IDBTransaction

现在，让我们聚焦到今天的主题：事务。在 IndexedDB 中，所有的读写操作都必须在一个事务的上下文中执行。事务是确保数据完整性和一致性的关键机制。

什么是事务？

事务（Transaction）是一系列作为单个逻辑工作单元执行的操作。这些操作要么全部成功并提交，要么全部失败并回滚，从而使数据库保持一致状态。

为什么要使用事务？

1. 数据完整性： 确保一组相关的操作作为一个整体执行，避免部分成功部分失败导致的数据损坏。

2. 数据一致性： 保证在事务开始和结束时，数据库都处于一个有效且一致的状态。

3. 并发控制： 管理多个并发操作，防止它们相互干扰，导致脏读、不可重复读或幻读等问题。

创建事务：db.transaction(storeNames, mode)

在 IndexedDB 中，事务通过 IDBDatabase 对象的 transaction() 方法创建。

storeNames (必选): 一个字符串或字符串数组，表示此事务将访问的所有对象存储的名称。这是非常重要的，因为它定义了事务的作用域，并影响并发行为。

mode (可选): 事务的模式，可以是 'readonly'、'readwrite' 或 'versionchange'。默认是 'readonly'。

事务模式详解：

1. 'readonly' (只读)

用途： 用于从一个或多个对象存储中读取数据。

特性： 不允许进行任何数据修改（put, add, delete, clear）。

并发： 多个 'readonly' 事务可以同时访问相同的对象存储，它们之间不会相互阻塞。

性能： 最快的模式，因为没有写操作的开销和并发控制的复杂性。

async function readUserData(userId) {
    await openDatabase(); // 确保数据库已打开
    const transaction = db.transaction(['users'], 'readonly');
    const userStore = transaction.objectStore('users');

    return new Promise((resolve, reject) => {
        const request = userStore.get(userId);

        request.onsuccess = (event) => {
            resolve(event.target.result);
        };

        request.onerror = (event) => {
            console.error("读取用户数据失败:", event.target.errorCode);
            reject(new Error("读取用户数据失败"));
        };

        // 事务完成事件，无论成功或失败都会触发
        transaction.oncomplete = () => {
            console.log("只读事务完成");
        };
        transaction.onerror = (event) => {
            console.error("只读事务发生错误:", event.target.errorCode);
            reject(new Error("只读事务发生错误"));
        };
        transaction.onabort = () => {
            console.warn("只读事务被中止");
            reject(new Error("只读事务被中止"));
        };
    });
}

// 示例调用
// readUserData(1)
//     .then(user => console.log("读取到的用户:", user))
//     .catch(error => console.error(error));

2. 'readwrite' (读写)

用途： 用于在一个或多个对象存储中读取和修改数据（put, add, delete, clear）。

特性： 允许所有数据操作。

并发： 这是最需要关注的模式。在一个特定的对象存储集合上，同一时间只能有一个 'readwrite' 事务是活动的。如果多个 'readwrite' 事务尝试访问相同的对象存储，它们将被排队。

性能： 相较于 'readonly' 模式有更高的开销，因为需要进行更严格的并发控制和日志记录以便回滚。

async function addProduct(product) {
    await openDatabase();
    const transaction = db.transaction(['products'], 'readwrite');
    const productStore = transaction.objectStore('products');

    return new Promise((resolve, reject) => {
        const request = productStore.add(product);

        request.onsuccess = (event) => {
            console.log("产品添加成功，键为:", event.target.result);
            resolve(event.target.result);
        };

        request.onerror = (event) => {
            console.error("产品添加失败:", event.target.errorCode);
            reject(new Error("产品添加失败"));
        };

        transaction.oncomplete = () => {
            console.log("读写事务完成");
        };
        transaction.onerror = (event) => {
            console.error("读写事务发生错误:", event.target.errorCode);
            // 事务会尝试自动回滚
            reject(new Error("读写事务发生错误"));
        };
        transaction.onabort = () => {
            console.warn("读写事务被中止");
            // 事务已经回滚
            reject(new Error("读写事务被中止"));
        };
    });
}

// 示例调用
// addProduct({ productId: 'P001', name: 'Laptop', price: 1200 })
//     .then(key => console.log("添加的产品键:", key))
//     .catch(error => console.error(error));

3. 'versionchange' (版本变更)

用途： 仅在 db.open() 的 onupgradeneeded 事件处理函数中可用。用于创建、删除对象存储和索引，以及执行数据迁移。

特性： 允许修改数据库结构（schema）。

并发： 这是一个独占模式。当一个 'versionchange' 事务活动时，它会阻止所有其他事务（包括 'readonly' 和 'readwrite' 事务）在同一个数据库上执行，直到它完成。

性能： 由于其独占性，应该尽量保持其执行时间简短。

versionchange 事务的示例已在 openDatabase 函数中展示。

事务的生命周期与事件：

一个事务从创建到结束会经历几个状态，并触发相应的事件：

active： 事务正在进行中。

committing： 所有请求已完成，事务正在提交。

finished： 事务已完成（成功提交或失败中止）。

关键事件：

transaction.oncomplete： 当事务中的所有操作成功完成，并且事务成功提交时触发。这是判断事务是否成功的最终信号。

transaction.onerror： 当事务中任何一个请求发生错误，或者事务本身因为某些原因（如数据库连接断开、存储空间不足等）而失败时触发。这会导致事务自动回滚。

transaction.onabort： 当事务被显式地通过 transaction.abort() 方法中止，或者由于 onerror 事件的未处理错误导致隐式中止时触发。这也会导致事务回滚。

重要提示： IndexedDB 的操作是异步的。所有的数据库请求（add, put, get, delete 等）都会返回一个 IDBRequest 对象。你需要监听 IDBRequest 的 onsuccess 和 onerror 事件来处理每个操作的结果。在事务中，所有的操作都必须在同一个事件循环任务中进行调度，或者通过 request.onsuccess 的回调进行链式调用。 如果你在事务中间执行了一个异步操作（如 setTimeout 或 fetch），然后尝试在那个异步操作的回调中继续数据库操作，那么原有的事务可能已经完成或被浏览器自动提交了。

IndexedDB 事务的 ACID 特性

现在，我们来详细探讨 IndexedDB 事务如何体现和保障数据库的 ACID 特性。ACID 是数据库管理系统中的一组基本原则，它们是：原子性（Atomicity）、一致性（Consistency）、隔离性（Isolation）和持久性（Durability）。

1. 原子性（Atomicity）

定义： 事务作为一个不可分割的整体执行，其中包含的所有操作要么全部成功提交，要么全部失败回滚。不可能出现部分成功的情况。

IndexedDB 的实现：
IndexedDB 严格遵循原子性原则。当一个 readwrite 事务被创建时，所有在其作用域内执行的修改操作都被视为一个单一的逻辑单元。

如果事务中的所有操作都成功完成，并且没有发生错误，那么在 transaction.oncomplete 事件触发后，所有的修改都将被永久保存。

如果事务中的任何一个操作失败（例如，违反了索引的 unique 约束，或者 request.onerror 被触发），或者事务被显式地调用 transaction.abort() 方法中止，那么在该事务中进行的所有修改都将被完全撤销（回滚），数据库将恢复到事务开始前的状态。

代码示例：原子性与回滚

假设我们要在一个事务中同时添加一个用户和一个产品，如果其中任何一个操作失败，整个事务都应该回滚。

async function addUserDataAndProduct(user, product) {
    await openDatabase();
    const transaction = db.transaction(['users', 'products'], 'readwrite');
    const userStore = transaction.objectStore('users');
    const productStore = transaction.objectStore('products');

    return new Promise((resolve, reject) => {
        let userAddSuccess = false;
        let productAddSuccess = false;

        // 添加用户
        const userRequest = userStore.add(user);
        userRequest.onsuccess = () => {
            userAddSuccess = true;
            console.log("用户添加请求成功:", user.id);
            // 注意：这里只是请求成功，不代表事务整体成功
            checkCompletion();
        };
        userRequest.onerror = (event) => {
            console.error("用户添加失败，事务将中止:", event.target.error);
            // 任何一个操作失败都会导致事务的onerror被触发，进而回滚
            // 手动abort也可以
            // transaction.abort();
            reject(new Error(`用户添加失败: ${event.target.error.message}`));
        };

        // 添加产品
        const productRequest = productStore.add(product);
        productRequest.onsuccess = () => {
            productAddSuccess = true;
            console.log("产品添加请求成功:", product.productId);
            checkCompletion();
        };
        productRequest.onerror = (event) => {
            console.error("产品添加失败，事务将中止:", event.target.error);
            // transaction.abort();
            reject(new Error(`产品添加失败: ${event.target.error.message}`));
        };

        // 辅助函数，检查两个操作是否都已请求成功
        // 注意：这只是请求的onsuccess，不是事务的oncomplete
        function checkCompletion() {
            if (userAddSuccess && productAddSuccess) {
                console.log("所有操作请求已成功，等待事务提交...");
                // 此时，所有请求都已发出，浏览器会尝试提交事务
            }
        }

        transaction.oncomplete = () => {
            console.log("事务成功提交：用户和产品均已添加。");
            resolve("用户和产品均已成功添加。");
        };
        transaction.onerror = (event) => {
            console.error("事务发生错误，已回滚:", event.target.error || "未知错误");
            reject(new Error("事务失败，已回滚。"));
        };
        transaction.onabort = () => {
            console.warn("事务被中止，已回滚。");
            reject(new Error("事务被中止，已回滚。"));
        };
    });
}

// 示例 1: 成功案例
// addUserDataAndProduct(
//     { id: 101, name: 'Alice', email: 'alice@example.com' },
//     { productId: 'PROD001', name: 'Keyboard', price: 75 }
// ).then(msg => console.log(msg)).catch(err => console.error(err));

// 示例 2: 失败案例 - 假设 'users' 对象的 'email' 索引是 unique 的
// 如果尝试添加一个已存在的email，则会失败
// addUserDataAndProduct(
//     { id: 102, name: 'Bob', email: 'alice@example.com' }, // 故意使用重复email
//     { productId: 'PROD002', name: 'Mouse', price: 30 }
// ).then(msg => console.log(msg)).catch(err => console.error(err));

在示例2中，如果 alice@example.com 已经存在于 users 对象存储中（并且 emailIndex 是 unique 的），那么 userRequest.onerror 会被触发，导致整个 readwrite 事务回滚。即使 productRequest 成功完成了添加产品的操作，这个产品也不会被持久化到数据库中。这就是原子性的体现。

2. 一致性（Consistency）

定义： 事务执行前后，数据库从一个一致状态转换到另一个一致状态。它确保数据满足所有的预定义规则、约束和完整性条件（例如，唯一性约束、外键约束虽然 IndexedDB 没有直接的外键，但可以通过应用层逻辑模拟）。

IndexedDB 的实现：
IndexedDB 本身提供了基础的一致性保障，主要体现在以下几个方面：

原子性保障： 由于原子性，事务要么完全成功，要么完全失败，这避免了因部分操作成功而导致的数据不一致。

结构约束： 对象存储的 keyPath、autoIncrement 属性以及索引的 unique 属性是 IndexedDB 提供的基本结构约束。如果事务中的操作违反了这些约束，事务就会失败并回滚，从而维护了数据的一致性。

版本控制： versionchange 事务确保在数据库结构升级过程中，整个数据库处于独占模式，防止在结构变更时出现不一致的数据操作。

然而，更高级的业务逻辑一致性（例如，账户余额不能为负，订单总价必须等于所有商品价格之和）需要由开发者在事务内部的业务逻辑中自行维护。IndexedDB 提供的是一个稳定的平台来执行这些逻辑，但不会自动强制执行复杂的业务规则。

代码示例：维护业务逻辑一致性

假设我们要更新一个用户的账户余额，并记录一笔交易。我们必须确保余额更新和交易记录是同步的，并且余额不能低于某个阈值。

async function updateBalanceAndLogTransaction(userId, amount, transactionType) {
    await openDatabase();
    const transaction = db.transaction(['users', 'transactions'], 'readwrite');
    const userStore = transaction.objectStore('users');
    const transactionStore = transaction.objectStore('transactions');

    return new Promise((resolve, reject) => {
        // 1. 获取用户当前余额
        const userGetRequest = userStore.get(userId);
        userGetRequest.onsuccess = (event) => {
            const user = event.target.result;
            if (!user) {
                console.error("用户不存在，事务中止。");
                transaction.abort(); // 用户不存在，中止事务
                return reject(new Error("用户不存在。"));
            }

            let newBalance = user.balance || 0;
            if (transactionType === 'deposit') {
                newBalance += amount;
            } else if (transactionType === 'withdraw') {
                newBalance -= amount;
                if (newBalance < 0) { // 业务逻辑约束：余额不能为负
                    console.error("余额不足，事务中止。");
                    transaction.abort(); // 违反业务规则，中止事务
                    return reject(new Error("余额不足。"));
                }
            } else {
                console.error("无效的交易类型，事务中止。");
                transaction.abort();
                return reject(new Error("无效的交易类型。"));
            }

            // 2. 更新用户余额
            user.balance = newBalance;
            const userPutRequest = userStore.put(user);
            userPutRequest.onsuccess = () => {
                console.log(`用户 ${userId} 余额更新成功。新余额: ${newBalance}`);

                // 3. 记录交易
                const transactionRecord = {
                    userId: userId,
                    type: transactionType,
                    amount: amount,
                    timestamp: new Date().toISOString(),
                    newBalance: newBalance
                };
                const transactionAddRequest = transactionStore.add(transactionRecord);
                transactionAddRequest.onsuccess = () => {
                    console.log("交易记录添加成功。");
                    // 所有操作都在onsuccess链中完成，等待事务自动提交
                };
                transactionAddRequest.onerror = (event) => {
                    console.error("交易记录添加失败，事务中止:", event.target.error);
                    // 任何内部错误都会导致事务onerror被触发
                    reject(new Error("交易记录添加失败。"));
                };
            };
            userPutRequest.onerror = (event) => {
                console.error("用户余额更新失败，事务中止:", event.target.error);
                reject(new Error("用户余额更新失败。"));
            };
        };
        userGetRequest.onerror = (event) => {
            console.error("获取用户失败，事务中止:", event.target.error);
            reject(new Error("获取用户失败。"));
        };

        transaction.oncomplete = () => {
            console.log("事务成功提交：余额更新和交易记录完成。");
            resolve("交易成功完成。");
        };
        transaction.onerror = (event) => {
            console.error("事务发生错误，已回滚:", event.target.error || "未知错误");
            reject(new Error("交易失败，已回滚。"));
        };
        transaction.onabort = () => {
            console.warn("事务被中止，已回滚。");
            reject(new Error("交易被中止，已回滚。"));
        };
    });
}

// 确保 'transactions' 对象存储存在，假设它有 { autoIncrement: true }
// db.onupgradeneeded 中添加:
// if (!dbUpgrade.objectStoreNames.contains('transactions')) {
//     dbUpgrade.createObjectStore('transactions', { autoIncrement: true });
// }

// 示例：首次添加用户并设置初始余额
// openDatabase().then(async () => {
//     const transaction = db.transaction(['users'], 'readwrite');
//     const userStore = transaction.objectStore('users');
//     userStore.add({ id: 1, name: 'Charlie', balance: 100 });
//     transaction.oncomplete = () => console.log('Charlie added with initial balance.');
// }).catch(err => console.error(err));

// 示例 1: 成功存款
// updateBalanceAndLogTransaction(1, 50, 'deposit')
//     .then(msg => console.log(msg))
//     .catch(err => console.error(err));

// 示例 2: 成功取款
// updateBalanceAndLogTransaction(1, 30, 'withdraw')
//     .then(msg => console.log(msg))
//     .catch(err => console.error(err));

// 示例 3: 失败取款（余额不足）
// updateBalanceAndLogTransaction(1, 200, 'withdraw')
//     .then(msg => console.log(msg))
//     .catch(err => console.error(err));

在这个例子中，transaction.abort() 的使用是确保业务逻辑一致性的关键。如果用户不存在或余额不足，我们显式地中止事务，保证了数据库不会进入不一致的状态。

3. 隔离性（Isolation）

定义： 多个并发事务的执行互不干扰，就好像它们是串行执行的一样。一个事务在执行过程中，看不到其他未提交事务所做的修改。

IndexedDB 的实现：
IndexedDB 提供了一种形式的隔离性，但其机制与传统的关系型数据库有所不同，通常被描述为提供“串行化”（Serializable）的隔离级别，至少对于 readwrite 事务在同一对象存储上是如此。

versionchange 事务： 拥有最高级别的隔离。它会完全阻塞所有其他事务，直到自身完成。这确保了数据库结构修改过程的绝对隔离。

readwrite 事务：

相同对象存储： 如果多个 readwrite 事务尝试访问相同的对象存储，它们会被 IndexedDB 内部排队，按顺序执行。这意味着，一个 readwrite 事务在访问特定对象存储时，对该存储拥有独占访问权，其他排队的 readwrite 事务必须等待当前事务完成。这有效防止了脏读、不可重复读和幻读等问题。

不同对象存储： 如果两个 readwrite 事务访问的是不同且不重叠的对象存储集合，它们可以并发执行。

readonly 事务：

多个 readonly 事务可以并发执行，并且可以与访问不同对象存储的 readwrite 事务并发执行。

readonly 事务通常会看到它启动时数据库的一个快照。这意味着，一个长时间运行的 readonly 事务不会被后续提交的 readwrite 事务所做的修改所影响。

并发控制机制：

IndexedDB 通过事务的 storeNames 参数来实现其并发控制。

锁定机制： 当一个 readwrite 事务被创建并传入一组 storeNames 时，IndexedDB 会对这些对象存储进行“逻辑锁定”。这意味着在当前事务完成之前，其他尝试访问这些相同对象存储的 readwrite 事务将被排队等待。

并行性： 如果两个事务的 storeNames 集合完全不重叠，那么它们可以并行执行。例如，一个事务操作 users 对象存储，另一个事务操作 products 对象存储，它们可以同时进行。

代码示例：隔离性与并发行为

async function simulateConcurrency() {
    await openDatabase();

    // 假设 db.onupgradeneeded 已经创建了 'users' 和 'products'
    // 为 'users' 存储添加一个初始用户
    const initialUserTx = db.transaction(['users'], 'readwrite');
    initialUserTx.objectStore('users').add({ id: 1, name: 'Initial User', data: 'initial' });
    await new Promise(resolve => initialUserTx.oncomplete = resolve);
    console.log('Initial user added.');

    // 事务 A: 修改 'users' 存储
    const transactionA = db.transaction(['users'], 'readwrite');
    const userStoreA = transactionA.objectStore('users');
    console.log('Transaction A started (users, readwrite)');

    // 事务 B: 修改 'users' 存储 (将排队等待 A 完成)
    const transactionB = db.transaction(['users'], 'readwrite');
    const userStoreB = transactionB.objectStore('users');
    console.log('Transaction B started (users, readwrite) - expecting to be queued');

    // 事务 C: 修改 'products' 存储 (可以与 A 或 B 并发)
    const transactionC = db.transaction(['products'], 'readwrite');
    const productStoreC = transactionC.objectStore('products');
    console.log('Transaction C started (products, readwrite) - expecting to run concurrently');

    // 事务 D: 只读 'users' 存储 (可以与 A 或 B 并发，但会看到 A 开始时的快照)
    const transactionD = db.transaction(['users'], 'readonly');
    const userStoreD = transactionD.objectStore('users');
    console.log('Transaction D started (users, readonly)');

    // 定义各个事务的操作
    // 事务 A: 模拟耗时操作，修改 user 1
    const opA = new Promise((resolve, reject) => {
        const getReqA = userStoreA.get(1);
        getReqA.onsuccess = (event) => {
            const user = event.target.result;
            if (user) {
                user.data = 'modified by A';
                const putReqA = userStoreA.put(user);
                putReqA.onsuccess = () => {
                    console.log('Transaction A: User 1 modified.');
                    // 模拟耗时，确保B在A完成前尝试操作
                    setTimeout(() => {
                        console.log('Transaction A: Completing after delay.');
                        resolve();
                    }, 500); // 模拟耗时
                };
                putReqA.onerror = reject;
            } else {
                reject('User 1 not found for A');
            }
        };
        getReqA.onerror = reject;

        transactionA.oncomplete = () => console.log('Transaction A completed.');
        transactionA.onerror = (e) => { console.error('Transaction A error:', e.target.error); reject(e); };
        transactionA.onabort = () => { console.warn('Transaction A aborted.'); reject(new Error('Aborted')); };
    });

    // 事务 B: 尝试修改 user 1，它将被排队
    const opB = new Promise((resolve, reject) => {
        // B 事务的操作会在 A 事务完成后才真正开始执行
        transactionB.oncomplete = () => {
            console.log('Transaction B completed.');
            resolve();
        };
        transactionB.onerror = (e) => { console.error('Transaction B error:', e.target.error); reject(e); };
        transactionB.onabort = () => { console.warn('Transaction B aborted.'); reject(new Error('Aborted')); };

        // 在 oncomplete/onerror 之后再执行实际操作，以确保它被排队
        setTimeout(() => { // 稍微延迟一下，确保A先开始
            const getReqB = userStoreB.get(1);
            getReqB.onsuccess = (event) => {
                const user = event.target.result;
                if (user) {
                    // 此时 user.data 应该已经是 'modified by A'
                    console.log('Transaction B: Reading User 1 data before modification:', user.data);
                    user.data = 'modified by B';
                    const putReqB = userStoreB.put(user);
                    putReqB.onsuccess = () => {
                        console.log('Transaction B: User 1 modified.');
                        resolve();
                    };
                    putReqB.onerror = reject;
                } else {
                    reject('User 1 not found for B');
                }
            };
            getReqB.onerror = reject;
        }, 10);
    });

    // 事务 C: 添加一个产品
    const opC = new Promise((resolve, reject) => {
        const addReqC = productStoreC.add({ productId: 'P003', name: 'Webcam', price: 50 });
        addReqC.onsuccess = () => {
            console.log('Transaction C: Product P003 added.');
            resolve();
        };
        addReqC.onerror = reject;

        transactionC.oncomplete = () => console.log('Transaction C completed.');
        transactionC.onerror = (e) => { console.error('Transaction C error:', e.target.error); reject(e); };
        transactionC.onabort = () => { console.warn('Transaction C aborted.'); reject(new Error('Aborted')); };
    });

    // 事务 D: 读取 user 1。它将看到 Transaction A *开始*时的状态
    const opD = new Promise((resolve, reject) => {
        const getReqD = userStoreD.get(1);
        getReqD.onsuccess = (event) => {
            const user = event.target.result;
            if (user) {
                console.log('Transaction D: Reading User 1 data:', user.data); // 应该还是 'initial'
            } else {
                console.warn('Transaction D: User 1 not found.');
            }
            resolve();
        };
        getReqD.onerror = reject;

        transactionD.oncomplete = () => console.log('Transaction D completed.');
        transactionD.onerror = (e) => { console.error('Transaction D error:', e.target.error); reject(e); };
        transactionD.onabort = () => { console.warn('Transaction D aborted.'); reject(new Error('Aborted')); };
    });

    // 等待所有事务完成
    await Promise.allSettled([opA, opB, opC, opD]);

    // 最终检查 user 1 的数据
    const finalTx = db.transaction(['users'], 'readonly');
    const finalUserStore = finalTx.objectStore('users');
    const finalGetReq = finalUserStore.get(1);
    finalGetReq.onsuccess = (event) => {
        const user = event.target.result;
        console.log('Final check: User 1 data is:', user.data); // 应该显示 'modified by B'
    };
    finalGetReq.onerror = (e) => console.error('Final check error:', e.target.error);
}

// simulateConcurrency();

运行结果分析：

Transaction A 和 Transaction B 都尝试修改 users 对象存储。由于它们都是 readwrite 模式且作用于相同的存储，Transaction B 会被 IndexedDB 内部排队，直到 Transaction A 完成。因此，Transaction B 在读取 User 1 数据时，会看到 Transaction A 提交后的状态 ('modified by A')，然后 B 再进行自己的修改。最终 User 1 的数据将是 'modified by B'。

Transaction C 操作 products 对象存储，与 Transaction A 和 B 的 users 存储不重叠，因此它会并发执行。

Transaction D 是一个 readonly 事务，它会读取 users 存储。它将看到 Transaction A 开始时的数据库快照（'initial'）。即使 Transaction A 在 Transaction D 读取之后提交，Transaction D 也不会看到 Transaction A 的修改。这是 IndexedDB 隔离性的一种体现，它避免了脏读和不可重复读。

4. 持久性（Durability）

定义： 一旦事务成功提交，其所做的更改就是永久性的，即使系统发生故障（如断电、浏览器崩溃），这些更改也不会丢失。

IndexedDB 的实现：
IndexedDB 承诺持久性。当 transaction.oncomplete 事件触发时，意味着事务中的所有操作都已成功写入浏览器的持久化存储机制。

浏览器存储： 浏览器会将数据写入本地磁盘。具体的实现细节可能因浏览器而异，但其目标是确保数据在 oncomplete 后是持久化的。

缓存与同步： 浏览器可能会有内部的缓存机制，但最终会将数据同步到磁盘。在 oncomplete 触发后，可以合理地假定数据已安全存储。

用户清空数据： 需要注意的是，持久性是指在正常操作下数据不会丢失。用户仍然可以通过浏览器设置手动清除站点数据（包括 IndexedDB 数据），这不属于系统故障。

总结：
IndexedDB 事务提供了强大的 ACID 特性，为客户端数据存储提供了可靠的保障。通过理解这些特性，我们可以编写出更健壮、更可靠的 Web 应用程序。

事务的最佳实践与注意事项

虽然 IndexedDB 提供了强大的事务功能，但在实际开发中，仍需遵循一些最佳实践，以避免常见的陷阱并优化性能：

1. 保持事务简短： 特别是 readwrite 事务。长时间运行的 readwrite 事务会阻塞其他同作用域的 readwrite 事务，影响应用的响应性。尽可能将相关的操作打包在一个短小的事务中。

2. 只请求必要的对象存储： 在 db.transaction() 中，只列出事务实际需要访问的对象存储。这有助于提高并发性，因为不重叠的事务可以并行执行。

3. 链式操作，避免异步跳出事务上下文： IndexedDB 事务是“自动提交”的。当事务中的所有请求都已发出，并且其回调都已执行完毕，如果没有任何错误，事务就会自动提交。如果在事务中的某个 onsuccess 回调中，你启动了一个非 IndexedDB 的异步操作（如 setTimeout, fetch），然后尝试在那个异步操作的回调中继续数据库操作，那么原有的事务可能已经完成或被浏览器自动提交了。你需要在一个事务的 IDBRequest.onsuccess 回调中继续下一个 IDBRequest 操作，以确保它们都在同一个事务中。

// 错误示例：异步跳出事务
// function wrongUpdate(db) {
//     const tx = db.transaction(['users'], 'readwrite');
//     const store = tx.objectStore('users');
//     store.get(1).onsuccess = (event) => {
//         const user = event.target.result;
//         if (user) {
//             // 错误！setTimeout 会导致事务在回调执行前提交
//             setTimeout(() => {
//                 user.name = 'New Name';
//                 store.put(user); // 这个 put 操作可能在一个新事务中，或失败
//             }, 0);
//         }
//     };
// }

// 正确示例：链式操作
function correctUpdate(db, userId, newName) {
    return new Promise((resolve, reject) => {
        const tx = db.transaction(['users'], 'readwrite');
        const store = tx.objectStore('users');

        tx.oncomplete = () => resolve("User updated.");
        tx.onerror = (e) => reject(e.target.error);
        tx.onabort = () => reject(new Error("Transaction aborted."));

        const getRequest = store.get(userId);
        getRequest.onsuccess = (event) => {
            const user = event.target.result;
            if (user) {
                user.name = newName;
                const putRequest = store.put(user);
                putRequest.onsuccess = () => {
                    console.log(`User ${userId} name updated to ${newName}.`);
                    // 所有操作已发出，事务将在下一个事件循环tick中尝试提交
                };
                putRequest.onerror = (e) => {
                    console.error("Failed to put user:", e.target.error);
                    reject(e.target.error);
                };
            } else {
                console.warn(`User ${userId} not found.`);
                reject(new Error(`User ${userId} not found.`));
            }
        };
        getRequest.onerror = (e) => {
            console.error("Failed to get user:", e.target.error);
            reject(e.target.error);
        };
    });
}
// openDatabase().then(dbInstance => correctUpdate(dbInstance, 1, 'Alice Smith')).catch(console.error);

4. 彻底的错误处理： 始终监听 transaction.onerror 和 transaction.onabort 事件。任何未捕获的错误都可能导致事务回滚，并可能留下未处理的 IDBRequest。

5. 使用 Promise 封装： 原生 IndexedDB API 是基于事件的，这使得代码冗长且难以管理异步流。使用 Promise 封装可以极大地改善代码的可读性和可维护性。许多第三方库（如 idb 库）提供了 Promise 化的 API。

// 简单的 Promise 封装
function wrapIDBRequest(request) {
    return new Promise((resolve, reject) => {
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
    });
}

async function getUserWithPromise(userId) {
    await openDatabase();
    const transaction = db.transaction(['users'], 'readonly');
    const userStore = transaction.objectStore('users');

    try {
        const user = await wrapIDBRequest(userStore.get(userId));
        console.log("Promise-based get user:", user);
        return user;
    } catch (error) {
        console.error("Error getting user with promise:", error);
        throw error;
    } finally {
        // 事务的完成/错误处理可以放在这里，或者直接依赖 Promise 链
        // 通常，transaction.oncomplete/onerror 由事务内部所有请求的完成状态决定
        // 这里只是一个简单的请求封装，事务本身的 complete/error 还需要额外处理
        // 例如：await new Promise(res => tx.oncomplete = res);
    }
}
// getUserWithPromise(1).catch(console.error);

// 更完整的 Promise 封装，包含事务级别
class IndexedDBService {
    constructor(dbName, version, onUpgradeNeeded) {
        this.dbName = dbName;
        this.version = version;
        this.onUpgradeNeeded = onUpgradeNeeded;
        this.db = null;
    }

    async open() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(this.dbName, this.version);

            request.onerror = (event) => reject(event.target.error);
            request.onsuccess = (event) => {
                this.db = event.target.result;
                resolve(this.db);
            };
            request.onupgradeneeded = this.onUpgradeNeeded;
        });
    }

    async transaction(storeNames, mode, operations) {
        if (!this.db) {
            await this.open();
        }
        return new Promise((resolve, reject) => {
            const tx = this.db.transaction(storeNames, mode);
            const results = [];

            tx.oncomplete = () => resolve(results);
            tx.onerror = (event) => reject(event.target.error);
            tx.onabort = () => reject(new Error("Transaction aborted."));

            // operations 是一个函数，接收 objectStore 实例并返回 Promise 数组
            // 允许在 operations 函数内部使用 await
            (async () => {
                try {
                    const storeMap = {};
                    for (const storeName of storeNames) {
                        storeMap[storeName] = tx.objectStore(storeName);
                    }
                    await operations(storeMap, results);
                } catch (error) {
                    console.error("Operation within transaction failed:", error);
                    tx.abort(); // 确保外部错误也能中止事务
                }
            })();
        });
    }
}

// 使用示例
// const myDbService = new IndexedDBService(DB_NAME, DB_VERSION, (event) => {
//     const dbUpgrade = event.target.result;
//     if (!dbUpgrade.objectStoreNames.contains('users')) {
//         dbUpgrade.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
//     }
//     if (!dbUpgrade.objectStoreNames.contains('products')) {
//         dbUpgrade.createObjectStore('products', { keyPath: 'productId' });
//     }
// });

// async function performComplexOperation() {
//     try {
//         await myDbService.open();
//         const result = await myDbService.transaction(['users', 'products'], 'readwrite', async (stores) => {
//             const userStore = stores['users'];
//             const productStore = stores['products'];

//             // 假设用户101已存在，更新其名称
//             const user = await wrapIDBRequest(userStore.get(101));
//             if (user) {
//                 user.name = 'Updated Alice';
//                 await wrapIDBRequest(userStore.put(user));
//                 console.log("User updated via service.");
//             } else {
//                 await wrapIDBRequest(userStore.add({ id: 101, name: 'Alice', email: 'alice@example.com' }));
//                 console.log("User added via service.");
//             }

//             // 添加一个产品
//             await wrapIDBRequest(productStore.add({ productId: 'P004', name: 'Monitor', price: 250 }));
//             console.log("Product added via service.");
//         });
//         console.log("Complex operation successful:", result);
//     } catch (error) {
//         console.error("Complex operation failed:", error);
//     }
// }
// performComplexOperation();

6. Web Workers： 对于涉及大量数据处理或长时间运行的 IndexedDB 操作，考虑将其放在 Web Worker 中执行。这样可以避免阻塞主线程，保持 UI 的响应性。IndexedDB API 可以在 Web Worker 中使用。

事务模式与并发控制机制概览表

为了更清晰地理解不同事务模式下的特性和行为，这里提供一个简要的概览表：

事务模式 | 描述 | 作用域 | 并发控制机制 | ACID 特性影响 | 典型场景