Skip to content

1)事务

一句话

一致性是目标,事务是手段。

一、事务(Transaction)的概念和特性

事务是一组操作的集合,要么全部成功,要么全部失败。
假如事务中的某一步操作失败,系统会回滚之前的所有操作,让数据恢复到事务开始前的状态。

事务的特性:ACID

事务具有四大特性,简称 ACID

  1. 原子性(Atomicity):

    • 事务是不可分割的,所有操作要么全部成功,要么全部失败。
    • 比喻:买东西时,钱和商品的交换是原子性的,要么交易成功,要么双方恢复原样。
  2. 一致性(Consistency):

    • 事务完成后,数据必须满足系统定义的规则,不能出现不符合规则的状态。
    • 比喻:银行转账后,总金额不变。
  3. 隔离性(Isolation):

    • 不同事务之间互不干扰,事务执行的结果不受其他事务的影响。
    • 比喻:超市里,两个顾客结账互不影响。
  4. 持久性(Durability):

    • 一旦事务提交成功,数据的变更将永久保存,即使系统崩溃也不会丢失。
    • 比喻:存款成功后,银行的账本永远记下这笔钱。

二、事务的生命周期

事务的生命周期包括四个阶段:

  1. 开始事务。
  2. 执行操作。
  3. 提交事务或回滚事务。
  4. 结束事务。

三、事务的分类:它的类型有哪些?

按执行模式分类

  1. 手动事务
    • 开发人员手动控制事务的开始、提交和回滚。
    • 示例:BEGIN, COMMIT, ROLLBACK
  2. 自动事务
    • 数据库管理系统自动处理事务,开发人员无需显式控制。
    • 示例:简单的 INSERTUPDATE

按事务操作范围分类

  1. 本地事务
    • 在单个数据库上执行操作。
    • 示例:MySQL 的事务。
  2. 分布式事务
    • 涉及多个数据库或服务,通常需要两阶段提交协议(2PC)或三阶段提交协议(3PC)。
    • 示例:微服务架构中的跨服务操作。

按隔离级别分类

(解决多个事务并发执行的问题)

如果没有指定隔离级别,数据库就会使用默认的隔离级别。在 MySQL 中,如果使用 InnoDB,默认的隔离级别是 Repeatable Read。

  1. 读未提交(Read Uncommitted):最低隔离级别,可能会读到未提交的数据。

    在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是 脏读(Dirty Read)。

  2. 读已提交(Read Committed):只能读到已提交的数据。

    在 Read Committed 隔离级别下,一个事务不会读到另一个事务还没有提交的数据,但可能会遇到 不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么,在第一个事务中,两次读取的数据就可能不一致。

  3. 可重复读(Repeatable Read):保证一个事务中的多次读操作一致。

    在 Repeatable Read 隔离级别下,一个事务可能会遇到 幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且,再次读取同一条记录,它就神奇地出现了。

  4. 串行化(Serializable):最高隔离级别,事务完全串行化执行。

    Serializable 是最严格的隔离级别。在 Serializable 隔离级别下,所有事务按照次序依次执行,因此,脏读、不可重复读、幻读都不会出现。虽然 Serializable 隔离级别下的事务具有最高的安全性,但是,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。如果没有特别重要的情景,一般都不会使用 Serializable 隔离级别

提示:脏读、不可重复读、幻读的出现场景

现象发生条件结果举例
脏读事务 A 修改数据但未提交,事务 B 读取该数据事务 B 可能读到尚未提交的脏数据事务 A 扣款操作未提交,事务 B 查询余额并开始转账,最终事务 A 回滚导致事务 B 操作基于错误数据
不可重复读事务 A 多次读取同一数据,事务 B 在此期间修改并提交数据事务 A 读取数据时,数据可能发生变化事务 A 查询商品库存为 10,事务 B 更新库存为 5 并提交,事务 A 再次查询时,库存变为 5
幻读事务 A 执行查询,事务 B 插入或删除满足查询条件的数据事务 A 查询的结果集发生变化事务 A 查询金额大于 100 的订单,查询结果为空;事务 B 插入一笔金额为 120 的订单,事务 A 再次查询时发现该订单

隔离级别从 读未提交串行化,随着级别的提高,系统的并发性能逐渐下降,但数据一致性和安全性得到了增强。
一般情况下,读未提交串行化 的隔离级别很少使用,读已提交(RC)可重复读(RR) 是较为常见的隔离级别。

按使用场景分类

  1. 单操作事务:简单的一次增删改操作。
  2. 批处理事务:需要多个步骤共同完成的任务。
  3. 长事务:需要长时间运行,通常用在大型数据处理或流式计算中。

四、事务的使用:它是如何使用的?

数据库中的事务使用

  1. SQL 示例(以 MySQL 为例)

    sql
    START TRANSACTION; -- 开始事务
    UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 扣款
    UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 收款
    COMMIT; -- 提交事务
  2. 事务回滚示例

    sql
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE id = 2;
    ROLLBACK; -- 发生异常,撤销所有更改

在后端代码中的使用

示例 1:Java 示例(Spring JDBC)

java
@Transactional
public void transferMoney(int fromAccount, int toAccount, int amount) {
    accountRepository.debit(fromAccount, amount);
    accountRepository.credit(toAccount, amount);
}

示例 2:Go 示例(使用 database/sql 包)

go
tx, err := db.Begin()
if err != nil {
    log.Fatal(err)
}
defer tx.Rollback()

_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", 100, 1)
if err != nil {
    log.Fatal(err)
}

_, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", 100, 2)
if err != nil {
    log.Fatal(err)
}

if err := tx.Commit(); err != nil {
    log.Fatal(err)
}

示例 3:使用 MySQL 和 Express 的事务管理

假设我们使用 mysql2sequelize 等库来连接数据库,在 Express 路由中执行事务。

1. 使用 mysql2 库管理事务

首先需要安装 mysql2

bash
npm install mysql2

然后在 Express 路由中进行事务控制:

javascript
const express = require("express");
const mysql = require("mysql2");
const app = express();

// 创建 MySQL 连接池
const pool = mysql.createPool({
  host: "localhost",
  user: "root",
  password: "password",
  database: "bank",
});

// 转账 API
app.post("/transfer", (req, res) => {
  const { fromAccount, toAccount, amount } = req.body;

  // 开始一个新的事务
  pool.getConnection((err, connection) => {
    if (err) {
      return res.status(500).json({ error: "Database connection failed" });
    }

    connection.beginTransaction((err) => {
      if (err) {
        connection.release();
        return res.status(500).json({ error: "Transaction failed to start" });
      }

      // 扣款
      connection.query(
        "UPDATE accounts SET balance = balance - ? WHERE id = ?",
        [amount, fromAccount],
        (err, results) => {
          if (err) {
            return connection.rollback(() => {
              connection.release();
              res.status(500).json({ error: "Transaction error during debit" });
            });
          }

          // 收款
          connection.query(
            "UPDATE accounts SET balance = balance + ? WHERE id = ?",
            [amount, toAccount],
            (err, results) => {
              if (err) {
                return connection.rollback(() => {
                  connection.release();
                  res
                    .status(500)
                    .json({ error: "Transaction error during credit" });
                });
              }

              // 提交事务
              connection.commit((err) => {
                if (err) {
                  return connection.rollback(() => {
                    connection.release();
                    res
                      .status(500)
                      .json({ error: "Transaction commit failed" });
                  });
                }
                connection.release();
                res.status(200).json({ message: "Transaction successful" });
              });
            }
          );
        }
      );
    });
  });
});

// 启动 Express 服务
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

在这个例子中:

  • 我们创建了一个 MySQL 连接池来连接数据库。
  • 使用事务控制开始、执行操作、提交或回滚操作。
  • 如果在执行任何查询时发生错误,事务会回滚,确保数据库的原子性。

2. 使用 Sequelize 库管理事务

Sequelize 是一个 ORM 库,可以更方便地管理事务。 首先安装 Sequelize:

bash
npm install sequelize mysql2

然后在 Express 路由中使用 Sequelize 进行事务管理:

javascript
const express = require("express");
const { Sequelize, DataTypes } = require("sequelize");
const app = express();

// 创建 Sequelize 实例
const sequelize = new Sequelize("mysql://root:password@localhost:3306/bank");

// 定义 Account 模型
const Account = sequelize.define("Account", {
  id: { type: DataTypes.INTEGER, primaryKey: true },
  balance: DataTypes.INTEGER,
});

// 转账 API
app.post("/transfer", async (req, res) => {
  const { fromAccount, toAccount, amount } = req.body;

  const t = await sequelize.transaction(); // 开始事务

  try {
    // 扣款
    const debitAccount = await Account.findByPk(fromAccount, {
      transaction: t,
    });
    if (!debitAccount || debitAccount.balance < amount) {
      throw new Error("Insufficient balance");
    }
    debitAccount.balance -= amount;
    await debitAccount.save({ transaction: t });

    // 收款
    const creditAccount = await Account.findByPk(toAccount, { transaction: t });
    if (!creditAccount) {
      throw new Error("Account not found");
    }
    creditAccount.balance += amount;
    await creditAccount.save({ transaction: t });

    // 提交事务
    await t.commit();
    res.status(200).json({ message: "Transaction successful" });
  } catch (error) {
    // 回滚事务
    await t.rollback();
    res.status(500).json({ error: error.message });
  }
});

// 启动 Express 服务
app.listen(3000, () => {
  console.log("Server is running on port 3000");
});

在这个示例中:

  • 使用 Sequelize 创建了一个 Account 模型。
  • 使用 sequelize.transaction() 来管理事务。
  • 在事务中进行余额的更新,遇到错误时通过 t.rollback() 回滚事务。

分布式事务的使用

  • 基于两阶段提交(2PC)

    1. 准备阶段:所有参与者锁定资源,准备提交。
    2. 提交阶段:如果所有参与者都准备成功,提交事务,否则回滚。
  • 基于消息队列(MQ)的最终一致性

    1. 将事务结果以消息形式写入队列。
    2. 消费者处理消息,完成操作,确保最终一致性。

五、事务的注意点:它有什么需要注意的?

  1. 性能影响

    • 长事务会锁定资源,可能导致其他事务等待,影响性能。
    • 解决方案:将事务范围缩小到必要的部分。

解决方案:将事务范围缩小到必要的部分

  1. 精确划分事务范围

    • 只对关键操作进行事务保护,避免将非关键操作(如日志记录、缓存更新等)放入事务中。
    • 避免冗余的数据库查询,确保每个查询和更新都必要。
  2. 使用短小的事务

    • 缩短事务执行时间,避免在事务内进行复杂计算或外部 API 调用。
    • 尽早提交或回滚事务,避免长时间持有锁。
  3. 分批处理大任务

    • 批量操作拆分为多个小事务,分别处理每个小任务,减少锁定时间。
    • 使用分页查询和更新,减少一次性处理大量数据导致的事务锁定。
  4. 异步操作与事务结合

    • 将非关键操作(如日志记录、缓存更新等)转为异步操作,避免事务中不必要的等待。
    • 分离读写操作,读取操作可以在事务外进行,写入操作仅在必要时使用事务。
  5. 避免长时间持有锁

    • 加锁粒度细化,采用行级锁而非表级锁,减少对其他事务的影响。
    • 使用乐观锁,只有在提交时检查数据是否发生冲突。
  6. 合理配置数据库隔离级别

    • 选择合适的隔离级别,如使用读已提交而不是可重复读,以减少锁竞争。
  7. 使用数据库特性进行优化

    • 优化数据库索引,减少查询和更新的耗时,缩短事务执行时间。
    • 优化数据库连接池配置,减少事务等待数据库连接的时间。
  1. 死锁问题

    • 两个事务因资源互相等待而无法完成。
    • 解决方案:设计良好的锁顺序和超时策略。

解决方案:设计良好的锁顺序和超时策略

  1. 设计良好的锁顺序

    • 统一锁的顺序,确保所有事务对资源的访问顺序一致。
    • 最小化锁的持有时间,减少在锁定资源期间进行复杂操作或等待。
  2. 使用超时策略

    • 设置事务超时,确保事务在规定时间内完成,否则主动回滚。
    • 设置锁超时,避免事务在等待锁时一直阻塞,达到超时后回滚事务。
  3. 使用乐观锁而非悲观锁

    • 使用乐观锁,在提交时检查数据是否被修改,避免死锁。
    • 避免使用悲观锁,减少死锁的发生风险。
  4. 避免长时间的事务

    • 保持事务尽量简短,避免长时间锁定资源。
    • 将大事务拆分为多个小事务,并确保每个小事务尽早提交。
  5. 使用数据库的死锁检测机制

    • 利用数据库自带的死锁检测机制,自动回滚其中一个事务解除死锁。
    • 分析数据库返回的死锁信息,优化应用逻辑。
  6. 死锁预防(预防死锁的算法)

    • 使用资源分配图或等待图来检查事务是否可能引发死锁,避免死锁发生。
  7. 定期检查并手动解决死锁

    • 定期监控系统事务,发现长时间处于等待状态的事务,手动识别并解决死锁问题。
  1. 隔离级别选择

    • 高隔离级别会降低并发性能,需要权衡数据一致性和性能之间的关系。
  2. 分布式事务的复杂性

    • 分布式事务因网络延迟、失败风险高,需要引入补偿机制或最终一致性模型。

解决方案:补偿机制或最终一致性模型

TODO

  1. 幂等性
    • 确保事务操作在重试时不会引起副作用。
    • 示例:向用户账户添加积分时,确保重复请求不会导致重复加分。

六、事务的扩展方向

现代事务模型

  1. 乐观锁与悲观锁
    • 乐观锁:假设没有冲突,提交时验证。
    • 悲观锁:在操作前锁定资源。
  2. 无锁事务
    • 一些新型数据库使用无锁算法(如 MVCC,Multi-Version Concurrency Control)实现高并发性能。

事务与微服务

  • 微服务架构中,事务多用Saga 模式TCC(Try-Confirm-Cancel)模式 处理分布式事务。