目录
一、数据库多客户端访问问题
1. 数据库的CURD无限制带来的问题
在了解什么是事务之前,我们要先了解一下数据库中可能出现的CURD问题。
大家知道, 一个数据库是可能同时被多个客户端访问的,这些客户端在访问时就可能会对数据库进行CURD操作,如果不对客户端的CURD操作加以限制,就可能出现各种问题。
举个例子,假设现在有一个火车票的购票系统,里面只剩1张票。此时分别有客户端A和客户端B要通过这个购票系统买票。当客户端A买完票后,购票系统就需要修改数据库中的剩余票数为0。但是在购票系统修改数据库中的剩余票数之前,客户端B也要买票。由于此时数据库中的剩余票数还没有更新,依然是1,此时客户端B就也会买到票。这就导致了客户端A和客户端B买到了同一张票,出现问题。
2. 如何解决CURD导致的问题
看了上面的问题, 大家如果学过线程,就应该会发现,数据库中的CURD问题本质上就是多个客户端在没有限制的情况下随意访问导致的,不就和线程安全如出一辙么。因此,要解决这个问题,第一个方面就是要让这个购票过程是原子的。
在上面的购票过程中,如果客户端A在买票出票的过程中,客户端B也过来买票出票,很明显就会出问题。因此,第二个方面就是要让买票双方不能互相影响。
当我们买完票后,这张票在理论上就已经是永久属于我了,无论这张票有没有过期,只要我买下了这张票,它就是我的。不能说我买了票,这张票却不属于我。因此,买完票后这张票应该是永久的,是持久化的。
当我们在买票的过程中,我们的状态只会存在没买票,就是没买;买了票,就是买了。不存在说什么可能买了或没买。这就意味着,在购票的过程中,我们的状态应该是明确的,是确定的。
只要解决了上面的四个问题,整个买票过程就是有效的,没有问题的。而这四个问题,也就是数据库CURD需要解决的问题。
二、事务的概念
1. 什么是事务
举个例子,假设现在我们要向一个人转账,在这个转账的过程中就需要对数据库中保存的数据做修改。当我们转账时,在程序员的角度来看,就是先通过一条update语句减去一方的金额,再用一条update语句增加另一方的金额。即用两条sql语句完成。但是从用户的角度来看,这两条sql语句组合起来才能完成一次转账,是一个整体。
通过这个例子我们就应该知道,在现实中很多工作是无法用一条sql语句完成的,而需要用一批sql语句完成。虽然在程序员眼中这两条sql语句并没有什么,但是在用户的眼中却是构成“转账”所必须的操作。因此,在这个例子中,这两条sql语句合在一起,我们就不将它们叫做“两条sql语句”,而是叫做“事务”。
要理解“事务”,就不能站在底层语言的角度,而需要站在上层用户的角度来看。“事务”其实就是上层用户需要具体完成的一个应用层的功能,这个功能可能需要由多条sql语句构成。简单来讲,“事务”其实就是一条或多条sql语句组合起来所能完成的一个具体业务。例如转账这个具体业务就需要两条sql语句完成,此时这两条sql语句就在数据库中构成一个“事务”。
总的来说,事务就是一组由DML语句组成的,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。mysql中提供了一种机制,保证我们达到这样的效果。事务还规定不同的客户端能看到的数据是不相同的。
一般来讲,事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。举个例子。例如有一个人在网络上发表了一些不当言论,需要被封号处理。此时平台就可能需要删除这个人在这个平台上的所有相关信息,如姓名、电话、以前发表的各种评论等等。这些操作就需要多条mysql语句构成,这些所有操作合起来,就构成了一个事务。
同时我们要知道,一个mysql数据库中,往往不止一个事务在运行。在同一时刻,可能有大量的请求被包装成事务,向mysql服务器发起事务处理请求。此时就可能出现不同的事务需要通过sql语句获取或修改同一份数据,此时如果不对加以保护,就会出现问题。甚至于,由于一个事务有多条sql语句组成,就可能出现事务中的sql语句才执行一部分,发起事务的客户端就因为某种原因崩溃退出了,那这些已经处理过的数据和已经执行过的sql语句该怎么办呢?
因此,虽然事务由多条sql语句组成,但它绝对不是简单的sql语句集合,还需要有其他东西来保证它的安全。保证事务安全的方法,其实就是让事务拥有四个属性。
2. 事务的四个属性
(1)原子性
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间的某个环节。事务在执行过程中发生错误,会被“回滚(Roolback)”到事务开始前的状态,就像这个事务从来没有执行过一样。
(2)一致性
在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性的完成预定的工作。
简单来讲,一致性就是指事务处理后的结果是可预期的。例如你要向你朋友转100元,你的预期就是在转账后你的账户会少100,对方的账户会多100。数据库在处理完这个事务后,数据库中的结果和预期一样,这就被称作一致性。
(3)隔离性
数据库允许多个并发事务同时对其数据进行读写和修改。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括“读未提交(Read uncommitted)”、“读提交(read committed)”、“可重复读(repeatable read)”和“串行化(Serializable)”。
(4)持久性
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。可以理解为一个事务处理完后,它的处理结果才会真正的写入到数据库中。
在这四个属性中,原子性、隔离性和持久性都是需要数据库能够做到的。而一致性,数据库中没有对其进行任何处理。原因就是,当数据库中保证了事务的原子性、隔离性和持久性后,也就能在很大程度上保证一致性。但要完全保证一致性,单靠数据库是不行的,还需要用户的配合。
在这上面的原子性(Atomicity)、一致性(Consistency)、持久性(Isolation)和持久性(Durability)共同简称为“ACID”。
3. mysql对事务的管理
在上文中说了,一条或多条sql语句的组合,就被称为“事务”。但我们知道,一个数据库在同一时刻是可能被多个客户端访问的,这就意味着在数据库中一定会存在大量的事务,这些事务属于不同的客户端。由此,数据库就必然需要将这些事务通过某种方式组织并管理起来,即“先描述,再组织”。因此,在数据库看来,事务就是就是客户端请求中来了一批sql,数据库将这些sql打包成一个事务对象,然后将这个事务对象放入对应的事务执行列表中执行。
4. 为什么会有事务
事务这个东西,其实并不是在数据库设计之初就有的,而是在数据库的不断使用中发现,需要有事务之后,才被设计出来的。
试想一下,假设你在要用C\C++这类编程语言连接数据库,在你写对应的程序的时候,还需要你自己去处理数据库中的并发访问问题、网络异常后的处理问题等等,无疑会在很大程度上增加程序员的负担。
因此,事务被mysql编写者设计出来之后,其本质是为了当应用程序访问数据库的时候,事务能够简化上层的编程模型,不需要让程序员去考虑各种各样的潜在错误和并发问题。当我们在使用事务时,要么提交,要么回滚,我们不需要去考虑网络异常、服务器宕机等等问题。因此,事务的本质就是为应用层服务的,并不是伴随着数据库系统天生就有的。
5. 事务的版本支持
虽然事务很有用,但并不是每个引擎都支持事务的。在mysql常用的引擎中,只有Innodb引擎才支持事务,MyISAM引擎不支持。其他不常用的引擎也不支持事务。
大家可以在linux中登录mysql,输入“show engines \G”命令查看mysql数据库下的数据库引擎支持情况:
因为在mysql中最常用的就是innodb和MyISAM引擎,所以这里就只截了这两个引擎的内容。
可以看到,在InnoDB引擎中,明确写了“supports transaction”,即支持事务。而MySIAM引擎则是不支持。
三、事务的操作
1. 事务提交方式
在mysql中,要提交一个事务,有两种提交方式,分别为“自动提交”和“手动提交”。
要知道mysql中是否开启了自动提交,输入“show variables like 'autocommit';”命令查看:
显示为ON就是已开启,显示为OFF则为已关闭。
当然,我们也可以手动修改事务的提交方式。通过“set autocommit=0\1;”命令即可。0表示关闭,1表示开启。
可以看到,当设置为0时,就关闭了自动提交。当然,我们最好还是将其设置回开启自动提交比较好。
2. 事务操作的准备工作
2.1 数据库是网络服务
首先大家要知道,数据库在本质上是一个网络服务,大家的linux上在启动了mysqld后,其实就是启动了mysql的服务端。大家登陆mysql,其实就是用mysql的客户端登陆服务端。
这就意味着,这个网络服务在未来是可以被多个客户端从远端访问的。当然,要从远端登录数据库,除了用户名和密码,还需要在数据库中对特定账户进行一些设置,该账户才能从远端登录,这里就不再多讲。
然后,在进行实际的事务操作前,我们要先将事务的隔离级别设置为“读未提交”,即“read uncommitted”。这个隔离级别是最低级别,方便我们看到事务的实际情况。
2.2 设置全局事务隔离界别
设置方法很简单,用“set global transaction isolation level 隔离级别;”命令即可:
这条sql语句的意思就是,设置全局事务隔离级别等级为”读未提交”。
当设置好后,我们可以用“select @@tx_isolation;”查看当前的会话的事务隔离级别:
此时就很奇怪了,明明在上面将隔离级别设置为了读未提交,为什么这里查出来的结果还是可重复读呢?这是因为在mysql中重新设置了全局事务隔离级别后,需要重新登录mysql,对应的事务隔离级别才会在当前会话生效:
为了方便后续测试,大家可以在linux中打开两个会话窗口并登录mysql。
2.3 创建一个测试用的表
在这里就先创建如下一个user表,在后面的几乎所有关于事务的操作都是在这个表中完成的。
3. 事务的开始与结束
在开始事务之前,我们先来看看当前的事务自动提交是否开启:
可以看到,自动提交是开启了的。这个东西大家要先记住。
要开启事务,有两种方法,第一种是“start transaction;”;第二种是“begin;”。这两条sql语句都可以用于开始一个事务。
当开始一个事务后,在开始事务的sql语句后面的sql语句,都属于同一个事务。要结束一个事务,输入“commit;”即可。
4. 事务的回滚(正常情况)
4.1 设置回滚点
在事务中,我们可以用“savepoint 回滚点名;”来设置一个回滚点。为了方便后面的讲解,在这里做一个规定,在后面的内容中,执行插入、更新、删除等会修改数据的操作的事务,叫做事务a;而查看表的事务叫做事务b。
先开始事务a:
然后设置一个回滚点:
设置好后,再向继续插入数据,并继续设置一个回滚点:
设置完成后,再在另一个窗口中开启事务b,并查看user表中的数据:
可以看到,在事务b中,是能够看到事务a刚刚插入的数据的。
在上面的插入数据和设置回滚点的事务a中,这些sql语句共同组成了一个事务。在事务b中也能看到它刚刚插入的数据。但如果我们现在后悔了,不想插入id为3的数据,而是想回到插入id为3的数据之前该怎么办呢?
此时就可以使用刚刚设置的回滚点了。使用“rollback to 回滚点名;”,就可以回到指定的回滚点:
再在事务b中次查看user表:
可以看到,此时就事务b查询到的表中的内容就没有了id为3的数据。那如果我们想回滚到s1的位置呢?
可以看到,此时表中的数据就回到了设置回滚点s1时的状态了。对于回滚点,可以理解为游戏的存档点,即通过选择一个存档点来回到游戏的某个位置,回滚点也是如此。但不同的是,回滚点只能先前回滚,不能向后回滚。
可以看到,当回滚到s1后,因为s2是在s1之后设置的,所以此时无法从s1位置回滚到s2。
当回滚到表的开始时,我们再执行commit,分别结束事务a和事务b。
结束事务后,我们在查看user表的内容:
4.2 直接回滚
可以发现,依然为空。这也就说明了,事务中的回滚操作确实会影响到数据库中的数据。但现在我们还有一个问题,那就是要实现事务的回滚,难道一定要设置通过savepoint设置回滚点吗?如果不设置会怎么样?
我们再次启动事务a,然后插入如下数据:
然后启动事务b,查看user表:
数据正常插入。然后我们再在事务a中直接回滚并在事务b中查看user表:
可以看到,如果不明确要回滚到哪个位置而直接使用rollback,就是回到打开事务时表的初始状态。
4.3 结束事务后再回滚
上面的回滚都是在事务运行期间回滚的,那如果在事务结束后回滚呢?
开启事务a,然后向里面插入如下数据后:
然后打开事务b,查看表中的数据:
结束事务a,然后执行一次回滚:
再在事务b中查看一次user表的数据:
表中的数据没有变化。
通过上面的实验就可以知道,当对一个结束后的事务进行回滚,是无效的。换言之,事务回滚只能在一个事务运行的过程中回滚。
5. 事务回滚(非正常情况)
当前有以下user表,表中的数据如下所示:
启动事务a,向里面插入如下数据:
插入完成后,在另一个窗口启动事务b,并查询user表中的数据:
可以看到,当前是能够在user表中看到id为4的数据的。然后我们直接关闭掉事务a的窗口,再在事务b中查看user表的数据:
此时id为4的数据就没有,变为了插入数据之前的状态。这就是事务的原子性。一个事务,要么做完,要么就不做,如果在中间过程中出现了异常,就会将数据回滚到这个事务启动之前的状态。mysql中的数据不会受到该事务的影响。
6. mysql中设置自动提交的作用
6.1 自动提交在手动启动事务中无效
通过上面的实验可以发现,当我们手动开启一个事务后,如果这个事务还没有提交,但客户端出现异常的时候,mysql就会将数据回滚到启动这个事务之前。但这里有一个问题。
输入“show variables like 'autocommit';”指令可以看到,自动提交是打开的,那为什么当客户端异常的时候我们所执行的事务客户端没有帮我们自动提交已经完成的内容呢?
那如果将自动提交关闭后,mysql会不会在客户端出异常的时候帮我们自动提交呢?
先将自动提交关闭:
继续测试一下。还是用上面的user表:
插入如下数据:
继续在另一个窗口中查看user表的内容,同样可以看到插入的数据。
此时我们再将事务a的窗口关闭,并在事务b中查看user表:
同样的,事务a中插入的数据还是被回滚了。那这个自动提交到底有什么用呢?很明显,在这里这个自动提交是否开启并没有起到任何作用。
6.2 单条sql语句与事务的关系
在上文中说了,事务的提交方式分为“手动提交”和“自动提交”。其实这个手动提交和自动提交就分别对应了事务的手动开启和自动开启。在上文中我们用“start transaciton;”和“beign;”这两种方式启动事务,其实都属于是“手动开始事务”。同时我们知道,“事务”其实就是一个或多个sql语句的组合。这就意味着,一个sql语句,其实也可以是一个事务。因此,其实我们在mysql中输入的每一个sql语句,都会被mysql自动识别为一个事务。
查询当前的自动提交:
再查看当前的user表数据:
可以看到,当前的自动提交是关闭的。在这里,我们不再手动启动一个事务,而是直接在mysql中执行一条sql语句:
然后在另一个窗口中查看user表中的数据:
可以看到,确实删除了id为2的数据。此时,在执行了delete语句的窗口下按“ctrl \”,强制结束掉mysql服务,再在这个窗口中查看表中的数据:
可以发现,刚刚删除的数据又重新回来了。这其实就是因为刚刚执行的delete语句在mysql中其实也是一个事务,但是我们将mysql的自动提交关闭了,导致这条sql语句在执行完后并没有结束事务。因此当我们强制结束mysql时,这个事务由于没有结束,数据库中的数据就会被回滚到delete语句执行之前。
当然,如果将自动提交打开,就不会出现这种情况。因为在没有手动开始事务的情况下,执行完一条sql语句后,mysql会自动执行commit帮我们结束该事务。
总的来讲,我们只需要记住,手动启动的事务必须要我们手动commit;自动启动的事务在自动提交启动时,mysql会自动提交。但如果自动提交关闭,就需要我们自行执行commit结束事务。
7. 总结
(1)只要输入begin或者start transaction, 事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。
(2)事务可以手动回滚。同时,当操作异常时,mysql会自动回滚。
(3)对于InnoDB,每一条sql语言都默认封装成事务,自动提交。(select有特殊情况,因为mysql有MVCC)
8. 事务操作注意事项
(1)在一个事务中,如果没有设置保存点,也可以回滚,但只能回滚到事务的开始。直接使用rollback(前提是事务还没有提交)。
(2)如果一个事务已经执行commit提交了,则无法使用rollback回滚。
(3)可以通过set savepoint语句自行设置回滚点,然后通过rollback to语句选择回到哪个回滚点。
(4)InnoDB支持事务,但MyISAM不支持事务。
(5)开始事务可以使用start transaction或begin语句。