概述
在使用Spring开发应用时,Spring的事务管理可能是被使用最多、应用最广的功能。 Spring不但提供了和底层事务源无关的事务抽象,还提供了声明性事务的功能,可以让开发者从事务代码中解放出来。
数据库事务的概念
“一荣俱荣,一损俱损”。
数据库事务必须同时满足4个特性 (ACID):
- 原子性Atomic
- 一致性Consistency
- 隔离性Isolation
- 持久性Durability
原子性
表示组成一个事务的多个数据库操作时一个不可分割的原子单元,只有所有的操作都成功,整个事务才提交。 事务中的任何一个数据库操作失败,已经执行的任何操作都必须回滚,让数据库返回到初始状态。
一致性
事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。 比如 A转给B100元,不管操作成功与否,A账户和B账户的存款总额是不变的。
隔离性
在并发数据操作时,不同的事务拥有各自的数据空间,他们的操作不会对对方产生干扰, 准确的的说,并非要求做到完全无干扰,数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度。 隔离级别越高,数据一致性越好,但并发性越弱。
持久性
一旦事务提供成功后,事务中所有的数据操作都必须被持久化到数据库中。 即使在提交事务后,数据库马上崩溃,再重启数据时,也必须保证能通过某种机制恢复数据。
在这些事务特性中,数据的“一致性”是最终目标, 其他特性都是为了达到这个目标而采取的措施、要求或者手段。
数据库管理系统一般采取redolog来保证原子行、一致性和持久性。
数据库管理系统采用数据库锁机制保证事物的隔离性,当多个事务视图对相同的数据机型操作时,只有持有锁的事务才能操作数据,直到前面一个一个事务完成后,后面的事务才有机会对数据进行操作。 Oracle还使用了数据版本你的机制,在回滚段为数据的每一个变化保存一个版本,数据的更改不影响数据的读取。
数据并发的问题
一个数据库同时拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。 数据库中相同的数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。
这些问题可以归结为5类,包括3类数据读问题(脏读、不可重复度、幻象读)和2类数据更新问题(第一类丢失更新和第二类丢失更新)
脏读dirty read
A 事务读取了B事务尚未提交的更改数据,并在这个数据的基础上进行操作。如果恰巧B事务回滚,那么A事务独到的数据根本是不被承认的。
时间 | 转账事务A | 取款事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户为1000元 | |
T4 | 取出500,更改余额为500 |
T5 | 查询账户余额为500(脏读) | |
T6 | 撤销事务,余额恢复为1000 | |
T7 | 汇入100,余额更改为600 | |
T8 | 提交事务 |
在这个场景中,B希望取款500,而后又撤销了动作,而A相同的装回转入100,就因为A事务读取了B事务尚未提交的数据,因而造成B账户白白丢失了500元。
在Oracle数据库中,不会发生脏读的情况。
不可重复读unrepeatable read
不可重复读是指A事务读取了B事务已经提交的更改数据。
时间 | 取款事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户为1000元 |
T4 | 查询账户为1000元 | |
T5 | 取出100,将余额更改为900 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900(和T4读取的不一致) |
幻象读 phantom read
A事务读取了B事务提交的新增数据,这时A事务将出现幻象读的问题。 幻象读一般发生在计算统计数据的事务中。
举个例子:假设银行系统在同一个事务中两次统计存款账户的总金额,再凉菜统计过程中,刚好新增了一个存款账户,并存入100元。 这时,两次统计的金额将不一致。
时间 | 统计金额事务A | 转账事务B |
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款数为1000元 |
T4 | 新增一个存款账户,存入100 | |
T5 | 提交事务 | |
T6 | 再此统计存款总数为1100(幻象读) |
如果新增数据刚好满足事务的查询条件,那么这个新数据就进入了事务的视野,因此产生了两次统计结果不一致的情况。
幻象读和不可重复度的区别
幻象读是指读到了其他已经提交的新增数据。
不可重复读是指读到了已经提交事务的更改数据(更改或者删除)。
为了避免这两种情况,采取的策略是不同的:
为了防止读到更改数据,只需要对操作的数据添加行级锁,组织操作中的数据发生变化。
而为了防止读到新增数据,这往往需要添加表级锁,将整张表锁定,防止新增数据,(Oracle使用多版本数据的方式实现)
第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。
A事务再撤销时,“不小心”将B事务已经转入账户的金额抹去了。
第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失
由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行损失了100, 相反,如果转转事务先提交,那么用户将损失100元。
数据库锁机制
数据库通过锁机制解决并发访问的问题。 不同的数据库实现细节上存在差别,但是原理基本是一致的。
锁的分类(oracle)
一、按操作划分,可分为DML锁、DDL锁
二、按锁的粒度划分,可分为表级锁、行级锁、页级锁(mysql)
三、按锁级别划分,可分为共享锁、排他锁
四、按加锁方式划分,可分为自动锁、显示锁
五、按使用方式划分,可分为乐观锁、悲观锁
为了更改数据,数据库必须在进行更改的行上施加行独占锁定,insert、update、delete、select for update语句都会隐士采用必要的行锁定。
下面介绍oracle常用的5中锁定
1、行共享锁定:一般通过select for update 语句隐式获得。行共享锁定并不防止对数据行进行更改的操作,但是可以防止其他会话获取独占性数据表锁定。允许进行多个并发的行共享和行独占性锁定,还允许进行数据表的共享或者采用共享行独占锁定。
2、行独占锁定:通过一条insert、update、delete语句隐士获取,或者通过一条LOCK TABLE ROW EXCLUSIVE MODE语句显示获取。这个锁可以防止其他会话获取一个共享锁定,共享行独占锁定或独占锁定。
3、表共享锁定:通过LOCK TABLE IN SHARE MODE语句显示获得,这种锁定可以防止其他会话获取行独占锁定(insert,update,delete),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定,该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回溯该事务并释放对该表的锁定之前不能更改这个被锁定的表。
4、表共享行独占锁定:通过 LOCK TABLE IN SHARE ROW EXCLUSIVE MODE语句显示获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定,它允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行select for update操作,但如果B会话试图更新选择的行,则需要等待。
5、表独占:通过LOCK TABLE IN EXCLUSIVE MODE显示获得。这个锁定防止其他会话对该表的任何锁定。
事务隔离级别
因为直接使用数据的锁比较麻烦,用户可以设置事务的隔离级别来实现自动锁机制。通过设置事务的隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源加上适合的锁。
JDBC对事务的支持
并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别,用户可以通过Connection的getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。
Connection默认情况下是自动提交的,也即每条执行的SQL都对应一个事务,为了能够将多条SQL当成一个事务执行,必须先通过Connection的setAutoCommit(false)阻止Connection自动提交,并可通过Connection的setTransactionIsolation()设置事务的隔离级别,Connection中定义了对应SQL 92标准4个事务隔离级别的常量。通过Connection的commit()提交事务,通过Connection的rollback()回滚事务。
下面是典型的JDBC事务数据操作的代码:
Connection conn ; try{ conn = DriverManager.getConnection();//①获取数据连接 conn.setAutoCommit(false); //②关闭自动提交的机制 //③设置事务隔离级别 conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); Statement stmt = conn.createStatement(); int rows = stmt.executeUpdate( "INSERT INTO t_topic ALUES(1,’tom’) " ); rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+ "WHERE user_id = 1"); conn.commit();//④提交事务 }catch(Exception e){ … conn.rollback();//⑤回滚事务 }finally{ … }
在JDBC2.0中,事务只有两个操作: 提交或者回滚。
在JDBC3.0(Java1.4以及以后的版本)引入了保存点特性。 JDBC定义了SavePoint接口,提供在一个更细粒度的事务控制机制。当设置了一个保存点后,可以rollback到该保存点处的状态,而不是rollback整个事务。Connection接口的setSavepoint和releaseSavepoint方法可以设置和释放保存点
Statement stmt = conn.createStatement(); int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’)"); Savepoint svpt = conn.setSavepoint("savePoint1");//①设置一个保存点 rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 "+ "WHERE user_id = 1"); … //②回滚到①处的savePoint1,①之前的SQL操作,在整个事务提交后依然提交, //但①到②之间的SQL操作被撤销了 conn.rollback(svpt); … conn.commit();//③提交事务
并非所有数据库都支持保存点功能,用户可以通过DatabaseMetaData的supportsSavepoints()方法查看是否支持。