锁定老帖子 主题:我和JAVA数据库操作的那些事儿(3)
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2011-10-20
最后修改:2011-10-20
在前面的两篇文章中,第一篇主要是讲了在jdbc编程中容易碰到的几个问题,以及大致的解决方法。第二篇从代码上实现了第一篇的一些方法,并且对批处理进行了一些描述。在批处理的过程中,我并没有和事务的概念放在一起,因为我发现,这两个概念往往会引起混淆。因为jdbc的批处理是jdbc层面上的,而事务是数据库层面上的,如果写一个存储过程的话,这两者是一样的,即到了一批再commit,而在jdbc编程就是两个层面了。比如下面的代码: public void addEmployees(Connection conn, List<Employee> empList, int batchSize) throws SQLException { long bt = System.currentTimeMillis(); PreparedStatement stmt = null; try { // 设置conn自动提交为false,而由程序在需要的地方进行commit或者rollback conn.setAutoCommit(false); String sql = SqlParser.getInstance().getSql("Employee.insert"); stmt = conn.prepareStatement(sql); int count = 0; for (Employee emp : empList) { stmt.setInt(1, emp.getId()); stmt.setString(2, emp.getName()); stmt.setInt(3, emp.getDepartment().getId()); stmt.setString(4, emp.getDescription()); stmt.addBatch(); count++; if (count % batchSize == 0) { stmt.executeBatch(); // 注意,如果不加conn.commit()方法,即使我用stmt.executeBatch(),也不会真正提交到数据库 // conn.commit(); } } stmt.executeBatch(); // 虽然我在上面多次调用executeBatch(),但都没有真正提交到数据库,只有在conn.commit()之后才算真正提交 conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { long et = System.currentTimeMillis(); System.out.println(String.format("用时%dms", et - bt)); DBUtil.close(stmt); DBUtil.close(conn); } }
需要注意的是: 1. conn.setAutoCommit(false) 这里就是让Connection不自动commit,而由程序来调用。 2. 在我使用stmt.executeBatch()方法之后,如果不使用conn.commit()方法的话,数据并没有真正提交到数据库。
通过上面的代码,我们已经开始使用事务了,还记得上一篇中结束的时候,我提供了测试的速度,现在再来看一下 运行时间
5条:用时22740ms
10条:用时16109ms 50条:用时9314ms 100条:用时8723ms
是不是觉得有点问题?速度好像比较慢,这是怎么回事?原来我没有将conn.setAutoCommit(false)之前,每次stmt的操作都是新开一个事务的,1000条数据,开1000次事务,能不慢吗?现在换上新的代码,再来测试一下:
运行时间
5条:用时179ms
10条:用时199ms 50条:用时269ms 100条:用时193ms
是不是快了许多呢。
言归正传,说到事务,我觉得有必要把事务的特性说一下,这是很基本的东西,但是很多人包括我自己有时也不太容易讲清楚。 事务的特性
事务的特性-ACID:
原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability) 原子性:事务中的操作,要么全做成,要么都不做 一致性:事务在完成时,必须使所有的数据都保持一致状态,而且在相关数据中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有 的内部数据结构都应该是正确的。 隔离性:多个并发事务之间不能相互干扰,并发不影响事务的执行 持久性:一旦事务成功完成(Commit),它对数据库的更新应该是持久的。即使在写入磁盘之前,系统发生故障,在下次启动之后,也应保障数据更新的有效 说到原子性和持久性,这两个比较好理解,对于一致性和隔离性,有必要好好想一想。
对于一致性,我的理解有两个层面: 1. 完整性约束,比如员工表和部门表,员工所属的部门必须是部门表的数据 2. 读一致性,比如读取员工表,本来是可以读1000员工,但读的过程可能有一点长,在此过程中,另外一个事务删除了一些员工数据,但读的事务应该仍然能读到1000名员工。
对于隔离性,我觉得多个事务之间不能相互干扰,应该是事务隔离机制里最严格的Serialiable(序列化)级别,关于事务的隔离级别,我写了一篇《图说事务隔离级别 》,可以参考。
对于事务的特性的讨论暂时到此,现在来看一下,使用jdbc如何进行事务?接着我之前的例子,即删除部门的时候,会把员工所属于的部门设置为空,那就是删除部门有两个操作,一是删除部门表数据,二是更新员工表数据,这两个操作应该在一个事务里。代码如下: public void deleteDepartmentTrans(Connection conn, Department dept) throws SQLException { try { conn.setAutoCommit(false); this.deleteDepartment(conn, dept); this.updateEmpDeptNull(conn, dept); conn.commit(); } catch (SQLException e) { conn.rollback(); } } private void deleteDepartment(Connection conn, Department dept) throws SQLException { PreparedStatement stmt = null; try { String sql = SqlParser.getInstance().getSql("Department.delete"); stmt = conn.prepareStatement(sql); stmt.setInt(1, dept.getId()); stmt.execute(); } finally { DBUtil.close(stmt); DBUtil.close(conn); } } private void updateEmpDeptNull(Connection conn, Department dept) throws SQLException { PreparedStatement stmt = null; try { String sql = SqlParser.getInstance().getSql( "Employee.departmentisnull"); stmt = conn.prepareStatement(sql); stmt.setInt(1, dept.getId()); stmt.execute(); } finally { DBUtil.close(stmt); DBUtil.close(conn); } }
简单说来: 1. conn.setAutoCommit(false),由程序自己设置什么时候commit,什么时候rollback 2. conn.commit(),真正提交到数据库 3. conn.rollback(),在需要的时候进行rollback,即恢复到上一次commit()的状态
对于jdbc3.0来说,新增加了Savepoint,我们来看一下之前批量insert员工表的方法: public void addEmployees(Connection conn, List<Employee> empList, int batchSize) throws SQLException { long bt = System.currentTimeMillis(); PreparedStatement stmt = null; // 设置conn自动提交为false,而由程序在需要的地方进行commit或者rollback conn.setAutoCommit(false); try { String sql = SqlParser.getInstance().getSql("Employee.insert"); stmt = conn.prepareStatement(sql); int count = 0; for (Employee emp : empList) { stmt.setInt(1, emp.getId()); stmt.setString(2, emp.getName()); stmt.setInt(3, emp.getDepartment().getId()); stmt.setString(4, emp.getDescription()); stmt.addBatch(); count++; if (count % batchSize == 0) { stmt.executeBatch(); // 注意,如果不加conn.commit()方法,即使我用stmt.executeBatch(),也不会真正提交到数据库 conn.commit(); } } stmt.executeBatch(); // 虽然我在上面多次调用executeBatch(),但都没有真正提交到数据库,只有在conn.commit()之后才算真正提交 conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { long et = System.currentTimeMillis(); System.out.println(String.format("用时%dms", et - bt)); DBUtil.close(stmt); DBUtil.close(conn); } } 假如我有1000个员工要写到员工表,每100个一批commit一次,假设在第900多个的时候发生了异常,于是进行rollback,但这次的 rollback只是回滚最后一次commit之前的状态,即之前900条员工已经写到数据库了,那么我想要实现要么1000都写成功,要么一条都不成 功,就需要Savepoint帮忙了。 public void addEmployees(Connection conn, List<Employee> empList, int batchSize) throws SQLException { long bt = System.currentTimeMillis(); PreparedStatement stmt = null; // 设置conn自动提交为false,而由程序在需要的地方进行commit或者rollback conn.setAutoCommit(false); //在这里设置一个Savepoint,以便在rollback的时候回滚到这时的状态 Savepoint sp = conn.setSavepoint(); try { String sql = SqlParser.getInstance().getSql("Employee.insert"); stmt = conn.prepareStatement(sql); int count = 0; for (Employee emp : empList) { stmt.setInt(1, emp.getId()); stmt.setString(2, emp.getName()); stmt.setInt(3, emp.getDepartment().getId()); stmt.setString(4, emp.getDescription()); stmt.addBatch(); count++; if (count % batchSize == 0) { stmt.executeBatch(); // 注意,如果不加conn.commit()方法,即使我用stmt.executeBatch(),也不会真正提交到数据库 conn.commit(); } } stmt.executeBatch(); // 虽然我在上面多次调用executeBatch(),但都没有真正提交到数据库,只有在conn.commit()之后才算真正提交 conn.commit(); } catch (SQLException e) { //回滚到Savepoint那个点 conn.rollback(sp); } finally { long et = System.currentTimeMillis(); System.out.println(String.format("用时%dms", et - bt)); DBUtil.close(stmt); DBUtil.close(conn); } }
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 1494 次