作者:一一哥
转账是生活中常见的操作,比如从A账户转账100元到B账号。站在用户的角度而言,这是一个逻辑上的单一操作,然而在数据库系统中,至少会分成两个步骤来完成:
在这个过程中可能会出现以下问题:
数据库事务(Database Transaction),简短的说就是一组对数据库的相关增删改查的操作,要么全部完成,要么全部不做,绝不允许只做其中的一部分操作。
一个事务由事务开始
(begin transaction)
和事务结束(end transaction)
之间执行的全体操作组成。
当一个事务执行过程中发生了异常、错误,则重新回到最先未开始执行的过程。
比如上面那个银行转账过程,假设A-100操作已经完成,但是在执行B+100操作时,系统发生位置错误,这时需要回到未执行该转账操作之前的状态,即A、B原来多少钱还是多少钱,一分不能少。
当一个事务执行过程没有发生任何异常、错误,这时我们要保存这个事务的修改。
比如上面的银行转账过程,假设A-100、B+100操作全部完成,没有出现任何异常、错误,这时需要保存事务执行状态修改(A减少了100元,B增加了100元),即事务提交。
Atomicity 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节.事务在执行过程中如果发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样.
Consistency 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏.
Isolation 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致.
Durability 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失.
当多个线程都开启事务同时操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。
在介绍数据库提供的各种隔离级别之前,我们先看看如果不考虑事务的隔离性,会发生的几种问题:
1️⃣. 脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据.
当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致.
例如:用户A向用户B转账1000元,对应SQL命令如下:
#(此时A通知B)
update account set money=money+1000 where name=’B’;
update account set money=money - 1000 where name=’A’;
当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转.
2️⃣. 不可重复读
不可重复读是指对于数据库中的某个数据,在一个事务范围内多次查询却返回了不同的数据值,这是由于在查询的间隔期间,被另一个事务修改并提交了.
例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读现象.
不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务已提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主.但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询可能会有不同,A和B就可能打起来了...
3️⃣. 虚读(幻读)
幻读是事务非独立执行时发生的一种现象.
例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给了数据库.而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读.
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据(比如数据的个数).
1️⃣. Serializable(串行化):最高的隔离级别,可避免脏读、不可重复读、幻读的发生;
2️⃣. Repeatable read(可重复读):默认的隔离级别,可避免脏读、不可重复读的发生;
3️⃣. Read committed(读已提交):可避免脏读的发生;
4️⃣. Read uncommitted(读未提交):最低的隔离级别,最低级别,任何情况都无法保证.
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低.**
像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read(可重复读)。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read(可重复读)。
而在Oracle数据库中,只支持Serializable(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed级别。
在MySQL数据库中查看当前事务的隔离级别:
select @@tx_isolation;
在MySQL数据库中设置事务的隔离级别:
set [glogal | session] transaction isolation level 隔离级别名称;
或者:
set tx_isolation=’隔离级别名称;’
记住:设置数据库的隔离级别一定要是在开启事务之前!
如果是使用JDBC对数据库的事务设置隔离级别的话,也应该是在调用Connection对象的
setAutoCommit(false)
方法之前,调用Connection
对象的setTransactionIsolation(level)
即可设置当前链接的隔离级别,至于参数level,可以使用Connection
对象的字段:
注意:
隔离级别的设置只对当前链接有效.对于使用MySQL命令窗口而言,一个窗口就相当于一个链接。当前窗口设置的隔离级别只对当前窗口中的事务有效;对于JDBC操作数据库来说,一个Connection
对象相当于一个链接,而对于Connection
对象设置的隔离级别只对该Connection
对象有效,与其他链接Connection
对象无关。
为了解决事务隔离性问题,引入锁的概念,只有拿到锁的事务才可对数据库进行读写操作\color{red}只有拿到锁的事务才可对数据库进行读写操作只有拿到锁的事务才可对数据库进行读写操作。
事务有两种锁,并且有相应的权限。
某个事务A拿到该锁时,事务A只能进行读操作,此时其他事务也可以拿到这把锁(共享)。
某个事务A拿到该锁时,事务A能进行读、写操作,此时其他事务不能拿到这把锁(排它)。
如果某个事务A拿到了读锁,其它事务可以拿到读锁(共享),但是无法获取写锁。
如果某个事务A拿到了写锁,其他事务既不能拿到写锁,也拿不到读锁!
所谓锁的粒度,就是锁的范围,比如如果锁的范围是一张表,则事务A获取写锁后,只能事务A进行读、写,其他事务全部要靠边站。
如果锁的粒度是事务A需要操作的某几行记录,其它记录如果其他事务拿到锁仍然可以读、写。
一般情况下,锁的粒度越小(锁的范围小),则并发问题解决越好(事务都是并发执行),但是效率越低,因为需要大量的资源来确保各个事务的锁的粒度没有交集、冲突。
锁的粒度越大(锁的范围大),则并发问题解决越差(其他事务都在等待),但是效率较高,因为不要资源来控制各个事务的锁粒度交集问题。
Spring通过配置事务的传播属性来严格控制事务行为。
比如在一个配置了事务的方法中调用了另一个方法,则另一个方法应该怎么运行,是新开启一个事务,还是和调用方法是一个事务,还是不开启事务?
Spring中一共有七种传播属性
PROPAGATION_REQUIRED(需要) 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务。
PROPAGATION_SUPPORTS (支持)表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行,如果不存在事务就不在事务中执行。
PROPAGATION_MANDATORY (强制必须)表示该当前方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。
PROPAGATION_REQUIRED_NEW(要求新事物) 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
PROPAGATION_NOT_SUPPORTED(不支持新事物) 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
PROPAGATION_NEVER (从不)表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常。
PROPAGATION_NESTED(嵌套)(spring) 表示如果当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。
注意:
各厂商对传播行为的支持是有所差异的。
我们的本地事务由资源管理器进行管理,以下是本地事务的基本执行过程.

事务的ACID
特性是通过InnoDB
引擎的日志和锁机制来保证的,我们知道InnoDB
是mysql
的一个存储引擎.事务的隔离性是通过数据库锁的机制实现的,持久性通过Redo log
(重做日志)来实现,原子性和一致性通过Undo log
来实现.UndoLog
的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为UndoLog
),然后进行数据的修改.如果出现了错误或者用户执行了ROLLBACK
语句,系统可以利用UndoLog
中的备份将数据恢复到事务开始之前的状态.与UndoLog
相反,RedoLog
记录的是新数据的备份,在事务提交前,只要将RedoLog
持久化即可,不需要将数据持久化.当系统崩溃时,虽然数据没有持久化,但是RedoLog
已经持久化,系统可以根据RedoLog
的内容,将所有数据恢复到最新的状态.
1️⃣. 编程式事务:
编程式事务需要你在代码中直接加入处理事务的代码逻辑,可能需要在代码中显式调用
beginTransaction()、commit()、rollback()
等事务管理相关的方法。我们可以使用
TransactionTemplate
或者直接使用底层的PlatformTransactionManager
,对于编程式事务管理,Spring推荐使用TransactionTemplate
.当系统需要明确的,细粒度的控制各个事务的边界时,应选择编程式事务.编程式事务侵入性比较强,但处理粒度更细。
2️⃣. 声明式事务:
声明式事务是建立在AOP之上的,它的做法是在a方法的外围添加注解或者直接在配置文件中定义a方法需要的事务处理,在Spring中会通过配置文件在a方法前后进行拦截,并添加事务。
其本质是对方法的前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要通过编程的方式管理事务,当系统对于事务的控制粒度较粗时,应该选择申明式事务。
无论你选择上述何种事务方式去实现事务控制,Spring都提供了基于门面设计模式的事务管理器供选择.