MySQL 事物详解

事务(Transaction)是数据库区别于文件系统的重要特性之一,事务会把数据库从一种一致性状态转换为另一种一致性状态。在数据库提交时,可以确保要么所有修改都已保存,要么所有修改都不保存。

事务的ACID特性

事务必须同时满足ACID的特性:
原子性(Atomicity) 。事务中的所有操作要么全部执行成功,要么全部取消。
一致性(Consistency) 。事务开始之前和结束之后,数据库完整性约束没有破坏。
隔离性(Isolation) 。事务提交之前对其它事务不可见。
持久性(Durability) 。事务一旦提交,其结果是永久的。

事务的实现

事务隔离性由锁来实现。原子性、一致性、持久性通过数据库的redo log 和undo log来完成。redo和undo的作用都可以视为一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本。因此两者记录的内容不同,redo通常是物理日志,记录的是页的物理修改操作。undo是逻辑日志,根据每行记录进行记录。
1、redo log
在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。如下一个简单示例:
记录1:<trx1, insert...>
记录2:<trx2, delete...>
记录3:<trx3, update...>
记录4:<trx1, update...>
记录5:<trx3, insert...>
2、undo log
undo log主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
以下是undo+redo事务的简化过程
假设有2个数值,分别为A和B,值为1,2
1. start transaction;
2. 记录 A=1 到undo log;
3. update A = 3;
4. 记录 A=3 到redo log;
5. 记录 B=2 到undo log;
6. update B = 4;
7. 记录B = 4 到redo log;
8. 将redo log刷新到磁盘
9. commit
在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。

事务的隔离级别

READ_UNCOMMITTED(读未提交)
READ_COMMITTED(读已提交)
REPEATABLE_READ(可重复读)
SERIALIZABLE(序列化)
从上往下,级别越来越高,并发性越来越差,安全性越来越高,反之则反。其实,定义这四个级别就是为了解决数据在高并发下所产生的问题,那又有哪些问题呢

1. Dirty Read(脏读)

  首先区分脏页和脏数据, 脏页是内存的缓冲池中已经修改的page,未及时flush到硬盘,但已经写到redo log中。读取和修改缓冲池的page很正常,可以提高效率,flush即可同步。脏数据是指事务对缓冲池中的行记录record进行了修改,但是还没提交!!!如果这时读取缓冲池中未提交的行数据就叫脏读,违反了事务的隔离性。脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交 数据库 ,这时,另外一个事务也访问这个数据,然后使用了这个数据。

时间 事务 A(存款) 事务 B(取款)
T1 SET @@tx_isolation='read-uncommitted'  
T2 开始事务  
T3   SET @@tx_isolation='read-uncommitted'
T4   开始事务
T5   查询余额(1000 元)
T6   取出 1000 元(余额 0 元)
T7 查询余额(0 元)  
T8   撤销事务(余额恢复为 1000 元)
T9 存入 500 元(余额 500 元)  
T10 提交事务  

余额应该为 1500 元才对!请看 T7时间点,事务 A 此时查询余额为 0 元,这个数据就是脏数据,它是事务 B 造成的,明显事务没有进行隔离,渗过来了,乱套了。
所以脏读这件事情是非常要不得的,一定要解决掉!让事务之间隔离起来才是硬道理。
2.Unrepeatable Read(不可重复读)
不可重复读是指在一个事物内多次读取同一数据集合。在这个事物还没有结束时,另外一个事物也访问该同一数据集合,并做了一些DML操作。因此,再第一个事物中的两次读数据之间,由于第二个事物的修改,那么第一个事物两次读到的数据可能是不一样的。这样就发生了在一个事物内两次读到的数据是不一样的情况,这种情况称为不可重复读。
不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了数据库事务一致性的要求。
时间 事务 A(存款) 事务 B(取款)
T1 SET @@tx_isolation='read-committed'  
T2 开始事务  
T3   SET @@tx_isolation='read-committed'
T4   开始事务
T5   查询余额(1000 元)
T6 查询余额(1000 元)  
T7   取出 1000 元(余额 0 元)
T8   提交事务
T9 查询余额(0 元)  
事务 A 其实除了查询了两次以外,其他什么事情都没有做,结果钱就从 1000 变成 0 了,这就是重复读了。可想而知,这是别人干的,不是我干的。其实这样也是合理的,毕竟事务 B 提交了事务,数据库将结果进行了持久化,所以事务 A 再次读取自然就发生了变化。
这种现象基本上是可以理解的,但在有些变态的场景下却是不允许的。毕竟这种现象也是事务之间没有隔离所造成的,但我们对于这种问题,似乎可以忽略。
3.Phantom Read(幻读)
幻读是指在同一事务下,连续执行两次相同的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前 不存在的行

时间 事务 A(统计总存款) 事务 B(存款)
T1 开始事务  
T2   开始事务
T3 统计总存款(10000 元)  
T4   存入 100 元
T5   提交事务
T6 统计总存款(10100 元)  
银行工作人员,每次统计总存款,都看到不一样的结果。不过这也确实也挺正常的,总存款增多了,肯定是这个时候有人在存钱。但是如果银行系统真的这样设计,那算是玩完了。这同样也是事务没有隔离所造成的,但对于大多数应用系统而言,这似乎也是正常的,可以理解,也是允许的。银行里那些恶心的那些系统,要求非常严密,统计的时候,甚至会将所有的其他操作给隔离开,这种隔离级别就算非常高了(估计要到 SERIALIZABLE 级别了)。不过MySQL InnoDB 存储引擎中 REPEATABLE_READ隔离级别 采用Next-Key Locking的算法也可以避免幻读。

归纳一下,以上提到了事务并发所引起的跟读取数据有关的问题,各用一句话来描述一下:
  1. 脏读:事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。
  2. 不可重复读:事务 A 读取了事务 B 已提交的更改数据。
  3. 幻读:事务 A 读取了事务 B 已提交的新增数据。
第一条是坚决抵制的,后两条在大多数情况下可不作考虑。

这就是为什么必须要有事务隔离级别这个东西了,它就像一面墙一样,隔离不同的事务,看下面这个表格,您就清楚了不同的事务隔离级别能处理怎样的事务并发问题:
事务隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED 允许 允许 允许
READ_COMMITTED 禁止 允许 允许
REPEATABLE_READ 禁止 禁止 允许
SERIALIZABLE 禁止 禁止 禁止
  MySQL InnoDB的默认隔离级别是 REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLEREAD事务隔离级别下,使用Next-Key Lock锁的算法,因此避免幻读的产生。这与其他数据库系统(如SQL Server数据库)是不同的。所以说,InnoDB存储引擎在默认的REPEATABLE READ的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别。
例如:开启事务,查询数据
mysql> set @@tx_isolation="repeatable-read";
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|        7 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|        7 |
+----------+
1 row in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|        8 |
+----------+
1 row in set (0.00 sec)


新增一条数据,上面的查询结果还是7条
mysql> insert into t1 values(null,'xxxxxxx');
Query OK, 1 row affected (0.09 sec)

Spring 事务相关知识

事务是逻辑处理原子性的保证手段,通过使用事务控制,可以极大的避免出现逻辑处理失败导致的脏数据等问题。
事务最重要的两个特性,是事务的传播级别和数据隔离级别。传播级别定义的是事务的控制范围,事务隔离级别定义的是事务在数据库读写方面的控制范围。

Spring在 TransactionDefinition 接口中定义了5个隔离级别:
1)ISOLATION_DEFAULT  这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.另外四个与JDBC的隔离级别相对应 
2)ISOLATION_READ_UNCOMMITTED  
3)ISOLATION_READ_COMMITTED 
4)ISOLATION_REPEATABLE_READ 
5)ISOLATION_SERIALIZABLE 
Spring在 TransactionDefinition 接口中定义了7个传播行为:
1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

Spring 给我们带来了事务传播行为,这确实是一个非常强大而又实用的功能。除此以外,也提供了一些小的附加功能,比如:
事务超时(Transaction Timeout):为了解决事务时间太长,消耗太多的资源,所以故意给事务设置一个最大时常,如果超过了,就回滚事务。
只读事务(Readonly Transaction):为了忽略那些不需要事务的方法,比如读取数据,这样可以有效地提高一些性能。

猜你喜欢

转载自blog.csdn.net/tlk20071/article/details/78806449
今日推荐