MySQL-事务(TRANSACTION-ACID)管理
目录
一、什么是事务?
1.1.事务的定义
1.2.事务的基本语句
1.3.事务的四大特性(ACID)
二、数据库的并发控制
2.1.什么是并发及并发操作带来的影响?
2.2.并发操作带来的隔离级别
三、使用事务的场景
3.1.银行转账场景示例
3.2.模拟丢失修改、不可重复读、读脏数据
一、什么是事务?
1.1.事务的定义
MySQL事务是数据库操作的一个基本单元,它确保一组操作要么全部成功,要么全部失败。事务的主要目的是保证数据的一致性和完整性。
1.2.事务的基本语句
一个事务由应用程序的一组操作序列组成,它以 BEGIN TRANSACTION语句开始,以 END TRANSACTION 结束语句。
事务定义的语句如下:
- BEGIN TRANSACTION: 事务开始。
- END TRANSACTION:事务结束。
- COMMIT:事务提交。该操作表示事务成功地结束,它将通知事务管理器该事务的所有更新操作现在可以被提交或永久地保留。
- ROLLBACK:事务回滚。该操作表示事务非成功地结束,它将通知事务管理器出故障了,数据库可能处于不一致状态,该事务的所有更新操作必须回滚或撤销。这意味着将撤销该事务对数据库的更新。这样,数据库恢复到该事务执行第一条语句之前的状态。
典型示例:
典型的例子是银行转账业务。对“从账户 A 转入账户 B 金额x元”业务,站在顾客角度来看,转账是一次单独操作;而站在数据库系统的角度它至少是由两个操作组成的,第一步从账户A减去x元,第二步给账户B加上x元。下面是银行转账事务的伪代码:
BEGIN TRANSACTION
read(A); /*读账户A的余额*/
A=A-x;
IF(A<0)THEN
print("金额不足,不能转账");ROLLBACK; /*撤销该事务,回到事务执行前的状态*
ELSE
write(A); /*写入账户A的余额*/
read(B);
B=B+1;
write(B);
COMMIT; /*提交事务*/
ENDIF;
END TRANSACTION
1.3.事务的四大特性(ACID)
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。如果事务中的任何操作失败,整个事务将回滚到初始状态。
- 一致性(Consistency):事务执行前后,数据库的状态必须保持一致。这意味着事务必须遵循数据库的约束和规则。
- 隔离性(Isolation):多个事务并发执行时,一个事务的操作不会影响其他事务。每个事务都感觉不到其他事务的存在。
- 持久性(Durability):一旦事务提交,它对数据库的修改就是永久性的,即使系统发生故障也不会丢失。
二、数据库的并发控制
2.1.什么是并发及并发操作带来的影响?
所谓并发操作,是指在多用户共享的系统中,许多用户可能同时对同一数据进行操作。这种机制就有可能导致,其他用户想要得到原始数据,但由于其他用户的读写操作,导致数据已经进行修改等情况的产生。
通常并发操作导致数据的不一致性主要有三类:
1.丢失修改:
如图 12-7(a)所示,事务 T!、T,都是对數据 A 做減1操作。事务T,在时劍ち把A 修改后的值15写入数据库,但事务T在时刻t再把它对A减1后的值15写入。两个事务都是对 A的值进行减1操作并且都执行成功,但A中的值却只减了1。现实的例子如售票系统,同时售出了两张票,但数据库里的存票却只减了一张,造成数据的不一致。原因在于T事务对数据库的修改被 T事务覆盖而丢失了,破坏了事务的隔离性。2.不可重复读:
如图 12-7(b)所示,事务 T读取 A、B 的值后进行运算,事务 T在 t时刻对 B的值做了修改以后,事务 T又重新读取 A、B 的值再运算,同一事务内对同一组数据的相同运算结果不同,显然与事实不相符。同样是事务 工干扰了事务 T的独立性。
3.读脏数据:
如图 12-7(c)所示,事务 T对数据 ℃ 修改之后,在 t时刻事务 T读取修改后的 ℃ 值做处理,之后事务 T回滚,数据 ℃ 恢复了原来的值,事务 T对C所做的处理是无效的,它读的是被丢掉的垃圾值。
其主要原因是事务的并发操作破坏了事务的隔离性。DBMS的并发控制子系统负责协调并发事务的执行,保证数据库的完整性不受破坏,避免用户得到不正确的数据。
2.2.并发操作带来的隔离级别
并发操作对应产生了由于不同隔离级别所产生的不同并发问题。一般规律也是:隔离级别越高,数据一致性越强,但并发性能相应就越低。
并发问题 | 读未提交(Read Uncommitted) | 读已提交(Read committed) | 可重复读(Repeatable Read) | 串行化(Serializable) |
脏读(Dirty Read) | × | √ | √ | √ |
不可重复读 | × | × | √ | √ |
幻读(Phantom) | × | × | MySQL在可重复读级别通过Next-Key锁解决幻读 | √ |
丢失更新 | × | × | 不能完全解决 | √ |
幻读和不可重复读:
类型 | 关注点 | 变化 |
不可重复读 | 同一条记录的数据变化 | 余额从 1000 → 900 (值变化) |
幻读 | 结果集的行数变化 | 第一次查有 5 条,第二次变 6 条。 |
三、使用事务的场景
3.1.银行转账场景示例
测试事务的一致性(要么同时成功,要么同时失败)
-- 表结构设计
drop table book;
CREATE TABLE `book` (`recID` INT(11) PRIMARY KEY AUTO_INCREMENT,`title` VARCHAR(50) NOT NULL,`type` VARCHAR(20) NOT NULL,`price` DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;show TABLES;select * from book b;
-- 初始数据
INSERT INTO `book` (`title`, `type`, `price`) VALUES
('Java Programming', 'Computer', 16.00),
('Java EE Technology', 'Computer', 5.00),
('Information System', 'Computer', 15.00);-- 事务操作语句
BEGIN;
-- 批量降价8元
UPDATE book SET price = price - 8 WHERE recID = 1;
UPDATE book SET price = price - 8 WHERE recID = 2;
UPDATE book SET price = price - 8 WHERE recID = 3;-- 验证价格是否不低于0
SELECT * FROM book WHERE price < 0;-- 根据验证结果选择提交或回滚
COMMIT; -- 无负值时提交
ROLLBACK; -- 出现负值时回滚
验证:
验证价格是否不低于0,因为出现负值,我们执行回滚操作,看数据是否会回到我们最开始的状态值。
3.2.模拟丢失修改、不可重复读、读脏数据
由于mysql其自身的默认隔离级别就已经是可重复读,即使并发执行,也不会产生丢失修改和不可重复读问题。
-- 查看当前会话的隔离级别(MySQL 5.7+ 和 MariaDB)SELECT @@transaction_isolation;-- 查看全局隔离级别SELECT @@global.transaction_isolation;-- mysql中的默认隔离级别是:REPEATABLE-READ
模拟:
-- 1. 表结构设计(以银行账户为例)
CREATE TABLE account (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL,balance DECIMAL(10, 2) NOT NULL -- 账户余额
);
-- 插入初始数据:
INSERT INTO account (name, balance) VALUES ('Alice', 1000.00);-- 2. 模拟不可重复读
-- 现象:事务A多次读取同一数据,事务B修改并提交后,事务A两次读取结果不一致。
-- 隔离级别:READ COMMITTED(读已提交)
-- 操作步骤:-- 事务A(首次读取):
BEGIN;
SELECT balance FROM account WHERE name = 'Alice'; -- 结果:1000.00-- 事务B(修改并提交):
BEGIN;
UPDATE account SET balance = 900.00 WHERE name = 'Alice';
COMMIT; -- 修改生效-- 事务A(再次读取):
SELECT balance FROM account WHERE name = 'Alice'; -- 结果:900.00(与第一次不一致)
COMMIT;
-- 结果:事务A两次读取结果不同,即不可重复读。-- 3. 模拟丢失修改
-- 现象:两个事务同时读取并修改同一数据,后提交的事务覆盖前者的修改。
-- 隔离级别:READ UNCOMMITTED(读未提交)或 READ COMMITTED
-- 操作步骤:-- 事务A(读取并修改):
BEGIN;
SELECT balance FROM account WHERE name = 'Alice'; -- 读取1000.00
UPDATE account SET balance = 1000.00 - 100 = 900.00 WHERE name = 'Alice';-- 事务B(同时读取并修改):
BEGIN;
SELECT balance FROM account WHERE name = 'Alice'; -- 仍读取1000.00(未提交的数据)
UPDATE account SET balance = 1000.00 - 200 = 800.00 WHERE name = 'Alice';
COMMIT; -- 先提交-- 事务A提交:
COMMIT; -- 覆盖事务B的修改
-- 结果:余额变为 900.00(事务B的修改丢失),正确应为 700.00。
开启事务A B
事务A读取到的值
事务B修改提交之后:
事务A再次读取: