数据库事务
开始转账流程。查询账户 A 的余额是否足够,如果其余额不足,则终止转账。
从账户 A 扣除 1000 元。
往账户 B 存入 1000 元。
在系统中记录本次转账的交易流水。
提交并结束本次转账流程。
事务的 ACID 属性
原子性指的是一个事务中的操作要么全部成功,要么全部失败。例如,某个事务需要更新 100 条记录,但是在更新到一半时系统出现故障,数据库必须保证能够撤销已经修改过的数据,就像没有执行过任何更新一样。
一致性意味着在事务开始之前数据库处于一致性的状态,事务完成之后数据库仍然处于一致性的状态。例如,在银行转账事务中如果一个账户扣款成功,但是另一个账户入账失败,就会出现数据不一致(此时需要撤销已经执行的扣款操作)的情况。另外,数据库还必须确保数据的完整性,比如账户扣款之后不能出现余额为负数的情况(可以通过余额字段上的检查约束实现)。
隔离性与并发事务有关,表示一个事务的修改在提交之前对其他事务不可见,多个并发的事务之间相互隔离。例如,在账户 A 向账户 B 转账的过程中,账户 B 查询的余额应该是转账之前的数目。如果多个账户同时向账户 B 转账,最终账户 B 的余额也应该保持一致性,和多个账户依次进行转账的结果一样。SQL 标准定义了 4 种不同的事务隔离级别,我们将会在下文中进行介绍。
持久性表示已经提交的事务必须永久生效,即使发生断电、系统崩溃等故障,数据库也不会丢失数据。数据库管理系统通常使用重做日志(REDO)或者预写式日志(WAL)实现事务的持久性。简单来说,它们都是在提交之前将数据的修改记录到日志文件中,当数据库出现崩溃时就可以利用这些日志重做之前的修改,从而避免数据的丢失。
事务控制语句
START TRANSACTION,开始一个新的事务。
COMMIT,提交一个事务。
ROLLBACK,撤销一个事务。
SAVEPOINT name,设置一个事务保存点,用于撤销部分事务。
RELEASE SAVEPOINT name,释放事务保存点。
ROLLBACK TO name,将事务撤销到保存点,保存点之前的修改仍然保留。
事务控制 | MySQL(InnoDB) | Oracle | SQL Server | PostgreSQL | SQLite |
---|---|---|---|---|---|
开始事务 | START TRANSACTION、BEGIN | 自动开始 | BEGIN TRANSACTION | START TRANSACTION、BEGIN | BEGIN |
提交事务 | COMMIT | COMMIT | COMMIT | COMMIT | COMMIT |
撤销事务 | ROLLBACK | ROLLBACK | ROLLBACK TRANSACTION | ROLLBACK | ROLLBACK |
设置事务保存点 | SAVEPOINT | SAVEPOINT | SAVE TRANSACTION | SAVEPOINT | SAVEPOINT |
释放事务保存点 | RELEASE SAVEPOINT | 系统自动管理 | 系统自动管理 | RELEASE SAVEPOINT | RELEASE SAVEPOINT |
撤销到保存点 | ROLLBACK TO | ROLLBACK TO | ROLLBACK TRANSACTION | ROLLBACK TO | ROLLBACK TO |
设置自动提交 | 默认自动提交,SET autocommit = {ON | OFF} | 手动提交,不支持设置 | 默认自动提交,SET IMPLICIT_TRANSACTIONS {ON | OFF} | 自动提交,不支持设置 | 自动提交,不支持设置 |
CREATE TABLE bank_card(
card_id VARCHAR(20) NOT NULL PRIMARY KEY, -- 卡号
user_name VARCHAR(50) NOT NULL, -- 用户名
balance NUMERIC(10,4) NOT NULL, -- 余额
CHECK (balance >= 0)
);
开始事务
-- Oracle 无须执行 BEGIN 语句
-- Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
INSERT INTO bank_card VALUES ('62220801', 'A', 1000);
提交事务
COMMIT;
-- MySQL
show variables like 'autocommit';
Variable_name|Value
-------------|-----
autocommit |ON
撤销事务
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
INSERT INTO bank_card VALUES ('62220802', 'B', 500);
ROLLBACK; -- Microsoft SQL Server 使用 ROLLBACK TRANSACTION 语句
SELECT card_id, user_name, balance
FROM bank_card;
card_id |user_name|balance
--------|---------|---------
62220801|A |1000.0000
事务保存点
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
INSERT INTO bank_card VALUES ('62220802', 'B', 500);
SAVEPOINT sv; -- Microsoft SQL Server 使用 SAVE TRANSACTION 语句
INSERT INTO bank_card VALUES ('62220803', 'C', 2000);
ROLLBACK TO sv; -- Microsoft SQL Server 使用 ROLLBACK TRANSACTION 语句
COMMIT;
card_id |user_name|balance
--------|---------|---------
62220801|A |1000.0000
62220802|B | 500.0000
并发事务与隔离级别
并发问题
脏读(Dirty Read)。当一个事务允许读取另一个事务修改但未提交的数据时,就可能发生脏读。例如,账户 B 的初始余额为 0,账户 A 向账户 B 转账 1000 元但没有提交。如果此时账户 B 能够看到账户 A 转过来的 1000 元,并且取款 1000 元,然后 A 账户取消了转账操作,意味着银行损失了 1000 元。显然,银行不会允许这种事情发生。
不可重复读(Nonrepeatable Read)。一个事务读取某条记录后,该数据被另一个事务修改并提交,该事务再次读取相同的记录时,结果发生了变化。例如,在对账户 B 进行查询时的初始余额为 0,此时账户 A 向账户 B 转账 1000 元并且提交成功,之后对账户 B 再次查询时,发现余额变成了 1000 元。这种情况并不会导致数据的不一致。
幻读(Phantom Read)。一个事务第一次读取数据后,另一个事务增加或者删除了某些记录,导致该事务再次读取时返回结果的数量发生了变化。幻读和不可重复读有点类似,都是由于其他事务修改数据导致的结果变化。
更新丢失(Lost Update)。第一类更新丢失指的是,当两个事务更新相同的数据时,第一个事务被提交,之后第二个事务被撤销,这导致第一个事务的更新也被撤销。所有遵循SQL 标准的数据库都不会产生第一类更新丢失。第二类更新丢失指的是,当两个事务同时读取某个记录后分别进行修改提交,造成先提交事务的修改丢失。
隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 更新丢失 |
---|---|---|---|---|
读未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 第二类 |
读已提交(Read Committed) | – | 可能 | 可能 | 第二类 |
可重复读(Repeatable Read) | – | – | 可能 | – |
序列化(Serializable) | – | – | – | – |
案例分析
-- MySQL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 会话 1
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'A';
card_id |user_name|balance
--------|---------|---------
62220801|A |1000.0000
-- 会话 2
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
UPDATE bank_card
SET balance = balance + 100
WHERE user_name = 'A';
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'A';
card_id |user_name|balance
--------|---------|---------
62220801|A |1100.0000
-- 会话 1
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'A';
card_id |user_name|balance
--------|---------|---------
62220801|A |1000.0000
-- 会话 2
COMMIT;
-- 会话 1
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'A';
card_id |user_name|balance
--------|---------|---------
62220801|A |1100.0000
-- 会话 2
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
DELETE
FROM bank_card
WHERE user_name = 'A';
COMMIT;
-- 会话 1
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'A';
card_id |user_name|balance
--------|---------|---------
-- 会话 1
UPDATE bank_card
SET balance = 500 + 100
WHERE user_name = 'B';
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'B';
card_id |user_name|balance
--------|---------|---------
62220802|B |600.0000
-- 会话 2
-- Oracle 无须执行 BEGIN 语句,Microsoft SQL Server 使用 BEGIN TRANSACTION 语句
BEGIN;
SELECT card_id, user_name, balance
FROM bank_card
WHERE user_name = 'B';
card_id |user_name|balance
--------|---------|---------
62220802|B |500.0000
UPDATE bank_card
SET balance = 500 + 200
WHERE user_name = 'B';
-- 会话 1
COMMIT;
-- 会话 2
COMMIT;
UPDATE bank_card
SET balance = balance + 200
WHERE user_name = 'B';