数据库事务概念
数据库事务必须同时满足 4 个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durabiliy),简称为ACID。下面是对每个特性的说明。
- 原子性:表示组成一个事务的多个数据库操作要么全部成功、要么全部失败。
- 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。如从A账户转账100元到B账户,不管操作成功与否,A和B的存款总额是不变的。
- 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰,数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。
- 持久性:一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证能够通过某种机制恢复数据。
其实这四个特性,原子性是最终目的。
数据并发的问题
一个数据库可能拥有多个访问客户端,这些客户端都可以并发方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题( 脏读、 不可重复读和 幻象读)以及2类数据更新问题( 第一类丢失更新和 第二类丢失更新)。下面,我们分别通过实例讲解引发问题的场景。
脏读(dirty read)
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的。来看取款事务和转账事务并发时引发的脏读场景:
在这个场景中,B希望取款500元而后又撤销了动作,而A往相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。
不可重复读(unrepeatable read)
不可重复读是指 A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致:
在同一事务中,T4时间点和T7时间点读取账户存款余额不一样。
幻象读(phantom read)
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中,举一个例子,假设银行系统在同一个事务中,两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致:
如果新增数据刚好满足事务的查询条件,这个新数据就进入了事务的视野,因而产生了两个统计不一致的情况。
幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其他已经提交事务的新增数据,而后者是指读到了已经提交事务的更改数据(更改或删除),为了避免这两种情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新增数据,则往往需要添加表级锁——将整个表锁定,防止新增数据(Oracle使用多版本数据的方式实现)。
第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:
A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:
上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。
四种隔离级别
尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。此外数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说完全是透明的。
ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别:
事务的隔离级别和数据库并发性是对立的,两者此增彼长。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。
Mysql的默认隔离级别时Repeatable Read,即可重复读。
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事务数据操作的代码:
03 | conn = DriverManager.getConnection(); |
04 | conn.setAutoCommit( false ); |
06 | conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); |
07 | Statement stmt = conn.createStatement(); |
09 | int rows = stmt.executeUpdate( "INSERT INTO t_topic ALUES(1,’tom’) " ); |
10 | rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 " + "WHERE user_id = 1" ); |
在JDBC 2.0中,事务最终只能有两个操作:提交和回滚。但是,有些应用可能需要对事务进行更多的控制,而不是简单地提交或回滚。JDBC 3.0(JDK 1.4及以后的版本)引入了一个全新的保存点特性,Savepoint 接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而并非像JDBC 2.0一样只回滚到开始事务的点,如下图所示:
下面的代码使用了保存点的功能,在发生特定问题时,回滚到指定的保存点,而非回滚整个事务:
01 | Statement stmt = conn.createStatement(); |
02 | int rows = stmt.executeUpdate( "INSERT INTO t_topic VALUES(1,’tom’)" ); |
04 | Savepoint svpt = conn.setSavepoint( "savePoint1" ); |
05 | rows = stmt.executeUpdate( "UPDATE t_user set topic_nums = topic_nums +1 " + "WHERE user_id = 1" ); |
并非所有数据库都支持保存点功能,用户可以通过DatabaseMetaData的supportsSavepoints()方法查看是否支持。
事务类型
数据库事务类型有本地事务和分布式事务:
- 本地事务:就是普通事务,能保证单台数据库上的操作的ACID,被限定在一台数据库上;
- 分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;
Java事务类型有JDBC事务和JTA事务:
- JDBC事务:就是数据库事务类型中的本地事务,通过Connection对象的控制来管理事务;
- JTA事务:JTA指Java事务API(Java Transaction API),是Java EE数据库事务规范, JTA只提供了事务管理接口,由应用程序服务器厂商(如WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务。
Java EE事务类型有本地事务和全局事务:
- 本地事务:使用JDBC编程实现事务;
- 全局事务:由应用程序服务器提供,使用JTA事务;
按是否通过编程实现事务有声明式事务和编程式事务;
- 声明式事务: 通过注解或XML配置文件指定事务信息;
- 编程式事务:通过编写代码实现事务。