在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序.
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败。
在MySQL中MyISAM引擎并不支持事务,所以这里指的主要是InnoDB存储引擎(mysql默认存储引擎).
谈到事务,那肯定少不了ACID的特性,ACID是以下几个单词的缩写,下面一一对其进行介绍
原子性(atomicity):指事务是一个不可分割的工作单位,事务中的操作要么都发送,要么都不发生.
一致性(consistency):事务操作前与操作后的状态始终一致.
隔离性(isolation): 指的是每个读写事务的对象之间相互隔离,即该事务提交前对其他事务都不可见。
持久性(durability):事务一旦提交其结果就是永久性的.
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
例如我们去ATM机取钱,流程如下
插入银行卡,验证密码是否正确
从远程银行的数据库中获取当前账户的信息
用户在ATM机上输入想要提取的金额
从远程银行的数据库中更行账户信息
ATM出款
用户取款
上述的过程就应该视为一个原子操作,要么全都发生,要么就不发生。因为不可能扣账户中的钱,ATM却不出钞票,又或者是出了钞票,我的账户却没发生变化,这种行为是不容许发生的。
一致性即事务操作前与操作后的状态始终一致。
如何理解呢?就好比我们此时有用户A和用户B,他们的余额分别为300元和700元,此时两人总金额为1000元。此时若是用户B向用户A转账200元,则两者的此时都有500元,总金额还是1000元。
也就是说,无论我们两个怎么转账,总金额它只会是1000,既不会多,也不会少。这就是事务操作前后的状态始终一致。倘若钱多了或者少了,都代表着事务将数据库从一种状态变为了另外一种状态,此时就不再符合一致性了。
持久性指的是事务一旦提交,其结果就是永久性的。即使发生了服务器宕机的事故,数据库也能成功的将数据给恢复。
但是需要注意的是,只能从事务本身的角度来保证结果的永久性,即事务提交后所有变化都是永久的,但是这仅仅局限于数据库本身发生的问题,倘若是由于外部原因如RAID卡损坏、天灾人祸导致数据库发生问题,那么即使事务提交了,也可能会丢失。
基于上述原因,持久性只能保证事务系统的高可靠性,而无法保证其高可用性。
隔离性指的是每个读写事务的对象之间相互隔离,即该事务提交前对其他事务都不可见。
幻读指在同一事务中,用同样的操作读取两次,得到的记录数却不一样。(针对同一个范围的数据)。 主要原因就是当第一个事务对表中的数据进行修改,并且这个涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,向其中插入了一行。这样也就导致了操作第一个事务的用户发现表中还有没修改的数据行,像发生了幻觉一样。
明明在会话A的第一次查询中,大于2的数只有行只有一列,而由于会话B插入了新列后,对于会话A而言就凭空多出来了一列,像出现了幻觉一样。
不可重复读指的是在一个事务中多次读取同一行数据,但是多次读取的数据却不一样(主要针对同一行数据)。导致这一问题的主要原因就是一个事务读取到了其他事务已提交的数据。
例如以下情况
账户1中有300元,账户2中有500元
事务A读取账户B的内容,里面显示有500元
事务B将账户1的300元全部转给账户2,并提交事务
事务A再次读取账户B,此时里面有800元。
由于其他事务的干扰,对于事务A来说,两次读取的金额都不一样。
因为不可重复读读到的是已经提交的数据,由于其本身并不会带来很大的问题,所以大部分数据库厂商都会允许这种情况的发生,将隔离级别设置为 READ COMMITTED(读已提交)
脏读即一个事务读取到了另外一个事务中未提交的数据,也就是可能因为其他事务对数据进行修改或者回滚导致的问题。
例如下图,会话B在第一次查看时表中只有一条数据,但是在第五阶段中会话A向表中插入了另一条数据,这就导致了会话B在读取的时候得到的结果就不再一样,因为它读取到了脏数据。
脏读的现象并不会经常发生,因为脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED(读未提交),而大部分数据库的默认隔离级别都为READ COMMITTED。
丢失更新就是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。例如以下案例
事务A将行记录r更新为1,但是事务A并未提交
同时,事务B将行记录r更新为2,事务B未提交
事务A提交
事务B提交
此时由于B将A的修改覆盖,导致A虽然提交,但是更新却丢失了,只剩下了B的更新。
但是在当前数据库的任何隔离级别下,都不会导致理论意义上的丢失更新问题,即使是隔离级别最低的Read Uncommitted,也由于加锁保护,所以事务B的修改操作会被阻塞,直到事务A提交。
这也就是为什么通常大家讨论的只有幻读、脏读、丢失更新的原因。
为了解决上述问题,MySQL中实现了以下四种隔离级别,隔离级别由上往下依次增加.
需要注意的是隔离级别越高,事务请求的锁也就越多,保持锁的时间也就越长。所以隔离性越强,并发的效率也就越低。
在该隔离级别下,所有事务都可以看到其他未提交事务的执行结果。在该级别下,虽然并发的效率最高,但是安全性完全没有得到保护,所以很少用于实际应用。
该隔离级别是大部分数据库默认的隔离级别,如Oracle、SQL Server等。该隔离级别满足了隔离的简单定义,即一个事务只能看见提交了的事务所做的改变,虽然它还有不可重复读的问题,但是我前面也说了,不可重复读本身并不是一个大问题,所以为了兼顾到性能,大部分数据库都会容许这种问题的产生。
这是MySQL中InnoDB默认的隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。也正是因为这样,又导致了幻读的问题,但是InnoDB可以借助MVCC中的Next-Key Locking的加锁方式来解决这个问题,具体的解决方法我会在下一篇博客中讲解。
这是最高的隔离级别,通过强制事务进行排序,使事务之间不可能互相冲突,从而解决了其他隔离级别无法解决的幻读问题。由于其在每个读的数据行上加了共享锁,所以在该隔离级别下可能会导致大量的超时现象以及锁竞争。
这四种隔离级别分别可能发生的问题如下图所示
开启事务
START TRANSACTION
或者
BEGIN
(由于MySQL的数据分析器会自动将BEGIN识别为BEGIN...END,所以在存储过程中只能使用START TRANSACTION来开启事务)
提交事务
COMMIT
回滚事务
//回滚整个事务
ROLLBACK
//回滚至某个保存点
ROLLBACK TO SAVEPOINT 保存点ID
设置保存点
SAVEPOINT 保存点ID
删除保存点
RELEASE SAVEPOINT 保存点ID
设置隔离级别
SET TRANSACTION
事务的持久性主要依靠redo log(重做日志)来完成,而原子性、一致性则通过undo log(撤销日志)来完成。至于隔离性,则通过锁来完成,下面就分别介绍一下redo log 和 undo log分别是什么,如何实现事务的特性。
首先要知道,redo和undo的左右都可以视为一种恢复操作,所以undo绝不是redo的逆过程。其中redo恢复提交事务修改的页操作,而undo回归行记录到某个特定版本。也因为如此,两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。而undo是逻辑日志,根据每行记录进行记录。
redo log由两部分组成,一是存在于内存中的重做日志缓冲(redo log buff),由于存在内存中,所以其具有易失性。二是重做日志文件(redo log file),其存在于硬盘中,所以是持久的。
redo主要通过Force Log at Commit机制来实现事务的持久性。
步骤如下
当事务提交时,将该事务的所有重做日志从缓冲区中写入到重做日志文件进行持久化,当事务的提交完成后才算完成。为了确保每次重做日志每次都写入文件中,在每次将日志缓冲写入日志文件后,都会发起一次异步操作(fsync)
为什么需要这个异步调用呢?因为重做日志文件打开时并没有使用O_DIRECT选项,所以重做日志缓冲会先写入文件系统缓冲,为了保证其能够成功写入磁盘,必须发起一次异步调用。由于异步调用的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,即数据库性能。
当事务需要回滚操作的时候,就需要用到undo。undo是撤销日志,其中保留了数据库各个版本的状态,我们可以借助undo逻辑地将数据库恢复到原来地样子。除了进行回滚之外.
首先看看undo log的生成流程
从一上两图可以看出,每当事务发生变更的时候,都会伴随着undo log的产生,并且为了防止其丢失,undo log会比数据先持久化到硬盘上。
由于undo log是逻辑日志,所以其中记录的都是对于数据库的操作指令。而事务的回滚,其实也就是根据这个操作来进行一个逆向操作。如下面几种
当执行一个insert指令时,其逆向指令为delete
当执行一个delete指令时,其逆向指令为insert
当执行一个update指令时,其逆向指令为update
原子性就是借助以上机制实现,倘若事务中的某一个步骤未能成功完成,则借助undo log中存储的记录来回滚到事务的最原始状态
而至于一致性,则主要依靠上述的其他三种特性来实现,也就是说一致性是目的,而原子性、隔离性、持久性则是数据库实现一致性的手段,只有满足这三个性质,才能够保证一致性。