**本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**
原文:昨天去银行转钱,最后怒失300万
大家好,我是Eric,这是我的第24篇原创文章
我的300万"不见"了
小埃年初的时候看中了一套1000万的房子,需要首付300万,小埃暂时没钱然后向朋友小克借了300万,小克借了,小埃房子也买下了。
过去了半年,现在小埃有钱了,要还给小克。小埃说我不提现了,直接去银行转账吧,小克也觉得转账方便欣然答应。
那么,小埃转账过程银行系统应该要有以下几个步骤:
-
检查小埃账户里面余额至少有300万。
-
从小埃账户余额中扣减300万。
-
给小克账户余额增加300万。
这三个步骤涉及到两次MySQL操作,一次扣减,一次增加。如果执行完步骤2,MySQL突然宕机了,那就会出现一个“灵异”事件,小埃钱少了300万,但小克账户并未收到300万,小埃没有还钱成功,钱不翼而飞了!
要解决这种“灵异”事件,就必须把上面三个步骤打包成一个事务,任何一个步骤失败,则必须回滚所有的步骤到最开始的状态。
事务(Transaction),它是并发控制的基本单位,它可以由一条简单的SQL语句组成,也可以由多条SQL语句构成,但不管如何,一个事务中的SQL语句要么都执行成功,要么都失败,他们是不可分割的。
手动开启一个MySQL事务
首先,我们平常写SQL语句基本没有“明面上接触”过事务,我们大多数基本也没有手动开启过一个事务,但是我们的SQL都运行在事务之上。
这是因为我们MySQL默认是事务自动提交的,比如我们连接MySQL,通过select @@autocommit语句或者show variables like 'autocommit'语句查看是否开启事务自动提交。
1表示开启自动提交,0表示关闭自动提交。
ON表示开启自动提交,OFF表示关闭自动提交。
我们可以通过set autocommit = 0语句改变自动提交的模式。
比如我们现在有一张表bank_balance,保存了小埃和小克的账户余额,现在小埃账号余额300万,小克账号余额0:
通过set autocommit = 0关闭事务自动提交。
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.01 sec)
update bank_balance set balance = balance - 300000000 where user_name = '小埃';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
以上流程通过开启两个cmd窗口看起来最直观,左边窗口select,右边窗口update。
如下视频,当右边窗口update而未commit前,左边窗口select看不到更新后的数据:
事务的特征都有哪些?
市面上的MySQL存储引擎很多,比如MyISAM,NDB Cluster,Maria,Memory等。MyISAM是MySQL5.5.8版本以前的默认存储引擎,但MyISAM不支持事务,至于MyISAM不支持事务的原因以后有机会写一篇哈哈~
数据库事务是在引擎层实现的,我们常见的支持事务的引擎有InnoDB,所以InnoDB也是目前MySQL用的最多的引擎。
事务有四大基本特征,简称ACID特性:
A(Atomicity),原子性:原子性指整个数据库事务是不可分割的工作单位,要么全部操作完成,要么全部操作不完成,才算整个事务成功。如果事务中有任何一段SQL语句执行失败,那么在此之前已经执行的SQL语句必须回滚,也即数据库状态必须退回到执行事务前的状态。
C(Consistency),一致性:一致性是指事务将数据库从一种状态转变成为下一种状态时,数据库的完整性约束不会被破坏。比如现在有一个事务有两个SQL语句操作表中的一个字段user_name,而user_name字段有唯一键约束,事务如下:
begin;
update table set user_name = '小埃' where user_name = '小克';
insert into table(user_name)values('小埃');
commit;
这个事务会导致表中的user_name字段变得不唯一,这就破坏了事务的一致性要求,即事务将数据库从一种状态变为了一种不一致的状态,这时系统会自动撤销事务(返回初始的状态)。
l(Isolation),隔离性:事务的隔离性代表着每个事务之间相互分离,事务A的操作在提交前对事务B不可见。这个特性可以让多个事务多步骤交替执行时数据的最终一致性得到保障。
D(Durability),持久性:持久性表示事务一旦提交,其对数据的修改就是永久性的。即使发生数据库宕机,只要磁盘没坏,数据库也能把数据恢复到宕机前的状态。
丢失的300万"回来"了
有了以上四大特征作为标准保障,当我们把小埃还钱300万这个事情打包成一个事务的话:
事务开始
-
检查小埃账户里面余额至少有300万。
-
从小埃账户余额中扣减300万。
-
给小克账户余额增加300万。
提交事务
如果在执行完第2步而未执行第3步的时候,MySQL宕机了,那么MySQL它是如何保障小埃的300万还在,且小克账户余额也没有多300万的?
其实,MySQL为了实现事务,需要并发控制和恢复机制,这两玩意说白了就是在修改确认(commit)前不让别人看到我修改的东西,以及未来得及确认前可以把数据恢复到修改前的状态。
这就涉及到MySQL的MVCC(多版本并发控制)、Read View(读视图)、redo log(重做日志)等等。
先来看一个事务的大概流程:
(ps:事务是两阶段提交,严谨地说更新数据前有一个prepare redo log阶段)
MVCC和Read View一起实现了事务的隔离性,即一个事务数据更新而未提交前,其他事务查询不到该事务更新的数据值。
redo log是一个很关键的东西(以后再专门抽一讲来说),存在于InnoDB存储引擎中,我们现在先不用太过于纠结它的底层实现,只要知道它记录了事务的数据状态,也即是数据更新之后的值,MySQL宕机重启时数据恢复也基于redo log。
回到问题本身,以上所讲“执行完第2步而未执行第3步的时候,MySQL宕机了” 主要说是发生在更新数据这个环节,当然也可能在数据更新完之后、写入redo log时或写入redo log后MySQL宕机。
由于事务的隔离性,此时MySQL宕机,更新的数据其实其他"用户"查询不到,小埃账户扣减300万后,不管小埃本人还是小克等其他人查询小埃账户余额依然是300万!
当MySQL重启的时候,系统会做crash recovery,即是根据redo log中记录的日志和MySQL的数据值来决定未提交的事务是继续提交还是回滚,如果提交一定是小埃和小克账号余额同时加减300万成功,如果回滚则什么都没发生。
“执行完第2步而未执行第3步的时候,MySQL宕机了” ,在InnoDB引擎中的表象是:redo log仍未写入但MySQL已有数据,重启时系统会把事务提交,最终看到的是小埃还款成功。
“积土成山,非斯须之作,贵在坚持而非积土。”