浅谈 MySQL 的事务与 ACID

最近把我的博客搭建好了,连接在这里:tobe的呓语,文章会先在博客和公众号更新~ 但愿你们多多收藏啊html

所谓事务(Transaction),就是经过确保成批的操做要么彻底执行,要么彻底不执行,来维护数据库的完整性。举一个烂大街的例子:A 向 B 转帐 1000 元,对应的 SQL 语句为:(没有显式定义事务)mysql

UPDATE deposit_table set deposit = deposit - 1000 WHERE name = 'A';
UPDATE deposit_table set deposit = deposit + 1000 WHERE name = 'B';复制代码

运行后的结果以下:sql

mysql> SELECT * FROM deposit_table;
+------+---------+
| name | deposit |
+------+---------+
| A    |    3000 |
| B    |    5000 |
+------+---------+复制代码

这样作可能遇到问题,好比执行完第一条语句以后,数据库崩溃了,最后的结果就可能会是这样(毕竟咱不会模拟这种故障):数据库

+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    5000 |
+------+---------+复制代码

A 的 1000 块钱无缘无故消失了,这确定不合适。事务就是为了解决相似的问题而出现的,若是使用事务来处理转帐,对应的 SQL 就是:并发

START TRANSACTION;
UPDATE deposit_table set deposit = deposit - 1000 WHERE name = 'A';
UPDATE deposit_table set deposit = deposit + 1000 WHERE name = 'B';
COMMIT;复制代码

仅仅是在这原先的两条 SQL 语句先后加上了 START TRANSACTIONCOMMIT ,就能够保证即便转帐操做失败,A 的余额也不会减小。性能

仔细想想发现这个例子不是特别合适,由于数据库的故障恢复技术(之后会谈到)会影响最终的结果,也不容易模拟这种故障,最后结果只能靠猜 : ) 但我也想不出其它更加合适的例子。。。若是大家有更好的例子欢迎留言讨论。this

接下来就详细讨论事务的一些特性和(某些)实现细节。spa

ACID

  • A:Atomicity(原子性)
  • C:Consistency(一致性)
  • I:Isolation(隔离性)
  • D:Durability(持久性)

Atomicity(原子性)

先谈两个重要的概念:提交(commit)和回滚(rollback),当咱们执行提交操做后,将对数据库进行永久性的修改,执行回滚操做,意味着数据库将撤销正在进行的全部没有提交的修改。注意这里的永久性并不意味这事务一完成就把数据刷到磁盘上,即便没有刷入磁盘,MySQL 也有日志机制来保证修改不会丢失。操作系统

事务是支持提交和回滚的工做单元,原子性,就是说事务对数据库进行屡次更改时,要么在提交事务的时候全部更改都成功,要么在回滚事务的时候撤销全部更改。这是官方文档的表述,但有的人彷佛错误理解了 commit 语句,实际上,哪怕事务里某一语句出现了错误,一旦你执行 commit,前面正常的修改仍然会被提交,MySQL 不会自动判断事务中的 SQL 执行成功与否。命令行

咱们接下来用例子来看看 commit 和 rollback:

mysql> SELECT * FROM deposit_table;
+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    6000 |
+------+---------+
2 rows in set (0.04 sec)
复制代码

mysql> 
START TRANSACTION;
INSERT INTO deposit_table VALUES('C', 7000);
INSERT INTO deposit_table VALUES('D', 8000);
#再次插入 D,因为主键的惟一性,该语句会执行失败
INSERT INTO deposit_table VALUES('D', 9000);
COMMIT; #提交事务

Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

1062 - Duplicate entry 'D' for key 'PRIMARY'
Query OK, 0 rows affected (0.07 sec)复制代码
mysql> SELECT * FROM deposit_table;
+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    6000 |
| C    |    7000 |
| D    |    8000 |
+------+---------+
4 rows in set (0.04 sec)复制代码

咱们能够看到,在执行 INSERT INTO deposit_table VALUES('D', 9000) 的时候,因为前一条语句已经插入了 D,因此这一句 SQL 语句执行失败,报出 1062 - Duplicate entry 'D' for key 'PRIMARY' 错误,但执行 COMMIT 后,前面的修改仍然获得了提交,这显然是不符合咱们的预期的。

注意:若是你是使用 Navicat 的查询界面,将执行不到 COMMIT 语句,只能执行到报错的地方,建议使用命令行来执行。

因此在实际状况中,咱们须要根据 MySQL 的错误返回值来肯定,是使用 ROLLBACK 仍是 COMMIT 。就像这样:

# 建立一个存储过程
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_test`()
BEGIN
    # 建立一个标志符,出现错误就将其置为 1
    DECLARE err_flg INTEGER;
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET err_flg = 1;

    START TRANSACTION;
        INSERT INTO deposit_table VALUES('C', 7000);
                INSERT INTO deposit_table VALUES('D', 8000);
                INSERT INTO deposit_table VALUES('D', 9000);
        
        # 发生错误,回滚事务
        IF err_flg = 1 THEN
            SELECT 'SQL Err Invoked'; # 错误提示信息
            ROLLBACK;
            SELECT * FROM deposit_table;
        # 没有发生错误,直接提交
        ELSE
            SELECT 'TRANSACTION Success';
            COMMIT;
            SELECT * FROM deposit_table;
        END IF;
    
END复制代码

接下来咱们调用该存储过程:

mysql> call insert_test();
+-----------------+
| SQL Err Invoked |
+-----------------+
| SQL Err Invoked |
+-----------------+
1 row in set (0.04 sec)

+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    6000 |
+------+---------+
2 rows in set (0.09 sec)

Query OK, 0 rows affected (0.00 sec)复制代码

结果里打印出了错误信息 SQL Err Invoked 表的内容也没有更改,代表咱们的 ROLLBACK 成功回滚了事务,达到咱们的预期。若是你是使用其余语言调用 MySQL 的接口,也只须要获取错误标志,相应的执行 ROLLBACK 或者 COMMIT

Consistency(一致性)

官网给出的解释以下:

The database remains in a consistent state at all times — after each commit or rollback, and while transactions are in progress. If related data is being updated across multiple tables, queries see either all old values or all new values, not a mix of old and new values.

翻译过来就是:在每次提交或回滚以后以及正在进行的事务处理期间,数据库始终保持一致状态,若是跨多个表更新了相关数据,则查询将看到全部旧值或全部新值,而不是新旧值的混合

举个例子:

# 表 a,b 的定义略过
START TRANSACTION;
UPDATE a SET name = 'a_new' WHERE name = 'a_old';
UPDATE b SET name = 'b_new' WHERE name = 'b_old';
COMMIT;复制代码

这个例子里的一致性,就是说,若是此时有查询 SELECT a.name, b.name FROM a, b; 获得的结果要么是 a_old b_old (代表事务已回滚或者正在执行),要么是 a_new b_new (代表事务已经成功提交),而不会出现 a_old b_new 以及 a_new b_old 这两种状况。

有的博客将一致性解释为“数据符合现实世界中的约束,好比惟一性约束等等。” 我我的仍是倾向于官方文档的解释,这点见仁见智吧,纠结这些概念意义不大。

Isolation(隔离性)

事务的隔离性是说,事务之间不能互相干扰,也不能看到彼此的未提交数据。这种隔离是经过锁机制实现的。咱们在操做系统里也了解过,使用锁,每每就意味着并发性能的降低,由于可能会发生阻塞,甚至死锁现象。

固然,用户在肯定事务确实不会相互干扰时,能够调整隔离级别,牺牲部分隔离性以提升性能和并发性,至于使用哪一种隔离级别(isolation level)这就须要你本身作 trade off。

由于隔离性涉及的的内容不少,我把它放到下一篇文章详细解释。

Durability(持久性)

事务的持久性是说,一旦提交操做成功,该事务所作的更改就不会由于一些意外而丢失,好比电源断电,系统崩溃等潜在威胁。MySQL 提供了不少机制,好比日志技术,doublewrite buffer等等。

MySQL 的日志恢复技术我将单独写一篇文章,这里说说 doublewrite buffer 技术。

虽然这个技术名字叫作 buffer,但实际上该缓冲区并不位于内存,而是位于磁盘。这可能听起来很诡异——既然是把数据放入磁盘,为啥不直接写入到 data file,反而画蛇添足?

这是由于 InnoDB 的 Page Size 通常是 16kb,其数据校验也是针对页来计算的,在将数据刷入磁盘的过程当中,若是发生断电等故障,该页可能只写入了一部分(partial page write)。这种状况是 redo 日志没法解决的,由于 redo 日志中记录的是对页的物理操做,若是页自己发生了损坏,再对其进行 redo 是没有意义的。因此咱们须要一个副本,在发生这种状况时还原该页。

并且缓冲区是顺序写的,开销相对随机读写要小不少,因此 doublewrite 后,性能也不是降为原来的 50%。

事务中的经常使用语句

  • START TRANSACTION / BEGIN 显式开启一个事务
  • COMMIT 提交事务,永久性修改数据库
  • SAVEPOINT 在事务里建立保存点
  • RELEASE SAVAPOINT 移除某保存点
  • ROLLBACK 回滚事务,撤回全部未提交的更改,事务会终止
  • ROLLBACK TO [SAVEPOINT] 回滚到给定保存点,但事务不终止,另外,该**保存点后的行锁不会被释放**,详见[SAVEPOINT, ROLLBACK TO SAVEPOINT, and RELEASE SAVEPOINT Statements](https://dev.mysql.com/doc/refman/8.0/en/savepoint.html):

    ​ InnoDB does not release the row locks that were stored in memory after the savepoint. (For a new inserted row, the lock information is carried by the transaction ID stored in the row; the lock is not separately stored in memory. In this case, the row lock is released in the undo.)

  • SET TRANSACTION 设置事务隔离级别
  • SET autocommit 0/1 是否自动提交(默认自动提交)

强调一下 autocommit 参数,默认状况下,若是不显式使用 START TRANSACTION / BEGIN ,MySQL 会把每一句 SQL 当作独立的事务,举个例子:

原来的表结构:

mysql> SELECT * FROM deposit_table;
+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    6000 |
+------+---------+
2 rows in set (0.04 sec)复制代码

新的存储过程(仅仅删除了 START TRANSACTION ):

CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_test`()
BEGIN
    #Routine body goes here...
    DECLARE err_flg INTEGER;
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET err_flg = 1;

    # START TRANSACTION;
    INSERT INTO deposit_table VALUES('C', 7000);
    INSERT INTO deposit_table VALUES('D', 8000);
    INSERT INTO deposit_table VALUES('D', 9000);
        
        IF err_flg = 1 THEN
            SELECT 'SQL Err Invoked';
            ROLLBACK;
            SELECT * FROM deposit_table;
        ELSE
            SELECT 'TRANSACTION Success';
            COMMIT;
            SELECT * FROM deposit_table;
        END IF;
    
END复制代码

调用的结果:

mysql> call insert_test();
+-----------------+
| SQL Err Invoked |
+-----------------+
| SQL Err Invoked |
+-----------------+
1 row in set (0.24 sec)

+------+---------+
| name | deposit |
+------+---------+
| A    |    2000 |
| B    |    6000 |
| C    |    7000 |
| D    |    8000 |
+------+---------+
4 rows in set (0.28 sec)

Query OK, 0 rows affected (0.21 sec)复制代码

在这里,咱们看到尽管确实执行了 ROLLBACK,但 C 和 D 仍然插入到了 deposit_table 。这是由于没有显式标明事务,MySQL 会进行隐式事务,自动提交每次的修改,因此就没法进行回滚了。

事务的基本概念就介绍这么多,之后我将会讲到事务的隔离机制,范式设计等内容,敬请期待!

但愿你在看完个人文章以后有所收获,期待你的赞和转发!

相关文章
相关标签/搜索