什么是事务?
事务(transaction):最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元)。事务只和
DATA MANIPULATION LANGUAGE(数据操纵语言)语句有关,或者说只有DML语句才有事务。在事物进行过程中,未结束之前,DML语句不会更改底层数据,它只是将历史操作记录一下,在内存中完成记录。只有在事物结束的时候,而且是成功的结束的时候,才会修改底层硬盘文件中的数据。在MySQL中,默认情况下事务是自动提交的。
事务的基本要素(ACID)
- 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,同一个事务中的多条语句是不可分割的。
- 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
- 隔离性(Isolation):指多线程环境下,一个线程中的事务不能被其他线程中的事务打扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
- 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务的并发问题:
脏读
指一个事务读取到了另外一个线程中未提交的数据就会导致脏读。 示例:张三的工资为5000,事务A中把他的工资改为8000,但事务A尚未提交。 与此同时,事务B正在读取张三的工资,读取到张三的工资为8000。随后,事务A发生异常,而回滚了事务。张三的工资又回滚为5000。 最后,事务B读取到的张三工资为8000的数据即为脏数据,事务B做了一次脏读。
不可重复读
具体来说:是指在一个事务内,多次读同一条数据。在这个事务还没有结束时,另外一个事务也访问该数据。那么,在第一个事务中两次读数据之间,有可能第二个事务对该数据做了修改,那么第一个事务两次读到的的数据就是不一样的。在一个事务中前后两次读取的结果不一致,导致了不可重复读。
示例: 在事务A中,读取到张三的工资为5000,操作没有完成,事务还没提交。 与此同时, 事务B把张三的工资改为8000,并提交了事务。 随后,在事务A中,再次读取张三的工资,此时工资变为8000,两次读取到的结果不一致。
幻读
幻读通常发生在一个事务在读取范围内的数据,那么另外一个事务在这个范围内又新增或者是删除了几条数据,此时对于第一个事务就好像发生了幻觉一样,莫名其妙的多出几条数据或者是丢掉了几条数据,这就是幻读。 示例:目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。 此时,事务B插入一条工资也为5000的记录。 这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。
注意:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。 解决不可重复读的问题只需锁住满足条件的行。 解决幻读需要锁表。
事务的隔离级别以及案例:
事务隔离级别:
| 事务隔离级别 | 名称 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|---|
| read-uncommitted | 读未提交,也叫脏读 | 是 | 是 | 是 |
| read-committed | 不可重复读,也叫读已提交 | 否 | 是 | 是 |
| repeatable-read | 可重复读,默认级别 | 否 | 否 | 是 |
| serializable | 串行化 | 否 | 否 | 否 |
说明:
隔离级别为读已提交时,写数据只会锁住相应的行。
读已提交解决了脏读,但是两次读取的结果可能会不一致。
隔离级别可重复读解决了多次查询结果都是一样的问题,但是如果事务执行期间有其他事务插入新数据,此时会产生幻读。
隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有 索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
事务隔离级别为串行化时,读写数据都会锁住整张表
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
案例:
数据库:
默认数据:
查看MySQL默认的事务隔离级别: show variables like 'transaction_isolation';
注意:在MySQL8之前的命令为:select @@tx_isolation
演示事务提交
演示事务回滚
读未提交(read uncommitted)
一个事务读取到了另一个事务未提交的数据,这里读取到的数据叫做“脏数据”。这种隔离级别最低,这种级别一般是在理论上存在,实际应用中数据库隔离级别一般都高于该级别。会引发【脏读】【幻读】【不可重复读】。
示例:
第一步:打开客户端A,设置当前事务模式为read uncommitted(读未提交),查询tb_balance表zhangsan的money的初始值:
第二步:在客户端A的事务提交之前,打开另一个客户端B,更新tb_balance表zhangsan的money的值:
第三步:此时,虽然客户端B的事务还没提交,但是在客户端A已经可以查询到B已经更新的数据:
第四步:客户端B的事务因为某种原因回滚,它的所有的操作都将会被撤销。
第五步:客户端A查询到的数据其实就是脏数据了:
在客户端A执行执行更新语句zhangsan的money没有变成300,居然是400,数据不一致!
读已提交(read committed)
一个事物提交了后另一个事物才能读取到。 读已提交隔离级别高于读未提交。这种级别可以避免【脏读】,但是会导致【不可重复读】【幻读】问题。Oracle的默认级别是读已提交。
示例:
第一步:打开客户端A设置当前的事务隔离级别为read committed (未提交读)查询表tb_balance的所有记录:
第二步: 将客户端B的事务的隔离级别调整为read committed级别,并开启事务,更新表tb_balance
第三步:此时,客户端B的事务还没有提交,客户端A是不能查询到B已经更新的数据,解决了脏数据的问题:
第四步:客户端B提交事务
第五步:客户端A执行查询,结果就回发现与上一次查询结果不同,就产生了不可重复读的问题
可重复读(repeatable read)
一个事务执行过程看到的数据总是跟这个事务在开始时看到的数据是一致的,这种隔离级别高于读已提交,可以避免【不可重复读】,但是会导致【幻读】。MySQL默认隔离级别是可重复读。
示例:
第一步:打开客户端A将事务的隔离级别调整为repeatable read 级别
第二步: 打开客户端B将事务的隔离级别调整为repeatable read级别,并对表tb_balance进行更新,但并未提交事务
第三步:在客户端A查询表tb_balance的所有记录,没有出现脏读的情况。
第四步:客户端B修改数据后提交了事务。
第五步:在客户端A查询表tb_balance的所有记录,没有出现不可重复读的情况。
第六步:在客户端A执行更新语句发现数据的一致性得到保证。
之所以数据的一致性得到保证,原因是:在可重复读的隔离级别下,MySQL采用的是MVCC机制,select 操作不会更新版本号,是快照读(历史版本);而insert、update和delete会更新版本号,是当前读(当前版本)。
第七步:打开客户端B,尝试更新zhangsan的数据,会失败,这说明可重复隔离读级别下MySQL8已经不出现幻读的情况了。
串行化(serializable)
Serializable完全串行化的读,每次读都需要获得表级共享锁,读写操作相互互斥,这样可以更好的解决数据一致性的问题,但是同样会大大的降低数据库的实际吞吐性能。所以该隔离级别因为并发性比较低、损耗太大,一般很少在开发中使用。 MySQL中事务隔离级别为serializable时会锁表,因此不可能出现【脏读】、【不可重复读】、【幻读】的情况。两个事务,当一个事务操作数据库时,另一个事务只能排队等待,这种级别可以避免【幻读】,每一次读取的都是数据库中真实存在数据,多个事务之间串行,而不并发。这种隔离级别很少使用,吞吐量太低,用户体验差。
示例:
第一步:在客户端A中设置当前事务模式为Serializable,同时开启事务:
第二步:在客户端B中设置当前事务模式为serializable,插入一条记录,此时表tb_balance已经被锁定。
因为已经发生了锁表,所以此时在客户端中再次执行查询会报错: