掘金 后端 ( ) • 2024-07-01 18:05

肖哥弹架构 跟大家“弹弹” 事务,隔离,实现原理功能与业务案例 欢迎 点赞,点赞,点赞。

关注本人的公众号Solomon肖哥弹架构获取更多精彩内容

事务是数据库操作的一个单元,可以包含一个或多个数据库操作语句(如INSERTUPDATEDELETE等)。事务具有以下四个重要的属性,通常被称为 ACID 属性:

  1. 原子性(Atomicity) :事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个点。
  2. 一致性(Consistency) :事务必须保证数据库从一个一致的状态转移到另一个一致的状态。
  3. 隔离性(Isolation) :并发执行的事务之间不会互相影响。
  4. 持久性(Durability) :一旦事务提交,则其所做的修改永久保存在数据库中,即使系统发生故障。

事务实现的核心技术

多版本并发控制(MVCC)

在MVCC中,每次数据更新时,数据库不是直接覆盖原始数据,而是创建数据的新版本。这样,不同的事务可以看到数据的不同版本,具体取决于事务的隔离级别。

  1. 数据行: 假设我们有一个数据表accounts,其中有一个行,表示账户余额。
  2. 版本链: 每次更新操作都会在数据库中创建一个新的版本,并在该行中保存指向前一个版本的指针,形成一个版本链。

锁机制

锁机制用于控制并发事务对同一数据的访问,以避免数据竞争和不一致。

  1. 共享锁(Shared Lock) : 当事务读取数据时,它可能会在数据上放置共享锁,允许其他事务也读取数据,但阻止它们修改数据。
  2. 排他锁(Exclusive Lock) : 当事务需要修改数据时,它会在数据上放置排他锁,这阻止其他事务读取或修改数据,直到锁被释放。
  3. 死锁: 如果两个或多个事务相互持有对方需要的锁,就会发生死锁。数据库的死锁检测机制会识别这种情况并采取措施,比如回滚其中一个事务来解决死锁。

事务日志

  1. 日志记录: 每次数据修改前,数据库都会将更改记录到日志中。这允许在事务失败或系统崩溃时进行恢复。
  2. 写前日志(WAL) : 在执行数据修改之前,先将更改写入日志,确保即使在崩溃后也能恢复更改。
  3. 恢复: 系统重启后,可以使用日志来重做(Redo)或撤销(Undo)未完成的事务,以恢复到一致的状态。

事务保证业务数据的完整性

场景 1:数据插入的一致性保证

案例:在电子商务平台,当用户下单时,需要同时插入订单信息和订单详情。

START TRANSACTION;

INSERT INTO orders (user_id, order_date, status) VALUES (1, NOW(), 'Processing');
INSERT INTO order_details (order_id, product_id, quantity, price) 
VALUES (LAST_INSERT_ID(), 101, 2, 24.50);

COMMIT;

如果订单详情插入失败,事务将回滚,订单信息也会被撤销。

案例1:转账操作

案例:从一个银行账户向另一个账户转账时,需要保证资金的一致性。

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;

COMMIT;

如果任一更新失败,事务将回滚,确保两个账户的资金都保持不变。

案例2:库存更新

案例:当销售产品时,需要同时减少库存数量和记录销售信息。

START TRANSACTION;

UPDATE products SET stock = stock - 1 WHERE product_id = 101;
INSERT INTO sales (product_id, quantity, sale_date) VALUES (101, 1, NOW());

COMMIT;

如果销售记录插入失败,事务将回滚,库存也不会减少。

案例3:订单状态变更

案例:在订单处理过程中,如果支付失败,需要回滚订单状态。

START TRANSACTION;

UPDATE orders SET status = 'Paid', payment_date = NOW() WHERE order_id = 1;
-- 假设支付处理的代码在这里,如果失败则执行以下语句:
-- ROLLBACK;

COMMIT;

如果支付失败,事务将回滚,订单状态将恢复到之前的状态。

案例4:数据的批量更新

案例:在进行数据分析时,需要对一批数据进行处理,确保数据的一致性。

START TRANSACTION;

UPDATE products SET price = price * 0.9 WHERE category_id = 3;
-- 进行其他相关更新操作...

COMMIT;

如果任何更新失败,事务将回滚,所有批量更新将被撤销。

案例5:分布式事务

案例:在微服务架构中,需要跨多个服务进行数据更新。

XA START 'my_transaction';
-- 在不同的服务中执行SQL操作...
XA END 'my_transaction';
XA PREPARE 'my_transaction';
-- 在所有服务中确认操作无误后,提交事务...
XA COMMIT 'my_transaction';

使用 XA 事务可以确保跨多个数据库连接或服务的事务一致性。

四种事务隔离级别场景案例

1. READ UNCOMMITTED(读未提交)

场景:当隔离级别设置为 READ UNCOMMITTED 时,事务可以读取到其他未提交事务的更改。这可以用于需要高并发读操作,但不在乎脏读的场景。

SQL

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

START TRANSACTION;

SELECT * FROM accounts WHERE account_id = 1;
-- 可以读取到其他事务未提交的数据

UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;

COMMIT;

注意:这种隔离级别很少使用,因为它容易受到脏读的影响。

业务- 报告生成(READ UNCOMMITTED)

业务说明:在某些业务场景中,如生成报告或临时的数据分析,可能需要读取大量数据,而这些数据的精确一致性并不是立即需要的。例如,一个零售商可能需要生成前一天的销售报告,而这些报告不需要实时反映最新的交易数据。

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

START TRANSACTION;

-- 读取前一天的销售记录
SELECT product_id, SUM(quantity) AS total_sold
FROM sales
WHERE sale_date BETWEEN '2024-06-01 00:00:00' AND '2024-06-02 00:00:00'
GROUP BY product_id;

-- 进行其他报告相关的数据汇总操作
-- ...

COMMIT;

在这个场景中,READ UNCOMMITTED 隔离级别允许报告生成事务读取到其他未提交事务的更改,这对于最终的报告结果可能没有影响,因为报告是基于历史数据生成的,不需要实时反映所有最新的更改。

业务- 实时数据监控(READ UNCOMMITTED)

业务说明:在某些业务监控系统中,可能需要实时地展示一些数据指标,但这些指标不需要严格的事务一致性。例如,一个网站的访问统计系统可能需要展示实时的用户访问量,而这些数据主要用于观察趋势,不需要精确到每一笔未提交的事务。

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

START TRANSACTION;

-- 读取最近用户访问数据,用于实时监控
SELECT COUNT(*) AS visitor_count FROM page_visits;

-- 可能还有一些其他实时数据的读取操作
-- ...

COMMIT;

在这个场景中,使用 READ UNCOMMITTED 隔离级别允许监控系统快速读取数据,而不需要等待其他可能正在进行的事务提交。这对于实时监控仪表板来说可能是可接受的,因为它们更关注数据的动态变化趋势而非每一笔数据的绝对准确性。

2. READ COMMITTED(读已提交)

场景:当隔离级别设置为 READ COMMITTED 时,事务只能读取到其他事务提交后的更改。这可以用于大多数需要避免脏读的常规业务场景。

SQL

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

SELECT * FROM orders WHERE order_id = 1;
-- 只能读取到已提交的数据

UPDATE orders SET status = 'Shipped' WHERE order_id = 1;

COMMIT;

这个级别可以避免脏读,但仍然可能遇到不可重复读的问题。

业务- 银行账户转账(READ COMMITTED)

业务说明:在银行系统中,客户A向客户B转账。必须确保转账操作不会读取到其他未提交的交易数据,避免读取到不一致的数据状态。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

-- 检查客户A的账户余额是否充足
SELECT balance FROM accounts WHERE account_id = A;

-- 执行扣款操作
UPDATE accounts SET balance = balance - 100 WHERE account_id = A;

-- 检查扣款后客户A的余额是否小于0
SELECT balance FROM accounts WHERE account_id = A;

-- 如果扣款成功,给客户B加款
UPDATE accounts SET balance = balance + 100 WHERE account_id = B;

COMMIT;

在这个案例中,READ COMMITTED 确保了在两次 SELECT 之间,如果有其他事务修改了客户A的账户余额并提交了,那么第二次 SELECT 能读取到最新数据,避免超额支付。

业务- 医疗记录系统(READ COMMITTED)

业务说明:在医疗记录系统中,医生需要读取病人的病历,并根据最新的检查结果更新病历。系统需要避免脏读,但对并发性能有较高要求。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

-- 读取病人的病历信息
SELECT * FROM medical_records WHERE patient_id = Y;

-- 根据最新的检查结果更新病历
-- ...

UPDATE medical_records SET ... WHERE patient_id = Y;

COMMIT;

READ COMMITTED 隔离级别允许医生读取到最新的检查结果,同时避免了读取到其他医生未提交的更改。

3. REPEATABLE READ(可重复读)

场景:当隔离级别设置为 REPEATABLE READ 时,在一个事务范围内,多次读取同一数据的结果是一致的,即使有其他事务在该事务过程中提交了更新。这适用于需要确保数据在事务期间一致性的场景。

SQL

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

START TRANSACTION;

SELECT * FROM products WHERE product_id = 101;
-- 在事务过程中,多次读取该记录,结果一致

-- 假设此处执行了一些其他业务逻辑

SELECT * FROM products WHERE product_id = 101;
-- 再次读取,结果与第一次相同

UPDATE products SET stock = stock - 1 WHERE product_id = 101;

COMMIT;

这个级别可以避免脏读和不可重复读,但仍然可能遇到幻读。

业务- 电子商务库存管理(REPEATABLE READ)

业务说明:在线商城在处理订单时,需要确保在订单处理过程中库存数量的一致性,防止超卖现象发生。

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

START TRANSACTION;

-- 检查商品的库存数量
SELECT stock FROM products WHERE product_id = P;

-- 如果库存充足,继续处理订单
-- ...

-- 再次检查商品的库存数量,确保在事务期间没有变化
SELECT stock FROM products WHERE product_id = P;

-- 如果检查通过,减少库存数量
UPDATE products SET stock = stock - X WHERE product_id = P;

COMMIT;

REPEATABLE READ 隔离级别确保了在整个事务过程中,即使其他事务对库存数量做了更改并提交,当前事务再次检查时仍然能看到第一次读取时的库存数据,从而防止超卖。

业务- 电信计费系统(REPEATABLE READ)

业务说明:在电信计费系统中,需要在月底进行账单生成。系统需要确保在账单生成过程中,用户的消费记录不会因为其他并发的计费操作而改变。

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

START TRANSACTION;

-- 读取用户的通话记录和数据使用情况
SELECT * FROM usage_records WHERE user_id = Z;

-- 根据使用情况计算费用
-- ...

-- 再次读取用户的记录,确保在计算费用过程中没有其他更改
SELECT * FROM usage_records WHERE user_id = Z;

-- 更新用户的账单信息
UPDATE billing SET ... WHERE user_id = Z;

COMMIT;

REPEATABLE READ 隔离级别确保了在账单生成过程中,即使有其他计费操作并发执行,当前事务仍然能看到第一次读取时的记录。

4. SERIALIZABLE(串行化)

场景:当隔离级别设置为 SERIALIZABLE 时,事务会锁定涉及的所有数据,防止其他事务并发访问,从而提供最高级别的隔离。适用于需要严格数据一致性的场景,但会牺牲并发性能。

SQL

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

START TRANSACTION;

SELECT * FROM transactions WHERE transaction_id = 2023;
-- 在事务过程中,其他事务不能访问这些数据

-- 执行一些复杂的业务逻辑,涉及多个表和数据更新

UPDATE transactions SET status = 'Completed' WHERE transaction_id = 2023;

COMMIT;

这个级别可以避免脏读、不可重复读和幻读,但会大大降低系统的并发能力。

业务- 金融服务中的账户审计(SERIALIZABLE)

业务说明:在金融服务中,需要进行账户审计,确保在审计过程中对账户的所有访问都是串行的,以避免并发操作导致的数据不一致。

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

START TRANSACTION;

-- 选择需要审计的账户记录
SELECT * FROM accounts WHERE account_id = X FOR UPDATE;

-- 进行审计操作,可能包括复杂的数据验证和记录
-- ...

COMMIT;

SERIALIZABLE 隔离级别通过锁定涉及的所有数据,确保了在审计过程中没有其他事务可以同时访问这些数据,从而保证了审计的准确性。

业务- 金融交易处理(SERIALIZABLE)

业务说明:在金融行业中,交易的准确性和完整性至关重要。例如,当执行一笔股票交易时,必须确保交易过程中不会与其他交易冲突,导致数据不一致或违反业务规则。

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

START TRANSACTION;

-- 查询股票当前的可用数量
SELECT available_shares FROM stocks WHERE stock_id = X;

-- 检查是否有足够的股票进行交易
IF (available_shares >= Y) THEN
  -- 执行买入操作,减少股票数量
  UPDATE stocks SET available_shares = available_shares - Y WHERE stock_id = X;
  
  -- 记录交易详情到交易表
  INSERT INTO transactions (user_id, stock_id, share_count, transaction_type, transaction_time)
  VALUES (Z, X, Y, 'BUY', NOW());
  
  -- 提交事务
  COMMIT;
ELSE
  -- 如果没有足够的股票,回滚事务
  ROLLBACK;
END IF;

在这个场景中,SERIALIZABLE 隔离级别确保了在检查股票数量和执行买入操作的过程中,不会有其他事务能够修改股票的可用数量,从而避免了并发事务导致的数据不一致问题。

历史数据库文章