全部版块 我的主页
论坛 休闲区 十二区 休闲灌水
78 0
2025-11-20

关系型数据库事务特性

ACID是数据库事务的核心特性,确保事务执行的可靠性、数据的准确性和并发的安全性。这些特性包括原子性、一致性、隔离性和持久性。

1. 原子性:整体成功或失败

事务的所有操作被视为一个不可分割的整体,不允许部分执行。例如,在转账过程中,“扣款”和“到账”必须同时完成,否则都不会执行,以防止资金的一方被扣除而另一方未收到的情况发生。

2. 一致性:保持数据合法性

事务执行前后,数据库必须维持其完整性约束,如主键唯一性和金额非负性。这种特性依赖于原子性来确保,比如转账后双方账户的总金额保持不变,体现了数据的一致性。

3. 隔离性:并发事务独立运行

当多个事务同时执行时,每个事务的操作和数据对于其他事务应该是不可见的。这避免了一个事务读取到另一个事务未提交的数据,确保了每个事务的执行结果都是准确的。

4. 持久性:提交后不可逆

一旦事务提交,对数据库的更改就会永久保存,即使之后发生数据库崩溃或断电等故障,这些更改也不会丢失。

5. ACID特性总结

特性 核心定义 通俗场景(以电商下单支付为例)
原子性 事务操作要么全部执行,要么全部回滚,不可分割 下单流程包括“扣库存”、“减余额”、“生成订单”三步,如果余额不足导致减余额失败,库存和订单也应全部回滚,避免“库存减少但订单未生成”的情况。
一致性 事务执行前后,数据库数据符合预设规则,状态合法 下单前商品库存10件、用户余额500元,下单后库存变为9件、余额变为450元,确保总金额和库存逻辑无冲突;不会出现“库存为负”或“余额为负”的非法状态。
隔离性 并发事务相互不影响,各自操作和数据对其他事务隔离 用户A和用户B同时购买最后一件商品,A的下单事务执行时,B无法读取A未提交的“库存锁定”状态,防止两人同时下单成功导致超卖。
持久性 事务提交后,数据修改永久生效,不受后续故障影响 用户下单支付成功(事务提交)后,即使数据库服务器突然断电,重启后订单记录、库存变更、余额扣减的结果仍然存在,不会丢失。

事务隔离级别

一、隔离性失效的典型场景

在并发环境下,隔离性是最容易出现问题的特性,常见的失效情况有三种:

  • 脏读:一个事务读取到了另一个事务未提交的临时数据,如果后续事务回滚,读取的数据即为“脏数据”。
  • 不可重复读:同一事务内多次读取同一数据,由于其他事务修改并提交,导致前后读取结果不一致。
  • 幻读:同一事务内多次执行同一查询,由于其他事务插入或删除数据并提交,导致返回的记录数量发生变化。

二、解决方案:数据库隔离级别(从低到高)

通过设置不同的隔离级别,可以在并发效率和数据一致性之间找到平衡点,从而解决上述问题:

  • 读未提交(Read Uncommitted):允许读取未提交的事务数据,隔离性最低。适用于对数据一致性要求极低,追求极致并发效率的场景(如临时统计数据)。
  • 读已提交(Read Committed):只能读取已提交的事务数据,禁止脏读。适用于大多数业务场景(如电商订单查询),兼顾并发和基本一致性。
  • 可重复读(Repeatable Read):同一事务内多次读取同一数据结果一致,解决不可重复读的问题。适用于对数据一致性要求较高的场景(如金融对账),是MySQL的默认隔离级别。
  • 串行化(Serializable):事务串行执行,完全隔离,无并发问题。适用于对数据一致性要求极高的场景(如银行转账核心流程),允许牺牲并发性能。

三、其他ACID特性的常见问题与解决

原子性/持久性问题:事务提交失败、数据库崩溃

问题描述:事务执行过程中断电或数据库崩溃,导致部分操作执行、提交结果丢失。

解决方案:利用数据库日志(如MySQL的redo log、undo log),在崩溃后通过日志回滚未提交的事务、恢复已提交的事务。

一致性问题:业务逻辑错误导致数据非法

问题描述:如转账时计算错误,导致总金额变化,违反了一致性。

解决方案:在数据库层面设置约束(如主键、外键、CHECK约束),在业务层面在事务中增加校验逻辑(如转账前检查余额是否充足)。

MySQL 事务隔离级别操作

一、核心概念

MySQL 的隔离级别由变量控制(MySQL 8.0 以后推荐使用,替代旧版),默认隔离级别是REPEATABLE READ(可重复读)。

transaction_isolation

MySQL 8.0 以后推荐使用的变量控制方式,替代旧版的变量控制方法。

tx_isolation

二、实操步骤

1. 查询当前隔离级别

可以通过特定命令查询当前的隔离级别。

-- MySQL 8.0+
SELECT @@transaction_isolation;

-- MySQL 5.7 及以下
SELECT @@tx_isolation;

2. 修改隔离级别

修改隔离级别可以通过以下步骤进行:

  1. 会话级(仅当前连接有效,断开后失效):使用特定命令修改当前会话的隔离级别。
    SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别;
  2. 全局级(需重启连接生效,永久生效需改配置文件):使用特定命令修改全局隔离级别。
    SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;

隔离级别的可选值(大小写不敏感)包括:

  • 读未提交(Read Uncommitted)
    READ UNCOMMITTED
  • 读已提交(Read Committed)
    READ COMMITTED
  • 可重复读(Repeatable Read,默认)
    REPEATABLE READ
  • 串行化(Serializable)
    SERIALIZABLE

3. 永久生效(修改配置文件)

要使隔离级别永久生效,需要编辑MySQL配置文件。在Linux系统中,通常编辑/etc/my.cnf/etc/mysql/my.cnf文件。

my.cnf

my.ini

在 Windows 操作系统中,

[mysqld]

在指定节点添加:

[mysqld]
transaction-isolation = 隔离级别  # 可选值:READ-UNCOMMITTED、READ-COMMITTED、REPEATABLE-READ、SERIALIZABLE

重启 MySQL 服务使配置生效:

# Linux
systemctl restart mysqld

# Windows
net stop mysql && net start mysql

不同隔离级别的代码测试示例

测试准备:创建测试表并插入初始数据。

-- 创建用户余额表
CREATE TABLE `user_balance` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `user_id` INT NOT NULL,
  `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  UNIQUE KEY `uk_user_id` (`user_id`)
);

-- 插入测试数据(用户1余额100元,用户2余额200元)
INSERT INTO `user_balance` (`user_id`, `balance`) VALUES (1, 100.00), (2, 200.00);

测试工具:开启两个 MySQL 终端(会话 A 和会话 B),用于模拟并发事务。

1. 隔离级别:READ UNCOMMITTED(读未提交)

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 设置隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
设置隔离级别(同上) 两个会话均采用“读未提交”隔离级别
2. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始
3. 修改数据(不提交):
UPDATE user_balance SET balance = balance - 50 WHERE user_id = 1;
会话 A 减少用户1的余额50元(余额变为50元),但未提交
4. 查询用户1余额:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 可以读取到未提交的50元余额(脏读)
5. 回滚事务:
ROLLBACK;
会话 A 回滚事务,用户1的余额恢复为100元
6. 再次查询:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到回滚后的100元余额

结论:此隔离级别允许脏读,提供最低的隔离性,但具有最高的并发性能。

2. 隔离级别:READ COMMITTED(读已提交)

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 设置隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
设置隔离级别(同上) 两个会话均采用“读已提交”隔离级别
2. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始
3. 修改数据(不提交):
UPDATE user_balance SET balance = balance - 50 WHERE user_id = 1;
会话 A 减少用户1的余额50元(余额变为50元),但未提交
4. 查询用户1余额:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到原始的100元余额(没有脏读)
5. 提交事务:
COMMIT;
会话 A 提交事务,用户1的余额变为50元
6. 再次查询:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到已提交的50元余额
7. 同一事务内再次查询(不变): 若会话 B 未提交,多次查询结果一致?不! 会话 B 再次查询仍为50元(注意:READ COMMITTED 解决了脏读,但可能遇到不可重复读)

补充测试(不可重复读):

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始
2. 查询用户1余额:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到100元余额
3. 修改并提交:
UPDATE user_balance SET balance = 150 WHERE user_id = 1;
COMMIT;
会话 A 将用户1的余额更改为150元并提交
4. 同一事务内再次查询:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到150元余额(同一事务内的两次查询结果不一致,即不可重复读)

结论:解决了脏读问题,但存在不可重复读的情况,即在同一事务内读取已提交的数据时可能会发生变化。

3. 隔离级别:REPEATABLE READ(可重复读,默认)

操作步骤(测试不可重复读):

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 设置隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
设置隔离级别(同上) 两个会话均使用默认的“可重复读”隔离级别
2. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始
3. 查询用户1余额:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 读取到100元余额
4. 修改并提交:
UPDATE user_balance SET balance = 150 WHERE user_id = 1;
COMMIT;
会话 A 将用户1的余额更改为150元并提交
5. 同一事务内再次查询:
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 仍然读取到100元余额(解决了不可重复读)
6. 提交事务后查询:
COMMIT;
SELECT balance FROM user_balance WHERE user_id = 1;
会话 B 提交事务后,读取到150元余额(事务结束后可见提交的数据)

补充测试(幻读):

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始
2. 查询用户数:
SELECT COUNT(*) FROM user_balance;
会话 B 读取到2条记录(用户1、2)
3. 插入数据并提交:
INSERT INTO user_balance (
,
) VALUES (3, 300.00);
COMMIT;
会话 A 插入用户3并提交事务
4. 同一事务内再次查询:
SELECT COUNT(*) FROM user_balance;
会话 B 仍然读取到2条记录(InnoDB 优化避免了幻读)
5. 尝试插入用户3:
INSERT INTO user_balance (
,
) VALUES (3, 400.00);
会话 B 插入失败(唯一键冲突),但查询不到用户3(这是幻读的一种特殊情况,InnoDB 通过 MVCC + 间隙锁解决)

结论:解决了脏读和不可重复读的问题,InnoDB 引擎通过 MVCC(多版本并发控制)+ 间隙锁基本避免了幻读,是平衡一致性和并发性的最佳选择。

4. 隔离级别:SERIALIZABLE(串行化)

操作步骤:

操作步骤 会话 A(事务1) 会话 B(事务2) 现象说明
1. 设置隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
设置隔离级别(同上) 两个会话均使用“串行化”隔离级别
2. 开启事务:
START TRANSACTION;
START TRANSACTION;
两个事务同时开始

查询用户1余额:

SELECT balance FROM user_balance WHERE user_id = 1 FOR UPDATE;

会话 A 对用户1的记录加排他锁进行查询。

尝试修改用户1余额:

UPDATE user_balance SET balance = 200 WHERE user_id = 1;

会话 B 因为锁的存在而阻塞,等待会话 A 完成操作。

提交事务:

COMMIT;

当会话 A 提交事务后,锁被释放,会话 B 获得执行机会,成功修改用户1的余额至200元。

结论:

这种完全隔离的方式虽然解决了所有并发问题(如脏读、不可重复读、幻读),但由于事务必须串行执行,导致并发效率非常低。因此,它更适合用于低并发、高一致性需求的场景,例如银行的对账系统。

注意事项

隔离级别与锁的关系:

通常情况下,隔离级别越高,所需的锁粒度越细或持有锁的时间越长(例如,在 SERIALIZABLE 级别下,整个表可能会被锁定),这会导致并发性能显著下降。

InnoDB 存储引擎在 REPEATABLE READ 隔离级别下,利用多版本并发控制(MVCC)实现“读取时无需加锁”,从而大幅提升了并发读取的性能。

事务自动提交:

MySQL 默认设置为事务自动提交模式。如果需要手动控制事务,则需要先执行 SET autocommit=0 或者 START TRANSACTION 命令来关闭自动提交功能。

autocommit = 1

START TRANSACTION

SET autocommit = 0

MVCC 的适用场景:

MVCC 主要应用于 InnoDB 存储引擎,并且在 READ COMMITTED 和 REPEATABLE READ 隔离级别下有效。

InnoDB

READ COMMITTED

REPEATABLE READ

生产环境建议:

推荐在生产环境中默认使用 REPEATABLE READ 隔离级别,因为它能在一致性和并发性之间取得良好的平衡。

对于需要防止不可重复读取的情况(如订单状态查询),可以考虑使用 READ COMMITTED,但这可能带来幻读的风险,或者可以通过业务逻辑来规避这一问题。

只有在需要极高一致性的情况下(如金融交易),才应使用 SERIALIZABLE 隔离级别。

SERIALIZABLE

总结:

隔离级别 脏读 不可重复读 幻读 并发性能 适用场景
READ UNCOMMITTED 最高 临时统计、对一致性无要求的场景
READ COMMITTED 较高 多数业务场景(如电商查询)
REPEATABLE READ 否* 默认选择(平衡一致与并发)
SERIALIZABLE 最低 高一致性场景(如银行对账)

注:*在 InnoDB 引擎下,REPEATABLE READ 隔离级别基本能避免幻读现象。

通过上述实践操作,可以快速了解不同隔离级别的具体行为差异,从而根据实际业务需求选择最合适的隔离级别。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

相关推荐
栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群