数据库事务必须同时满足 4 个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durabiliy),简称为ACID。下面是对每个特性的说明。?
其实这四个特性,原子性是最终目的。
一个数据库可能拥有多个访问客户端,这些客户端都可以并发方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题(?脏读、?不可重复读和?幻象读)以及2类数据更新问题(?第一类丢失更新和?第二类丢失更新)。下面,我们分别通过实例讲解引发问题的场景。??
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的。来看取款事务和转账事务并发时引发的脏读场景:??
?
在这个场景中,B希望取款500元而后又撤销了动作,而A往相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。??
不可重复读是指?A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致:??
在同一事务中,T4时间点和T7时间点读取账户存款余额不一样。?
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,即可重复读。
并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别,用户可以通过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事务数据操作的代码:??
monospace !important; font-size: 10pt !important; display: block !important;">01
class="plain">Connection conn ;?
02
try
{?
03
???
conn = DriverManager.getConnection();
//①获取数据连接?
04
???
conn.setAutoCommit(
false
);?
//②关闭自动提交的机制?
05
???
//③设置事务隔离级别?????
06
???
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
07
???
Statement stmt = conn.createStatement();??
08
??????
?
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"
);??
11
???????
?
12
???
conn.commit();
//④提交事务?
13
}
catch
(Exception e){?
14
?????
…?
15
?????
conn.rollback();
//⑤回滚事务?
16
}
finally
{?
17
???
…?
18
}
在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’)"
);?
03
??
?
04
Savepoint svpt = conn.setSavepoint(
"savePoint1"
);
//①设置一个保存点?
05
rows = stmt.executeUpdate(?
"UPDATE t_user set topic_nums = topic_nums +1 "
+??
"WHERE user_id = 1"
);??
06
…?
07
//②回滚到①处的savePoint1,①之前的SQL操作,在整个事务提交后依然提交,?
08
//但①到②之间的SQL操作被撤销了?
09
conn.rollback(svpt);??
10
…?
11
conn.commit();
//③提交事务
并非所有数据库都支持保存点功能,用户可以通过DatabaseMetaData的supportsSavepoints()方法查看是否支持。?
数据库事务类型有本地事务和分布式事务:
Java事务类型有JDBC事务和JTA事务:
Java EE事务类型有本地事务和全局事务:
按是否通过编程实现事务有声明式事务和编程式事务;?