事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务一般由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。那么,在平时的应用中,为什么要使用事务呢?这里笔者想通过一个简单的例子来说明事务的重要性。支付宝是生活中经常使用的转账手段,假如账户A要通过支付宝将自己账户上的100元转到B账户,那么对于A账户来说余额就要减去100元,然后在B账户里增加100元,此时算是转账成功。假如就在A账户减去100元时,不巧出现了网络故障,B账户里还未来得及增加100元,那么整个转账业务就会出现问题。所以,为了保证业务的完整性,就需要通过事务来控制,将A减少100元和B增加100元放入同一个事务中,则该事务要么执行成功,要么执行失败后全部撤销,以此来保证数据的安全。
事务的四个特性包括原子性(atomicity),一致性(consistency),隔离性(isolation)和持久性(durability)。笔者将通过上文的例子来帮助读者理解事务的这四个特性。1.原子性:事务是数据库的逻辑工作单位,不可再分。当我们把“A减少100和B增加100”加入一个事务时,这个将会成为数据库工作的最小单位,对于A和B数据的修改,要么全部成功,要么全部失败,不会出现某一个成功另一个失败的情况。2.一致性:在事务处理执行前后,数据库是一致的(数据库数据完整性约束)。假设A原来有100元,B原来有0元,那么最初A账户和B账户一共有100元;当事务结束时,即转账成功后,最终A账户和B账户一共有100元,与最初的值保持不变,是数据达到一致。3.隔离性:每个事务都是独立的,一个事务的执行不能被其他事务所影响。例如A给B转账100元,C给A转账100元,那么“A减少100元,B增加100元”与“C减少100元,A增加100元”属于两个不同的事务,并且两个事务相对独立。4.持久性:事务处理的结果能够被永久保存在数据库中。
在JDBC中处理事务,都是通过Connection完成的。同一事务中所有的操作,都在使用同一个Connection对象。JDBC事务默认是开启的,并且是默认提交。下面是事务在JAVA中的最基本操作: connection.setAutoCommit(boolean);//设置是否为自动提交事务,如果true(默认值为true)表示自动提交,如果设置为false,需要手动提交事务。 connection.commit();//提交事务。 connection.rollback();//回滚事务。下面通过示例代码展示一下:
package cn.itcast.jdbc; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Savepoint; import java.sql.Statement; /** * 事务测试 */ public class test { public static void main(String[] args) throws SQLException { testTransaction(); } static void testTransaction() throws SQLException { Connection conn = null; Statement st = null; ResultSet rs = null; Savepoint sp = null; try { conn = JdbcUtils.getConnection(); //将事务设置成手动提交 conn.setAutoCommit(false); st = conn.createStatement(); //id为1的人的Money减100 String sql = "update user set money=money-100 where id=1"; st.executeUpdate(sql); //设置回滚点(savepoint) sp = conn.setSavepoint(); //id为2的人的Money减100 sql = "update user set money=money-100 where id=2"; st.executeUpdate(sql); sql = "select money from user where id=2"; rs = st.executeQuery(sql); float money = 0.0f; if (rs.next()) { money = rs.getFloat("money"); } if (money > 300){ throw new RuntimeException("已经超过最大值!"); } //id为2的人的Money加100 sql = "update user set money=money+100 where id=2"; st.executeUpdate(sql); //提交事务 conn.commit(); } catch (RuntimeException e) { if (conn != null && sp != null) { //回滚事务,注意里面的参数sp即为我们上面设置的savePoint,如果回滚的话只能回滚到savePoint以下的部分 //上面的部分不会得到回滚 conn.rollback(sp); conn.commit(); } throw e; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { //释放资源 JdbcUtils.free(rs, st, conn); } } }
为了应对多线程并发读取数据时出现的问题,事务有了“隔离级别”特性,多线程并发读取数据一般会引发如下三个问题:
1表示有,0表示无
隔离级别脏读不可重复读幻读读未提交(Read uncommitted)111读已提交(Readcommitted)011可重复读(Repeatableread)001可串行化(Serializable)000
隔离级别由高到低排列:可串行化>可重复读>读已提交>读未提交。通常情况下,数据库都有自己的默认隔离级别,我们使用spring框架可以指定隔离级别,但是如果指定了数据库不支持的隔离级别,数据库就会使用自己默认的。在Oracle数据库中,默认隔离级别是Read committed,而另一个常用数据库MySQL中,默认隔离级别是Repeatable read。下面,我们用mysql的例子说明各个隔离级别的情况:开启两个命令行客户端分别为A,B;不断改变A的隔离级别,在B端修改数据。实际步骤同序号。
1.读未提交(最低的隔离级别):
值得一提的是,如果在客户端A中接着执行update num= num + 1 where id = 1,num没有变成1+1=2,而是步骤(2)中更新过后的num=10来算的,所以是10 + 1 = 11,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
4.串行化
mysql中事务隔离级别为serializable时会锁表,若一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
事务控制是构建J2EE应用不可缺少的一部分,合理选择应用何种事务对整个应用系统来说至关重要。一般说来,在单个JDBC 连接连接的情况下可以选择JDBC事务,在跨多个连接或者数据库情况下,需要选择使用JTA事务,如果用到了EJB,则可以考虑使用EJB容器事务,有兴趣的朋友可以关注一下。对于隔离级别来说,读未提交、读已提交和可重复读这三种隔离级别隔离的是行数据,他们的不同只是对应读、写之间的锁定关系不同而已,读未提交,事务进行写操作时并没有锁定禁止读的动作;读已提交在进行事务在进行写操作时锁定了行数据,禁止在写期间读数据;而可重复读则是在读期间禁止数据的写;串行化即锁定整个表的读写。希望读者能够合理选择并使用它们。