ACID是数据库事务的核心特性,确保事务执行的可靠性、数据的准确性和并发的安全性。这些特性包括原子性、一致性、隔离性和持久性。
事务的所有操作被视为一个不可分割的整体,不允许部分执行。例如,在转账过程中,“扣款”和“到账”必须同时完成,否则都不会执行,以防止资金的一方被扣除而另一方未收到的情况发生。
事务执行前后,数据库必须维持其完整性约束,如主键唯一性和金额非负性。这种特性依赖于原子性来确保,比如转账后双方账户的总金额保持不变,体现了数据的一致性。
当多个事务同时执行时,每个事务的操作和数据对于其他事务应该是不可见的。这避免了一个事务读取到另一个事务未提交的数据,确保了每个事务的执行结果都是准确的。
一旦事务提交,对数据库的更改就会永久保存,即使之后发生数据库崩溃或断电等故障,这些更改也不会丢失。
| 特性 | 核心定义 | 通俗场景(以电商下单支付为例) |
|---|---|---|
| 原子性 | 事务操作要么全部执行,要么全部回滚,不可分割 | 下单流程包括“扣库存”、“减余额”、“生成订单”三步,如果余额不足导致减余额失败,库存和订单也应全部回滚,避免“库存减少但订单未生成”的情况。 |
| 一致性 | 事务执行前后,数据库数据符合预设规则,状态合法 | 下单前商品库存10件、用户余额500元,下单后库存变为9件、余额变为450元,确保总金额和库存逻辑无冲突;不会出现“库存为负”或“余额为负”的非法状态。 |
| 隔离性 | 并发事务相互不影响,各自操作和数据对其他事务隔离 | 用户A和用户B同时购买最后一件商品,A的下单事务执行时,B无法读取A未提交的“库存锁定”状态,防止两人同时下单成功导致超卖。 |
| 持久性 | 事务提交后,数据修改永久生效,不受后续故障影响 | 用户下单支付成功(事务提交)后,即使数据库服务器突然断电,重启后订单记录、库存变更、余额扣减的结果仍然存在,不会丢失。 |
在并发环境下,隔离性是最容易出现问题的特性,常见的失效情况有三种:
通过设置不同的隔离级别,可以在并发效率和数据一致性之间找到平衡点,从而解决上述问题:
原子性/持久性问题:事务提交失败、数据库崩溃
问题描述:事务执行过程中断电或数据库崩溃,导致部分操作执行、提交结果丢失。
解决方案:利用数据库日志(如MySQL的redo log、undo log),在崩溃后通过日志回滚未提交的事务、恢复已提交的事务。
一致性问题:业务逻辑错误导致数据非法
问题描述:如转账时计算错误,导致总金额变化,违反了一致性。
解决方案:在数据库层面设置约束(如主键、外键、CHECK约束),在业务层面在事务中增加校验逻辑(如转账前检查余额是否充足)。
MySQL 的隔离级别由变量控制(MySQL 8.0 以后推荐使用,替代旧版),默认隔离级别是REPEATABLE READ(可重复读)。
transaction_isolation
MySQL 8.0 以后推荐使用的变量控制方式,替代旧版的变量控制方法。
tx_isolation
可以通过特定命令查询当前的隔离级别。
-- MySQL 8.0+
SELECT @@transaction_isolation;
-- MySQL 5.7 及以下
SELECT @@tx_isolation;
修改隔离级别可以通过以下步骤进行:
SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别;SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;隔离级别的可选值(大小写不敏感)包括:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE要使隔离级别永久生效,需要编辑MySQL配置文件。在Linux系统中,通常编辑/etc/my.cnf或/etc/mysql/my.cnf文件。
my.cnfmy.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),用于模拟并发事务。
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 |
|---|---|---|---|
| 1. 设置隔离级别: | |
设置隔离级别(同上) | 两个会话均采用“读未提交”隔离级别 |
| 2. 开启事务: | |
|
两个事务同时开始 |
| 3. 修改数据(不提交): | |
会话 A 减少用户1的余额50元(余额变为50元),但未提交 | |
| 4. 查询用户1余额: | |
会话 B 可以读取到未提交的50元余额(脏读) | |
| 5. 回滚事务: | |
会话 A 回滚事务,用户1的余额恢复为100元 | |
| 6. 再次查询: | |
会话 B 读取到回滚后的100元余额 |
结论:此隔离级别允许脏读,提供最低的隔离性,但具有最高的并发性能。
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 |
|---|---|---|---|
| 1. 设置隔离级别: | |
设置隔离级别(同上) | 两个会话均采用“读已提交”隔离级别 |
| 2. 开启事务: | |
|
两个事务同时开始 |
| 3. 修改数据(不提交): | |
会话 A 减少用户1的余额50元(余额变为50元),但未提交 | |
| 4. 查询用户1余额: | |
会话 B 读取到原始的100元余额(没有脏读) | |
| 5. 提交事务: | |
会话 A 提交事务,用户1的余额变为50元 | |
| 6. 再次查询: | |
会话 B 读取到已提交的50元余额 | |
| 7. 同一事务内再次查询(不变): | 若会话 B 未提交,多次查询结果一致?不! | 会话 B 再次查询仍为50元(注意:READ COMMITTED 解决了脏读,但可能遇到不可重复读) |
补充测试(不可重复读):
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 |
|---|---|---|---|
| 1. 开启事务: | |
|
两个事务同时开始 |
| 2. 查询用户1余额: | |
会话 B 读取到100元余额 | |
| 3. 修改并提交: | |
|
会话 A 将用户1的余额更改为150元并提交 |
| 4. 同一事务内再次查询: | |
会话 B 读取到150元余额(同一事务内的两次查询结果不一致,即不可重复读) |
结论:解决了脏读问题,但存在不可重复读的情况,即在同一事务内读取已提交的数据时可能会发生变化。
操作步骤(测试不可重复读):
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 |
|---|---|---|---|
| 1. 设置隔离级别: | |
设置隔离级别(同上) | 两个会话均使用默认的“可重复读”隔离级别 |
| 2. 开启事务: | |
|
两个事务同时开始 |
| 3. 查询用户1余额: | |
会话 B 读取到100元余额 | |
| 4. 修改并提交: | |
|
会话 A 将用户1的余额更改为150元并提交 |
| 5. 同一事务内再次查询: | |
会话 B 仍然读取到100元余额(解决了不可重复读) | |
| 6. 提交事务后查询: | |
|
会话 B 提交事务后,读取到150元余额(事务结束后可见提交的数据) |
补充测试(幻读):
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 | ||
|---|---|---|---|---|---|
| 1. 开启事务: | |
|
两个事务同时开始 | ||
| 2. 查询用户数: | |
会话 B 读取到2条记录(用户1、2) | |||
| 3. 插入数据并提交: | |
|
|
|
会话 A 插入用户3并提交事务 |
| 4. 同一事务内再次查询: | |
会话 B 仍然读取到2条记录(InnoDB 优化避免了幻读) | |||
| 5. 尝试插入用户3: | |
|
|
会话 B 插入失败(唯一键冲突),但查询不到用户3(这是幻读的一种特殊情况,InnoDB 通过 MVCC + 间隙锁解决) |
结论:解决了脏读和不可重复读的问题,InnoDB 引擎通过 MVCC(多版本并发控制)+ 间隙锁基本避免了幻读,是平衡一致性和并发性的最佳选择。
操作步骤:
| 操作步骤 | 会话 A(事务1) | 会话 B(事务2) | 现象说明 |
|---|---|---|---|
| 1. 设置隔离级别: | |
设置隔离级别(同上) | 两个会话均使用“串行化”隔离级别 |
| 2. 开启事务: | |
|
两个事务同时开始 |
SELECT balance FROM user_balance WHERE user_id = 1 FOR UPDATE;
会话 A 对用户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 主要应用于 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 隔离级别基本能避免幻读现象。
通过上述实践操作,可以快速了解不同隔离级别的具体行为差异,从而根据实际业务需求选择最合适的隔离级别。
扫码加好友,拉您进群



收藏
