一、概念
事务:就是保证一组数据库操作,要么全部成功,要么全部失败,即这一组数据库操作是不可再分的整体;如果事务中某条SQL语句一旦执行失败或产生错误,将会回滚,所有受到影响的数据将返回到事务开始以前的状态;如果事务中的所有SQL语句均执行成功,则事务被顺利执行,更新的数据被持久化到磁盘。
在mysql中,事务支持是在引擎层实现的
。mysql是一个支持多引擎的系统,但不是所有引擎都支持事务,如MyISAM引擎、memory引擎就不支持事务,InnoDB、BDB支持事务。
二、 以银行转账业务来理解事务
关于银行账户转账操作,账户转账是一个完整的业务,最小的单元,不可再分————————也就是说银行账户转账是一个事务
以下是银行账户表t_act(账号、余额),进行转账操作
actno balance
1 500
2 100
转账操作
update t_act set balance=400 where actno=1;
update t_act set balance=200 where actno=2;
如果不借助事务,银行转账业务将发生无法接受的结果,如:第一条语句执行成功actno为1的账户转出100;但是第二条语句执行失败,那actno为1的账户少了100,但是actno为2的账户却没有增加100,丢失了100,转账前后资金总量不一致。
借助事务可以保证数据准确:以上两条DML语句必须同时成功或者同时失败。最小单元不可再分,当第一条DML语句执行成功后,并不能将底层数据库中的第一个账户的数据修改,只是将操作记录了一下;这个记录是在内存中完成的;当第二条DML语句执行成功后,和底层数据库文件中的数据完成同步。若第二条DML语句执行失败,则清空所有的历史操作记录,恢复到事务开始前的状态。虽然转账失败,但是这种情况仍然认为是正确的,再重新执行一次转账即可。
三、关于事务的一些术语
开启事务:Start Transaction
事务结束:End Transaction
提交事务:Commit Transaction 事务提交后,将数据更新持久化到数据表内
回滚事务:Rollback Transaction 有sql执行失败,恢复到事务开始前
开启标志:
- 默认情况下任何一条DML语句(insert、update、delete)执行,标志事务的开启,一条sql语句执行后自动提交。(在默认情况下MySQL开启的是autocommit模式,这就表示除非显式地开始一个事务,否则每个sql都被当做一个单独的事务自动执行。)
- 用户可以通过Start Transaction命令显示开启事务,或设置提交为手动提交,此时只有用户手动提交或回滚,这一事务才结束。
结束标志(提交或者回滚):
- 提交:成功的结束,将所有的DML语句操作历史记录和底层硬盘数据来一次同步
- 回滚:失败的结束,将所有的DML语句操作历史记录全部清空,恢复到事务开始前
当commit事务后,一个事务就结束了,此后再执行rollback,数据不会恢复到事务执行前,因为事务已经结束,数据更新持久化到了磁盘。
启动事务的方法有两种:
1、begin或START TRANSACTION ,推荐用START TRANSACTION 是SQL-99标准启动一个事务。
start transaction;
update from account set money=money-100 where name=‘a‘;
update from account set money=money+100 where name=‘b‘;
//rollback;
commit;
解释: 这样start transaction 手动开启事务,需要commit 手动关闭事务。
这样的事务结束之后回到自动提交方式。commit和rollback时可以设置两个属性,chain和release(如:(commit and chain)):chain会立即开启一个新事务,并和刚才事务具有相同的隔离级别,通过commit和rollback结束;release则会断开和客户端的连接。
2、默认的时候autocommit=1 自动提交是开启的
,所以你可以理解为每条语句一输入到mysql就commit 了。通过set autocommit可以修改当前数据库连接的提交方式
,如果设置为0,则设置之后的所有事务都需要通过明确的命令进行提交或回滚。
设置之后,执行任意一条sql语句,事务就启动,包括后面执行的sql语句,都一直处于某个事务中
,直到执行一条commit提交或rollback语句才会结束当前事务。后面执行的sql(块),仍然会处于另一个事务
,需要手动commit或rollback。
set autocommit=0 ;
update from account set money=money-100 where name=‘a‘;
update from account set money=money+100 where name=‘b‘;
//rollback;
commit;
// 默认都不提交,只有手动键入commit 时候才提交。
综述:一般使用1 方法。
四、 undo 日志文件
undo log主要为事务的回滚服务。
undo记录了数据在事务开始之前的值,当事务执行失败或者ROLLBACK时可以通过undo记录的值来恢复数据。例如 AA和BB的初始值分别为3,5。
A 事务开始
B 记录AA=3到undo_buf
C 修改AA=1
D 记录BB=5到undo_buf
E 修改BB=7
F 将undo_buf写到undo(磁盘)
G 将data_buf写到datafile(磁盘)
H 事务提交
通过undo可以保证原子性、稳定性和持久性
如果事务在F之前崩溃由于数据还没写入磁盘,所以数据不会被破坏。
如果事务在G之前崩溃或者回滚则可以根据undo恢复到初始状态。
数据在任务提交之前写到磁盘保证了持久性。
在mysql中还有redo重做日志,用于数据库宕机重启后的数据恢复;binlog
归档日志,用于数据库备份恢复,这里不详细讲解。
五、事务的四大特性
数据库事务拥有以下四个特性, 即 ACID 性质:
原子性(Atomicity): 事务作为一个整体被执行, 包含在其中的对数据库的操作要么全部执行成功, 要么全部失败回滚. 对于一个事务来说, 不可能只执行其中一部分操作, 这就是事务的原子性.
一致性(Consistency): 事务操作前与事务操作后总量保持一致.
隔离性(Isolation): 多个事务并发执行时, 一个事务的执行不应影响其他事务的执行。如账号a给账户b转账,不应该影响账号c给账户b转账
持久性(Durability): 已被提交的事务对数据库的修改应该永久保存在数据库中,持久化到磁盘上。
六、事务的隔离性
隔离性的作用是保证多事务处理时数据的正确性
在Mysql中,事务主要有四种隔离级别:
Read uncommitted (读未提交) 一个事务还没有提交时,它做的变更就能被其他事务看到
Read committed (读已提交) 一个事务提交后,它做的变更才会被其他事务看到
Repeatable read (可重复读) 一个事务执行过程中看到的数据,总是跟这个事务启动时看到的 数据一致。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
Serializable (可串行化) 事务执行时不允许其他事务并发执行.最高的隔离级别。意思是说这个事务执行的时候不允许别的事务并发写操作的执行.完全串行化的读,只要存在读就禁止写,但可以同时读,消除了幻读。这是事务隔离的最高级别,虽然最安全最省心,但是效率很低。
6.1 设置隔离级别
1.全局修改,修改mysql.ini配置文件,[mysqld]节里类似如下设置该选项:
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}
2. 命令行方式修改
用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。它的语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
GLOBAL:设置全局的事务隔离级别, 用这个方式修改完后,已经存在的连接还不会生效,只有对新建立的连接才会起作用(官方文档:Existing sessions are unaffected)
。当一个新的会话连接时,设置为当前全局隔离级别。
SESSION:设置当前session的事务隔离级别,指定 SESSION 关键字则是只修改当前连接会话的设置,修改完后立即生效,再次连接后会恢复成全局的设置,对其他连接/会话也不起作用
。当前未结束的事务不会生效(官方文档: but does not affect the current ongoing transaction)。
如果语句没有指定GLOBAL或SESSION,本次连接当前事务还未起作用,要到下一个(尚未启动的)新事务才起作用,下一个事务结束后,又将恢复成本次 SESSION中原先的设置。
官方文档给出的解释是这样的:
Without any SESSION or GLOBAL keyword, the statement applies to the next (not started) transaction performed within the current session. Subsequent transactions revert to using the SESSION isolation level.
6.2 查询隔离级别
你可以用下列语句查询全局和会话事务隔离级别:
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
SELECT @@tx_isolation; 查询当前会话的隔离级别
SELECT @@session.tx_isolation;和SELECT @@tx_isolation;两种方式是等价的,没有区别
,可以把SELECT @@tx_isolation;理解为一种省略写法
6.3 各个隔离级别引发的问题:
脏读
:A事务,会读取到B事务还未提交的数据。因为B事务可能会因为各种原因数据回滚,所以如果A事务读取了B未提交的数据,然后基于此进行一些业务操作,但是B事务发生错误回滚了,那A事务的业务操作就错了。
不可重复读
:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读
:在一个事务生命周期内,会查询到另外一个事务新插入的数据。例如:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结
:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
七、隔离级别示例
首先创建一个表account。创建表的过程略过(由于InnoDB存储引擎支持事务,所以将表的存储引擎设置为InnoDB)。表的结构如下:
然后往表中插入两条数据,插入后结果如下:
为了说明问题,我们打开两个控制台分别进行登录来模拟两个用户(称为用户A和用户B),并设置当前MySQL会话的事务隔离级别。
7.1读未提交
事务中的修改,即使没有提交,也会被其他事务读取。
设置隔离级别为为提交读:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
具体用户A的操作如下:
start transaction;
select * from account;
结果如下:
用户B的操作如下:
start transaction;
update account set account=account+200 where id = 1;
随后我们在A用户中查询数据,结果如下:
可以看到,事务A读取到了事务B未提交的数据,但如果此时事务B回滚,就会导致脏读
。因此,实际中,一般不会采用这种隔离级别。
实际上我们的数据改变了吗?
答案是否定的,因为只有事务commit后才会更新到数据库。
7.2 读已提交
读已提交隔离级别可以解决上述脏读问题,其只能读到其他事务已经提交的数据。
更改数据库隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
在用户A所在的会话中我们执行下面操作:
update account set account=account-200 where id=1;
我们将id=1的用户account减200。然后查询,发现id=1的用户account变为800。
在B用户所在的会话中查询:
select * from account;
结果如下:
我们会发现数据并没有变,还是1000。
接着在会话A中我们将事务提交:
commit;
在会话B中查询结果如下:
读已提交可能存在的问题:
那就是我们在会话B同一个事务中,读取到两次不同的结果。这就造成了不可重复读
,就是两次读取的结果不同。这种现象叫不可重复读。–而可重复读隔离级别可以解决这个问题。
7.3可重复读
更改数据库隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
在会话B中:
start transaction;
接着在会话B中查询数据:
我们在A用户所在会话中为表account添加一条数据:
insert into account(id,account) value(3,1000);
commit;
然后我们查询看数据插入是否成功:
回到B用户所在的会话,我们查询结果:
用户B在他所在的会话中想插入一条新数据id=3,value=1000。来我们操作下:
什么?竟然插不进去,说我数据重复?
用户B当然不服啊,因为查询到数据只有两条啊,为什么插入id=3说我数据重复了呢?
我再看一遍,莫非我眼花了?
试想一下,在实际中用户A和用户B肯定是相互隔离的,彼此不知道操作什么。用户B碰到这种现象,肯定会炸毛的啊,明明不存在的数据,插入却说主键id=3数据重复了。
当我们将当前会话的隔离级别设置为repeatable read的时候,当前会话可以重复读,就是每次读取的结果集都相同,而不管其他事务有没有提交。
出现的问题:
一个事务中读取的数据一致(可重复读),数据已经发生改变,但是我还是要保持一致。但是出现了用户B面对的问题,这种现象叫幻读
。
读取两次返回的结果集不同,这是幻读不准确的解释,比较准确的解释应该是:事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前检测读获取到的数据如同鬼影一般。其实,insert也属于隐式获取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
注意
:对于可冲度隔离级别,如果开启事务a和事务b,先在事务a中修改了数据,然后再commit;再在事务b中第一次读取,这此时查询的结果仍然还是更新后的数据,后面再次查询时才不会被更新。(实际操作验证得出的结论)
7.4 serializable(串行化)
同样,我们将用户B所在的会话的事务隔离级别设置为serializable并开启事务。
set session transaction isolation level serializable;
start transaction;
在用户B所在的会话中我们执行下面操作:
select * from account;
结果如下:
那我们这个时候在用户A所在的会话中写数据呢?
我们发现用户A所在的会话陷入等待,如果超时(这个时间可以进行配置),会出现Lock wait time out提示:
如果在等待期间我们用户B所在的会话事务提交,那么用户A所在的事务的写操作将提示操作成功。
当我们将当前会话的隔离级别设置为serializable的时候,其他会话对该表的写操作将被挂起。可以看到,这是隔离级别中最严格的,但是这样做势必对性能造成影响。所以在实际的选用上,我们要根据当前具体的情况选用合适的。
八、事务和锁的关系
现在的很多软件都是多用户,多程序,多线程的,对同一个表可能同时有很多人在用,为保持数据的一致性,所以提出了事务的概念。事务是在引擎层支持的,但是mysql中只有InnoDb和BDB等引擎支持事务,MyISAM、Memory等引擎并不支持事务。
没有事务该如何保证多用户同时操作数据库时数据的正确性呢? 这个要用到另外一种技术叫做LOCK !
并且多事务并发处理也可能存在数据丢失问题,即最后一个事务对一行数据的更新会覆盖之前事务对该行的修改,所以也需要用到锁机制。
实际上事务的4 个隔离级别所用的技术就是LOCK !
常见的锁:共享锁、排它锁、行级锁、表级锁等。
本篇文章对锁就不再做详细介绍,会在其他文章中介绍。