目录

第六部分:事务(Transaction)详解

6.1 事务概述

事务是 IndexedDB 中所有数据操作的基本执行单元。理解事务的工作机制,是正确使用 IndexedDB 的关键。事务保证了数据操作的原子性、一致性、隔离性和持久性(ACID)。

6.1.1 为什么需要事务

想象一个转账场景:从账户 A 转出 100 元到账户 B。这需要两个操作:

1. 从账户 A 扣除 100 元
2. 向账户 B 增加 100 元

如果第一步成功但第二步失败,数据就会不一致。事务确保这两个操作要么都成功,要么都失败。

6.1.2 IndexedDB 事务的特性

特性 说明
原子性 事务中的所有操作要么全部成功,要么全部回滚
一致性 事务执行前后,数据库保持有效状态
隔离性 不同事务之间互不干扰
持久性 事务提交后,数据永久保存

6.2 事务类型

6.2.1 readonly(只读事务)

只读事务用于读取数据,可以并发执行:

const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
 
// 多个只读事务可以同时进行
const tx1 = db.transaction(['users'], 'readonly');
const tx2 = db.transaction(['users'], 'readonly');
// tx1 和 tx2 可以同时执行

6.2.2 readwrite(读写事务)

读写事务用于修改数据,同一对象存储空间同时只能有一个读写事务:

const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
// 以下会等待上一个读写事务完成
const tx2 = db.transaction(['users'], 'readwrite');
// tx2 会排队等待

6.2.3 versionchange(版本变更事务)

versionchange 在数据库升级时自动创建,用于修改数据库结构:

request.onupgradeneeded = function(event) {
  const db = event.target.result;
  const transaction = event.target.transaction; // versionchange 事务
 
  // 可以创建/删除对象存储和索引
  db.createObjectStore('newStore', { keyPath: 'id' });
};

6.3 创建和使用事务

6.3.1 基本用法

// 单对象存储事务
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
 
// 多对象存储事务
const transaction = db.transaction(['users', 'orders'], 'readwrite');
const userStore = transaction.objectStore('users');
const orderStore = transaction.objectStore('orders');
 
// 使用字符串数组或字符串列表
const transaction = db.transaction(['users', 'orders', 'products'], 'readwrite');

6.3.2 事务生命周期

const transaction = db.transaction(['users'], 'readwrite');
 
// 1. 事务开始
console.log('事务已创建');
 
// 2. 执行操作
const store = transaction.objectStore('users');
store.add({ id: 1, name: '张三' });
store.add({ id: 2, name: '李四' });
 
// 3. 监听事件
transaction.oncomplete = function(event) {
  console.log('事务完成,所有操作已提交');
};
 
transaction.onerror = function(event) {
  console.error('事务错误,所有操作已回滚:', event.target.error);
};
 
transaction.onabort = function(event) {
  console.log('事务被中止');
};
 
// 4. 事务自动提交(当所有请求完成后)

6.3.3 事务的自动提交

IndexedDB 事务在以下情况自动提交:

// 错误示例:事务已自动提交
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
store.add({ id: 1, name: '张三' });
 
// 等待一段时间后...
setTimeout(() => {
  // 错误!事务可能已经自动提交
  store.add({ id: 2, name: '李四' }); // TransactionInactiveError
}, 100);

6.4 事务错误处理

6.4.1 错误传播

事务中的错误会冒泡到事务对象:

const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
// 第一个操作成功
store.add({ id: 1, name: '张三' });
 
// 第二个操作失败(主键重复)
store.add({ id: 1, name: '重复' }); // ConstraintError
 
// 第三个操作不会执行(事务已中止)
// store.add({ id: 2, name: '李四' });
 
transaction.onerror = function(event) {
  // 这里会收到 ConstraintError
  console.error('事务因错误而中止:', event.target.error);
};
 
transaction.oncomplete = function() {
  // 不会触发,因为事务已中止
  console.log('事务完成');
};

6.4.2 防止事务中止

有时我们希望某个操作失败不影响整个事务:

const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
// 捕获单个请求的错误,防止传播到事务
const request1 = store.add({ id: 1, name: '张三' });
request1.onerror = function(event) {
  event.preventDefault(); // 阻止错误冒泡
  console.log('添加失败,但事务继续');
};
 
// 即使 request1 失败,request2 仍会执行
const request2 = store.add({ id: 2, name: '李四' });

6.4.3 手动中止事务

const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
 
store.add({ id: 1, name: '张三' });
 
// 手动中止事务
if (someCondition) {
  transaction.abort();
}
 
transaction.onabort = function() {
  console.log('事务已手动中止,数据未保存');
};

6.5 多对象存储事务

6.5.1 跨存储操作

// 创建订单时同时更新用户和库存
async function createOrder(order) {
  const transaction = db.transaction(['orders', 'users', 'products'], 'readwrite');
 
  const orderStore = transaction.objectStore('orders');
  const userStore = transaction.objectStore('users');
  const productStore = transaction.objectStore('products');
 
  // 1. 检查用户是否存在
  const user = await new Promise((resolve, reject) => {
    const request = userStore.get(order.userId);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
 
  if (!user) {
    throw new Error('用户不存在');
  }
 
  // 2. 检查并扣减库存
  for (const item of order.items) {
    const product = await new Promise((resolve, reject) => {
      const request = productStore.get(item.productId);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
 
    if (!product || product.stock < item.quantity) {
      throw new Error(`库存不足: ${item.productId}`);
    }
 
    product.stock -= item.quantity;
    await new Promise((resolve, reject) => {
      const request = productStore.put(product);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
 
  // 3. 创建订单
  const orderId = await new Promise((resolve, reject) => {
    const request = orderStore.add(order);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
 
  // 4. 更新用户订单列表
  user.orders = user.orders || [];
  user.orders.push(orderId);
  await new Promise((resolve, reject) => {
    const request = userStore.put(user);
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
 
  // 等待事务完成
  return new Promise((resolve, reject) => {
    transaction.oncomplete = () => resolve(orderId);
    transaction.onerror = () => reject(transaction.error);
  });
}

6.6 事务并发控制

6.6.1 并发规则

事务1 事务2 是否并发
readonly readonly
readonly readwrite 是(只读先开始)
readwrite readonly 是(读写先开始)
readwrite readwrite 否(排队)

6.6.2 死锁避免

虽然 IndexedDB 事务通常不会死锁,但良好的实践很重要:

// 好的实践:总是按相同顺序访问对象存储
function operationA() {
  return db.transaction(['users', 'orders', 'products'], 'readwrite');
}
 
function operationB() {
  return db.transaction(['users', 'orders', 'products'], 'readwrite');
}
// 两个操作按相同顺序访问,避免死锁

6.7 本章小结

本章深入介绍了 IndexedDB 的事务机制:

继续阅读 第七部分:游标(Cursor)与范围查询