事务(Transaction) 是指一组操作的集合,集合中的操作作为一个整体被执行,全部成功或全部失败.
核心目的: 保证数据的一致性和可靠性,尤其是系统发生故障或错误时,能够保证数据的一致性.
我们以一个简单的银行转账为例,来说明为什么数据库需要保证数据的可靠性。
场景:
小王和小黎都在银行有账户,账户表如下:
CREATE TABLE account (
id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(100) COMMENT '客户名称',
balance INT COMMENT '余额',
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
当小王和小黎分别存入11元和2元时,数据库中对应记录是:
id | name | balance |
---|---|---|
1 | 小王 | 11 |
2 | 小黎 | 2 |
假设有一天,小黎因为学习买资料,需要借10元钱,小王决定帮他转账。现实世界中,小王在ATM机上输入小黎的账户信息,确认转账。对应的数据库操作如下: |
UPDATE account SET balance = balance - 10 WHERE id = 1; -- 小王账户减10
UPDATE account SET balance = balance + 10 WHERE id = 2; -- 小黎账户加10
问题:
如果在执行这两条语句时,突然发生了服务器崩溃,可能导致:
特性 | 说明 |
---|---|
原子性(Atomicity) | 事务中的所有操作要么全部成功,要么全部失败,不会出现只执行了部分操作的情况。 |
一致性(Consistency) | 事务执行前后,数据库的状态是符合所有约束和规则的。 |
隔离性(Isolation) | 多个事务并发执行时,互不干扰,保证每个事务的独立性。 |
持久性(Durability) | 一旦事务成功提交,对数据库的修改是永久保存的,不会丢失。 |
下面我们细聊这些特性帮助更好的理解.
我们从下面几个方面去理解原子性.
现实世界中的转账操作是一个整体,不能被拆分为“转账了一部分”或“转账失败一部分”的情况。要么转账完全成功,要么完全不成功,这就是我们在实际操作中想要的结果。
例如,如果小王要给小黎转账10元.这件事只会是下面这样的结果:
在数据库世界中,转账操作可能包含多个不同的操作。
例如:
数据库的操作过程中,可能会发生许多不可预见的错误,像是数据库本身的故障、操作系统错误,甚至是硬件故障(如断电)。如果在执行过程中某个步骤成功执行了,另一个步骤却因为故障未能完成,数据库就会处于一个不一致的状态,导致数据丢失或不准确。
总结原子性:
原子性就是确保一系列操作在数据库中作为一个不可分割的整体要么完全成功,要么完全失败。数据库设计者需要通过事务管理来确保,即使在发生错误的情况下,也能恢复操作之前的状态,从而避免数据库进入不一致的状态。
现实世界中的两次状态转换应该是互不影响的.
举个例子: 小王对小黎同时进行两次金额为5元的转账(在两个ATM上操作).
最后结果肯定是小王账户少10元,小黎账户多10元.
小王的两次操作是互不影响的,这也很符合我们的直觉判断.
但是在数据库里事情就复杂了,我们简化一下问题,来分析小王给小黎转5元的转账流程是什么样的:
所以不仅要以原子性的方式执行完成,还要保证它的状态转换不会被其他状态转换影响,这种规则就叫做隔离性.
怎么实现我们后面去聊.
我们生活的这个世界存在着形形色色的约束,比如身份证号不能重复,性别只能是男或者女,高考的分数只能在0~750之间.
比如有个小孩儿跟你说他高考考了1000分,你一听就知道他胡扯呢。
数据库世界只是现实世界的一个映射,现实世界中存在的约束当然也要在数据库世界中有所体现。如果数据库中的数据全部符合现实世界中的约束(all defined rules),我们说这些数据就是一致的,或者说符合一致性的。
一致性在数据库中意味着,**数据在执行操作之前和之后必须符合数据库定义的所有约束规则。**比如,在银行系统中,账户余额不应该小于 0,这是一个一致性的要求。
那么我们如何保证一致性呢?
CREATE TABLE account (
id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
name VARCHAR(100) COMMENT '客户名称',
balance INT COMMENT '余额',
PRIMARY KEY (id),
CHECK (balance >= 0) -- 确保账户余额不为负
);
这里的 CHECK (balance >= 0) 约束表示,账户余额不可以为负数。
总结而言呢,一致性 是保证数据符合现实世界规则的约束,确保数据合法有效。
当这个现实世界的转账操作映射到数据库时,持久性确保了一旦事务成功提交,所有的数据库修改都会保存在磁盘中。无论后续发生什么意外(如数据库崩溃、系统断电等),这些操作的结果都不会丢失。
它保证了所有经过提交的事务都不可撤销,即使发生系统崩溃或故障后,数据的变化仍会被恢复并保留,避免丢失。
我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,事务在执行过程中经历的各个阶段都对数据库的状态产生不同的影响。理解这些状态有助于我们更好地掌握事务管理和处理错误,让我们来详细分析每个状态及其含义:
定义:事务正在执行过程中,所有的数据库操作都在进行中,但还没有提交或回滚。
举例:小王正在向小黎转账5元,读取数据、修改余额、计算等操作正在进行时,事务处于“活动的”状态。
定义:事务中的最后一个操作执行完成,但数据还没有刷新到磁盘。也就是说,虽然操作在内存中完成,但还没有写入持久化存储中。
举例:小王正在向小黎转账5元,所有数据库操作(如扣除小王余额、增加小黎余额)都已完成,但修改还只存在于内存中,并未最终写入磁盘,事务就处于“部分提交”的状态。
定义:当事务的所有操作成功执行,并且所有修改都已经同步到磁盘时,事务进入“提交”状态。此时,事务对数据库的所有修改都永久生效。
举例:在转账完成后,小王账户的余额已减少,小黎账户的余额已增加,这些修改成功写入磁盘,事务进入“提交”状态,转账完成。
定义:事务在“活动”或“部分提交”状态时,因某些错误(如数据库错误、操作系统错误、系统崩溃等)无法继续执行。这时,事务无法完成,状态变为“失败的”。
举例:小王正在向小黎转账时,系统崩溃,事务处于“活动”状态,但由于错误,事务无法继续执行,进入“失败”的状态。
定义::事务已经处于“失败”状态,需要进行回滚操作,即撤销事务对数据库的修改。回滚会将数据恢复到事务开始前的状态。完成回滚后,事务变为“中止”状态。
举例:如果在转账过程中,小王账户扣款成功,但小黎账户没有增加金额,发生系统崩溃,事务进入“失败”状态。在这种情况下,已经执行的操作(如扣款)需要回滚,以确保数据库状态不受影响,最终事务进入“中止”状态。
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> 加入事务的语句...
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> 加入事务的语句...
不过比BEGIN语句牛逼一点儿的是,可以在START TRANSACTION语句后边跟随几个修饰符,就是它们几个:
开启事务之后就可以继续写需要放到该事务中的语句了,当最后一条语句写完了之后,我们就可以提交该事务了,提交的语句也很简单:
COMMIT
COMMIT语句就代表提交一个事务.比如我们上面说小王给小黎转10元钱其实对应MySQL中的两条语句,我们就可以把这两条语句放到一个事务中,完整的过程就是这样:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET balance = balance - 10 WHERE id = 1;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE account SET balance = balance + 10 WHERE id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
如果我们写了几条语句之后发现上面的某条语句写错了,我们可以手动的使用下面这个语句来将数据库恢复到事务执行之前的样子:
ROLLBACK
比如我们在写小王给小黎转账10元钱对应的MySQL语句时,先给小王扣了10元,然后一时大意只给小黎账户上增加了1元,此时就可以使用ROLLBACK语句进行回滚,完整的过程就是这样:
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET balance = balance - 10 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE account SET balance = balance + 1 WHERE id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
ROLLBACK语句是我们程序员手动的去回滚事务时才去使用的,如果事务在执行过程中遇到了某些错误而无法继续执行的话,事务自身会自动的回滚。
MySQL中有一个系统变量autocommit:
mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
以看到它的默认值为ON,也就是说默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。
假如我们在小王向小黎转账10元时不以START TRANSACTION或者BEGIN语句显式的开启一个事务,那么下面这两条语句就相当于放到两个独立的事务中去执行:
UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;
当然,如果我们想关闭这种自动提交的功能,可以使用下面两种方法之一:
SET autocommit = OFF;
当我们使用START TRANSACTION或者BEGIN语句开启了一个事务,或者把系统变量autocommit的值设置为OFF时,事务就不会进行自动提交,但是如果我们输入了某些语句之后就会悄悄的提交掉,就像我们输入了COMMIT语句了一样,这种因为某些特殊的语句而导致事务提交的情况称为隐式提交.
这些会导致事务隐式提交的语句包括:
BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条语句
... # 事务中的其它语句
CREATE TABLE ... # 此语句会隐式的提交前面语句所属于的事务
当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前面语句所属于的事务。
BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条语句
... # 事务中的其它语句
BEGIN; # 此语句会隐式的提交前面语句所属于的事务
如果你开启了一个事务,并且已经敲了很多语句,忽然发现上一条语句有点问题,你只好使用ROLLBACK语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,总有一种一夜回到解放前的感觉。
就像是打空洞骑士(难度高,死忙代价很高,会丢失很多东西)一样,死一次就让你抓狂.
相反奥日的森林就很轻松一点,里面的存档点很多,死亡代价小,让玩家更敢操作.
那这个游戏里的存档点就像这里的保存点一样:
在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定会滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:
SAVEPOINT 保存点名称;
当我们想回滚到某个保存点时,可以使用下面这个语句:
ROLLBACK TO 保存点名称;
如果ROLLBACK语句后边不跟随保存点名称的话,会直接回滚到事务执行之前的状态。
如果我们想删除某个保存点,可以使用这个语句:
RELEASE SAVEPOINT 保存点名称;
下面还是以小王向小黎转账10元的例子展示一下保存点的用法,在执行完扣除小王账户的钱10元的语句之后打一个保存点:
mysql> SELECT * FROM account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 小王 | 11 |
| 2 | 小黎 | 2 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET balance = balance - 10 WHERE id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SAVEPOINT s1; # 一个保存点
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 小王 | 1 |
| 2 | 小黎 | 2 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> UPDATE account SET balance = balance + 1 WHERE id = 2; # 更新错了
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> ROLLBACK TO s1; # 回滚到保存点s1处
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 小王 | 1 |
| 2 | 小黎 | 2 |
+----+--------+---------+
2 rows in set (0.00 sec)