举个例子:
比如说,我们需要让张三转钱给李四,那么在数据库里面就有3步,第一步就是在数据库中找到张三这个人的钱,第二步就是在张三数据库中减去,第三步在李四数据库中加上。
那么如果在第二步突然断电了,张三钱减去了,但是李四的钱没有加上,那么该怎么办呢?所以mysql中引入了事务这个概念。把这3步做完或者是一个或者多个sql语句的集合,叫做事务。
事务本不是数据库类软件天然有的,事务本质工作其实是为了简化程序员的工作模型。
备注:我们后面把 MySQL 中的一行信息,称为一行记录。
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
不过mysqld要提供事务的机制,就注定了mysqld内部编码和数据结构的支持。因为mysqld一定会同时存在多个事务,所以mysqld要对多个事务以某种数据结构和算法管理起来。
所有,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:
上面四个属性,可以简称为 ACID 。
原子性(Atomicity,或称不可分割性)
一致性(Consistency)
隔离性(Isolation,又称独立性)
持久性(Durability)。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
查看数据库引擎:
这里的transactions的意思就是事务。
事务的提交方式常见的有两种:自动提交和手动提交。
查看事务提交方式:
这个意思是mysql的事务是会自动被提交的。
用 SET 来改变 MySQL 的自动提交模式:
SET AUTOCOMMIT=0 禁止自动提交,SET AUTOCOMMIT=1 开启自动提交。
为了便于演示,我们将mysql的默认隔离级别设置成读未提交。具体操作我们后面专门会讲,现在已使用为主。
需要重启终端,进行查看。
下面我们要用两个客户端来进行操作,首先,创建一个简单的银行测试表:
正常演示 - 证明事务的开始与回滚:
查看事务是否自动提交。我们故意设置成自动提交,看看该选项是否影响begin。
开始一个事务。
创建一个保存点save1。
插入一条记录后,我们再创建一个保存点save2。
然后我们再插入一条数据,发现两条记录都在。
下面我们回滚到保存点save2:
此时我们在保存点2后插入的数据就没有了。
直接rollback,回滚在最开始。
非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
当前表内无数据,并且依旧是自动提交。
begin也是开启事务的一种方式。数据已经存在,但没有commit。我们直接ctrl + \ 异常终止MySQL。
终端A崩溃了。
我们在终端B查看数据自动回滚到最开始的地方。
非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
commit就是提交事务。
在终端B可以看到数据存在了,所以commit的作用是将数据持久。
非正常演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
我们把事务提交方式设置成关闭。
终端A崩溃后,我们查看终端B的。
所以,autocommit是否自动提交,并不影响用户手动对事务的操作。
非正常演示4 - 证明单条 SQL 与事务的关系
我们这里关闭自动提交,然后再插入一条数据。
崩溃前插入是有的。我们再查看终端B。
可以看到我们的王五没了,这里我们也没有启动事务,自动提交也是关闭的。
这是我们之前一直写的单SQL,我们再查看一下终端B。
王五存在数据库中。
结论:单SQL默认是以事务的方式进行提交的,只不过你的事务只有一个SQL语句。
autocommit 是否自动提交,并不影响用户手动开启事务。它影响的是:如果我们没有手动开启事务,就默认我们的SQL就是一个事务,SQL执行完毕会按照autocommit 来决定是否提交。
事务操作注意事项:
如何理解隔离性?
MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行。
一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。
数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
前面我们做的例子都是读未提交的。
事务中,所谓的提交commit,是不是把数据刷盘了呢?
不是,刷盘的过程是mysqld自己会执行的。commit设置事务的状态,表示该数据已经算是交付给mysqld。
几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用。
此时的置隔离级别为 读未提交。
我们在终端A更新了数据,但是没有进行提交。
但是我们在终端B读到终端A更新但是未commit的数据。
一个事务在执行中,读到另一个执行中事务的更新(或其它操作)但是未commit的数据,这种现象叫做脏读(dirty read)。
只有你提交了,才能被其它读到。
我们设置读提交,然后需要重新启动。
现在的设置就是读提交了。
两边同时开启事务,并且终端A进行更改,但是没有commit。
终端A的发生了变化,终端B的没有发生变化。
提交了之后,两边都发生了变化。但是我们的终端B还在事务中。并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段
(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读。
这个是问题吗?
很多人认为这是正常的,因为没有提交的时候看不到,提交了才能看到。但是这确实是一个问题。
举个例子:
比如说在公司发年终奖,工资在[2000,3000]给个ipad,工资在[4000,5000]给个华为手机。那么假设张三的工资在3000,那么在事务筛选的时候,张三是ipad,但是此时公司的老板看张三今年表现不错,给张三的工资涨了1000,那么张三的工资就来到了4000,那么在筛选[4000,5000]时又有了张三,说明给张飞发了2个礼物。那么就存在着问题。
我们进行设置可重复读。
然后两边同时开启事务:
然后我们进行操作数据:
终端B看不到修改的数据,那么我们试试提交。
还是看不到。
可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
我们在终端B进行提交commit后,再次查看数据,数据就更新了。
我们的mysql采用的就是这个方式,其它数据库在可重复读情况的时候,无法屏蔽其它事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。
对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用。
我们在这里先启动的是终端A,后启动的是终端B。两个读取不会串行化,共享锁。
终端A中有更新或者其它操作,会阻塞。直到终端B事务提交。
超出时间会有报错。
此时终端B还看不到事务A的修改。
当终端A结束后才能看见。