IndexedDB 的事务(Transaction):ACID 特性与并发控制机制

各位开发者,大家好!

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

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

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

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

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

核心概念:

  • 数据库(Database): 通过名称和版本号进行识别。
  • 对象存储(Object Store): 类似于关系型数据库中的表,但存储的是 JavaScript 对象。每个对象存储都有一个名称,并且可以选择指定一个 keyPathautoIncrement
  • 键(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 对象。你需要监听 IDBRequestonsuccessonerror 事件来处理每个操作的结果。在事务中,所有的操作都必须在同一个事件循环任务中进行调度,或者通过 request.onsuccess 的回调进行链式调用。 如果你在事务中间执行了一个异步操作(如 setTimeoutfetch),然后尝试在那个异步操作的回调中继续数据库操作,那么原有的事务可能已经完成或被浏览器自动提交了。

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: '[email protected]' },
//     { 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: '[email protected]' }, // 故意使用重复email
//     { productId: 'PROD002', name: 'Mouse', price: 30 }
// ).then(msg => console.log(msg)).catch(err => console.error(err));

在示例2中,如果 [email protected] 已经存在于 users 对象存储中(并且 emailIndexunique 的),那么 userRequest.onerror 会被触发,导致整个 readwrite 事务回滚。即使 productRequest 成功完成了添加产品的操作,这个产品也不会被持久化到数据库中。这就是原子性的体现。

2. 一致性(Consistency)

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

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

  • 原子性保障: 由于原子性,事务要么完全成功,要么完全失败,这避免了因部分操作成功而导致的数据不一致。
  • 结构约束: 对象存储的 keyPathautoIncrement 属性以及索引的 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 ATransaction 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 ABusers 存储不重叠,因此它会并发执行。
  • Transaction D 是一个 readonly 事务,它会读取 users 存储。它将看到 Transaction A 开始时的数据库快照('initial')。即使 Transaction ATransaction 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.onerrortransaction.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: '[email protected]' }));
    //                 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 特性影响 典型场景

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注