各位开发者,大家好!
今天,我们将深入探讨 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)是一系列作为单个逻辑工作单元执行的操作。这些操作要么全部成功并提交,要么全部失败并回滚,从而使数据库保持一致状态。
为什么要使用事务?
- 数据完整性: 确保一组相关的操作作为一个整体执行,避免部分成功部分失败导致的数据损坏。
- 数据一致性: 保证在事务开始和结束时,数据库都处于一个有效且一致的状态。
- 并发控制: 管理多个并发操作,防止它们相互干扰,导致脏读、不可重复读或幻读等问题。
创建事务:db.transaction(storeNames, mode)
在 IndexedDB 中,事务通过 IDBDatabase 对象的 transaction() 方法创建。
storeNames(必选): 一个字符串或字符串数组,表示此事务将访问的所有对象存储的名称。这是非常重要的,因为它定义了事务的作用域,并影响并发行为。mode(可选): 事务的模式,可以是'readonly'、'readwrite'或'versionchange'。默认是'readonly'。
事务模式详解:
-
'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)); -
'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)); - 用途: 用于在一个或多个对象存储中读取和修改数据(
-
'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: '[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 对象存储中(并且 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 提供了强大的事务功能,但在实际开发中,仍需遵循一些最佳实践,以避免常见的陷阱并优化性能:
-
保持事务简短: 特别是
readwrite事务。长时间运行的readwrite事务会阻塞其他同作用域的readwrite事务,影响应用的响应性。尽可能将相关的操作打包在一个短小的事务中。 -
只请求必要的对象存储: 在
db.transaction()中,只列出事务实际需要访问的对象存储。这有助于提高并发性,因为不重叠的事务可以并行执行。 -
链式操作,避免异步跳出事务上下文: 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); -
彻底的错误处理: 始终监听
transaction.onerror和transaction.onabort事件。任何未捕获的错误都可能导致事务回滚,并可能留下未处理的IDBRequest。 -
使用 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(); -
Web Workers: 对于涉及大量数据处理或长时间运行的 IndexedDB 操作,考虑将其放在 Web Worker 中执行。这样可以避免阻塞主线程,保持 UI 的响应性。IndexedDB API 可以在 Web Worker 中使用。
事务模式与并发控制机制概览表
为了更清晰地理解不同事务模式下的特性和行为,这里提供一个简要的概览表:
| 事务模式 | 描述 | 作用域 | 并发控制机制 | ACID 特性影响 | 典型场景 |
|---|