事务(Transaction):是并发控制的单元,是用户定义的一系列操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。通过事务,可以将逻辑相关的一组操作绑定在一起,以便服务器保持数据的完整性。事务通常是以begin transaction开始,以commit或rollback结束。commit表示提交,即提交事务的所有操作。具体地说就是将事务中所有的数据的更新写回到磁盘上的物理数据库中去,事务正常结束。rollback表示回滚,即在事务运行的过程中发生了某种故障,事务不能继续进行,系统将事务中的数据库的所有已完成的操作全部撤销,滚回到事务开始的状态。
事务的基本特性:
- 原子性(atomicity):组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分。
- 一致性(consistency):在事务处理执行前后,数据库是一致的(数据库数据完整约束)。
- 隔离性(isolcation):一个事务处理对另一个事务处理的影响。
- 持续性(durability):事务处理的效果能够被永久保存下来。
- connection.setAutoCommit(false);// 打开事务
- connection.commit(); // 提交事务
- connection.rollback();// 回滚事务
以现实生活中银行转账的为例子,从A账户转账10元到B账户,如果B账户的余额超过300就不转账。查询数据库初始数据如下:
id name birthday money
1 zhangs 1985-01-01 400
2 lisi 1986-01-01 500
3 wangwu 1987-01-01 300
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
示例代码:
1 package com.xxyh.jdbc; 2 import java.sql.Connection; 3 import java.sql.ResultSet; 4 import java.sql.SQLException; 5 import java.sql.Statement; 6 public class TxTest { 7 public static void main(String[] args) { 8 Connection conn = null; 9 Statement stmt = null; 10 ResultSet rs = null; 11 try { 12 conn = JdbcUtils.getConnection(); 13 stmt = conn.createStatement(); 14 String sql = "update user set money=money-10 where id=1"; 15 stmt.executeUpdate(sql); 16 17 sql = "select money from user where id=2"; 18 rs = stmt.executeQuery(sql); 19 float money = 0.0f; 20 if (rs.next()) { 21 money = rs.getFloat("money"); 22 } 23 if (money > 300) 24 throw new RuntimeException("已经超过最大值!"); 25 sql = "update user set money=money+10 where id=2"; 26 stmt.executeUpdate(sql); 27 } catch (SQLException e) { 28 // TODO Auto-generated catch block 29 e.printStackTrace(); 30 } finally{ 31 JdbcUtils.close(rs, stmt, conn); 32 } 33 } 34 }
【运行结果】:
Exception in thread "main" java.lang.RuntimeException: 已经超过最大值!
at com.xxyh.jdbc.TxTest.main(TxTest.java:27)
再次查询数据库:
id name birthday money
1 zhangs 1985-01-01 390
2 lisi 1986-01-01 500
3 wangwu 1987-01-01 300
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
发现A账号的余额减少了10元,而B账户的余额却并没有增加。
通过添加事务,防止事务并发可能引起的为题,代码如下:
1 package com.xxyh.jdbc; 2 import java.sql.Connection; 3 import java.sql.ResultSet; 4 import java.sql.SQLException; 5 import java.sql.Statement; 6 public class TxTest { 7 public static void main(String[] args) throws SQLException { 8 Connection conn = null; 9 Statement stmt = null; 10 ResultSet rs = null; 11 try { 12 conn = JdbcUtils.getConnection(); 13 conn.setAutoCommit(false);// 打开事务,关闭自动提交 14 stmt = conn.createStatement(); 15 String sql = "update user set money=money-10 where id=1"; 16 stmt.executeUpdate(sql); 17 18 sql = "select money from user where id=2"; 19 rs = stmt.executeQuery(sql); 20 float money = 0.0f; 21 if (rs.next()) { 22 money = rs.getFloat("money"); 23 } 24 if (money > 300) 25 throw new RuntimeException("已经超过最大值!"); 26 sql = "update user set money=money+10 where id=2"; 27 stmt.executeUpdate(sql); 28 conn.commit(); 29 } catch (SQLException e) { 30 if (conn != null) 31 conn.rollback(); 32 throw e; 33 } finally{ 34 JdbcUtils.close(rs, stmt, conn); 35 } 36 } 37 }
【运行结果】:
Exception in thread "main" java.lang.RuntimeException: 已经超过最大值!
at com.xxyh.jdbc.TxTest.main(TxTest.java:28)
再次查询数据库:
id name birthday money
1 zhangs 1985-01-01 400
2 lisi 1986-01-01 500
3 wangwu 1987-01-01 300
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
如果将B账号的余额最大设置为600,再次运行以上代码,【运行结果】:
id name birthday money
1 zhangs 1985-01-01 390
2 lisi 1986-01-01 510
3 wangwu 1987-01-01 300
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
JDBC中的事务保存点,即事务发生回滚的时候,回滚到保存点即止,事务开始到保存点之间的部分不回滚。
保存点在事务中的应用:
初始状体数据库的数据状况:
id name birthday money
1 zhangs 1985-01-01 370
2 lisi 1986-01-01 520
3 wangwu 1987-01-01 310
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
1 package com.xxyh.jdbc; 2 import java.sql.Connection; 3 import java.sql.ResultSet; 4 import java.sql.SQLException; 5 import java.sql.Savepoint; 6 import java.sql.Statement; 7 public class SavePointTest { 8 public static void main(String[] args) throws SQLException { 9 Connection conn = null; 10 Statement stmt = null; 11 ResultSet rs = null; 12 Savepoint sp = null; 13 try { 14 conn = JdbcUtils.getConnection(); 15 conn.setAutoCommit(false);// 打开事务,关闭自动提交 16 stmt = conn.createStatement(); 17 String sql = "update user set money=money-10 where id=1"; 18 stmt.executeUpdate(sql); 19 20 sp = conn.setSavepoint(); 21 22 // 转账的同时更新id=3的账户余额 23 sql = "update user set money=money+10 where id=3"; 24 stmt.executeUpdate(sql); 25 26 sql = "select money from user where id=2"; 27 rs = stmt.executeQuery(sql); 28 float money = 0.0f; 29 if (rs.next()) { 30 money = rs.getFloat("money"); 31 } 32 if (money > 200) 33 throw new RuntimeException("已经超过最大值!"); 34 sql = "update user set money=money+10 where id=2"; 35 stmt.executeUpdate(sql); 36 conn.commit(); 37 } catch(RuntimeException e) { 38 if (conn != null && sp != null) { 39 conn.rollback(sp);// 回滚到保存点 40 conn.commit(); 41 } 42 throw e; 43 } catch (SQLException e) { 44 if (conn != null) 45 conn.rollback(); 46 throw e; 47 } finally{ 48 JdbcUtils.close(rs, stmt, conn); 49 } 50 } 51 }
【运行结果】:
Exception in thread "main" java.lang.RuntimeException: 已经超过最大值!
at com.xxyh.jdbc.SavePointTest.main(SavePointTest.java:35)
运行以上程序后,查询数据库:
id name birthday money
1 zhangs 1985-01-01 360
2 lisi 1986-01-01 530
3 wangwu 1987-01-01 320
4 qianqi 2015-03-15 500
5 qianqi 2015-03-15 500
6 daoname1 2015-03-17 9000000
只有1的余额减少了,而2和3的余额均没有改变,因为事务只回滚到了保存点的位置并没有会滚到事务的开始位置。
事务并发可能导致的问题:
- 丢失更新:撤销一个事务时,把其他事务已提交的更新数据覆盖(A和B事务并行执行,A事务执行更新后,提交;B事务在A事务更新后,B事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了)。
- 脏读:一个事务读到另一个事物未提交的更新数据(A和B事务并发执行,B事务执行更新后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据)。
- 不可重复读:一个事务读到另一个事务已提交的更新数据(A和B并发执行,A事务查询数据,然后B事务更新该数据,A再次查询该数据时,发现数据变化了)。
- 覆盖更新:这是不可重复读的一个特例。一个事务覆盖另一个事务已提交的数据(即A事务更新数据,然后B事务更新该数据,A事务查询发现自己更新的数据发生了变化)。
- 幻读:一个事务读到另一个事务新插入的数据(A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了)。