JDBC学习笔记——事务、存储过程以及批量处理
1、事务
1.1、事务的基本概念和使用示例
数据库事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。
JDBC可以操作Connection的setAutoCommit()方法,给它false参数,提示数据库启动事务,在下达一连串的SQL命令后,自行调用Connection的commit()方法,提示数据库确认(Commit)操作。如果中间发生错误,则调用rollback(),提示数据库撤销(ROLLBACK)所有执行。同时,如果仅想要撤回某个SQL执行点,则可以设置存储点(SAVEPOINT)。一个示范的事务流程如下:
Connection conn = ...; Savepoint point = null; try { conn.setAutoCommit(false); Statement stmt = conn.createStatement(); stmt.executeUpdate("INSERT INTO ..."); ... point = conn.setSavepoint(); stmt.executeUpdate("INSERT INTO ..."); ... conn.commit(); } catch (SQLException e) { e.printStackTrace(); if (conn != null) { try { if (point == null) { conn.rollback(); } else { conn.rollback(point); conn.releaseSavepoint(point); } } catch (SQLException ex) { ex.printStackTrace(); } } } finally { ... if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) { ex.printStackTrace(); } } }
需要说明的是,JDBC操作事务的前提条件是数据库支持事务,如果数据库本身不支持事务,那么我们即使调用setAutoCommit(false)也无法启动事务。对于MYSQL来说,MyIsam数据库引擎不支持事务操作,InnoDB数据库引擎支持事务操作。
1.2、隔离级别
要理解隔离级别,首先要了解多个事务并行时,可能引发的数据不一致问题有哪些,常见的事务并行引发问题有以下几类:
更新遗失
更新遗失是指某个事务对字段进行更新的信息,因另一个事务的介入而遗失更新的效力,一个简单的示例如下:
- 事务A更新数据表字段为AAA;
- 事务B更新数据表字段为BBB;
- 事务A提交;
- 事务B提交。
这个序列就是典型的更新丢失,因为第三步所做的所有修改全部会丢失。如果要避免更新遗失问题,可以设置隔离级别为"read uncommitted",这样A事务已更新但未确认的数据,B事务仅可做读取操作,但不可做更新操作。这样上面的四个步骤就是不合法的,必须A事务完全提交,B事务才能做更新操作。
脏读
"read uncommitted"隔离级别保证了在A事务提交之前,B事务不能做更改操作,但是没有阻止B事务做读取操作,但是这个其实是有问题的:如果A事务更新字段为"AAA",B事务读取值为"AAA"并使用,然后A事务回滚事务,那么B事务读取的"AAA"就属于脏数据。如果要避免脏读问题,可以设置隔离级别为"Read Commited",也就是事务读取的数据必须是其他事务已经确认的数据。
不可重复读
不可重复度是指两次读取同一字段的数据不一致,例如:事务A读取字段为"AAA",事务B更新数据为"BBB",事务B提交,事务A读取字段为"BBB",事务A连续的两次读取,字段值不一样。要避免这种问题,可以设置数据库隔离级别为"Repeatable Read",对于这种隔离界别,事务A读取字段为"AAA"后,其他事务在事务A提交前只可读取该字段,不可更新该字段。
幻读
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的"全部数据行"。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入"一行新数据"。那么,以后就会发生操作第一个事务的用户发现表中还是有没有修改的数据行,就好象发生了幻觉一样。要解决幻读问题,必须设置隔离级别为Serializable,Serializable是数据库隔离级别的最高级别,串行化读,事务只能一个一个执行,避免了脏读、不可重复读、幻读,但是执行效率慢,需要谨慎使用。
可以使用以下命令查询mysql的隔离级别:
select @@global.tx_isolation,@@tx_isolation;
1.3、悲观锁、乐观锁
悲观锁是采用一种悲观的态度来对待事务并发问题,认为系统中的并发更新会非常频繁,并且事务失败了以后重来的开销很大,这样一来,我们就需要采用真正意义上的锁来进行实现。悲观锁的实现,往往依靠数据库提供的锁机制。悲观锁的基本思想就是每次一个事务读取某一条记录后,就会把这条记录锁住,这样其它的事务要想更新,必须等以前的事务提交或者回滚解除锁。
乐观锁,顾名思义就是保持一种乐观的态度,我们认为系统中的事务并发更新不会很频繁,即使冲突了也没事,大不了重新再来一次。它的基本思想就是每次提交一个事务更新时,我们先看看要修改的东西从上次读取以后有没有被其它事务修改过,如果修改过,那么更新就会失败。乐观锁的实现方式大多是基于数据版本 ( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提 交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
总的来说,悲观锁的机制依赖数据库的锁机制,较安全,而乐观锁机制通过应用程序控制,性能较好。
1.4、分布式事务
分布式事务处理涉及多个分布在不同地方的数据库,但对数据库的操作必须全部被提交或者回滚。只要任一数据库操作时失败,所有参与事务的数据库都需要回滚。
Open 组织定义的分布式事务处理模型X/Open DTP 模型包括应用程序( AP )、事务管理器( TM )、资源管理器( RM ,即数据库 )、通信资源管理器( CRM )四部分。而 XA 是 X/Open DTP 定义的事务管理器与数据库之间的接口规范(即接口函数),事务管理器用它来通知数据库事务的开始、结束以及提交、回滚等。
XA 接口规范使用两阶段提交协议来完成一个全局事务,保证同一事务中所有数据库同时成功或者回滚。
两阶段提交协议假设每个数据库点都存在一个write-ahead log,每一次的write请求都是先记log后才真正执行写入。
第一阶段为提交请求阶段(Commit-request phase):
1. 事务管理器给所有数据库发query to commit消息请求,然后开始等待回应;
2. 数据库如果可以提交属于自己的事务分支,则将自己在该事务分支中所做的操作固定记录下来(在undo log和redo log中各记一项);
3. 数据库都回应是否同意提交的应答。
第二阶段为提交阶段(Commit phase):
如果事务管理器收到的所有回应都是agreement,
1. 事务管理器记日志并给所有数据库发commit消息请求;
2. 各个数据库执行操作,释放所有该事务相关的锁和资源;
3. 各个数据库给事务管理器回复;
4.当收到所有回复,事务管理器结束当前事务
如果事务管理器收到的任一回应是abort,
1. 事务管理器记日志并给所有数据库发rollback消息请求;
2. 各个数据库执行undo操作,释放所有该事务相关的锁和资源;
3. 各个数据库给事务管理器回复;
4.当收到所有回复,事务管理器结束当前事务
两阶段提交协议的问题在于数据库在提交请求阶段应答后对很多资源处于锁定状态,要等到事务管理器收集齐所有数据库的应答后,才能发commit或者rollback消息结束这种锁定。锁定时间的长度是由最慢的一个数据库制约,如果数据库一直没有应答,所有其他库也需要无休止的锁并等待。并且,如果事务管理器出现故障,被锁定的资源将长时间处于锁定状态。无论是任一数据库或者事务管理器故障,其他数据库都需要永久锁定或者至少长时间锁定。并且,分布式系统中节点越多,存在缓慢网络或者故障节点的概率也就越大,资源被长时间锁定的概率指数上升。
两阶段提交协议的另一个问题是只要有任意一个数据库不可用都会导致事务失败,这导致事务更倾向于失败。对于多个副本的备份系统,很多时候我们希望部分副本点失效时系统仍然可用,使用该协议则不能实现。并且,分布式系统中节点越多,存在故障节点的概率也就越大,系统的可用性指数下降。
另外,如果数据库在第一阶段应答后到第二阶段正式提交前的某个阶段网络故障或者节点故障,该协议无法提交或回滚,数据不一致不能绝对避免。
2、存储过程
JDBC可以调用存储过程,要调用存储过程,首先我们应该创建存储过程:
//创建表,并插入数据 create table g(num int,value varchar(10)); insert into g values(1, '1'),(10, '10'),(60, '60'),(100, '100'); //创建存储过程 DELIMITER $ CREATE PROCEDURE p1(IN n int, OUT avg double, OUT min int) BEGIN select avg(num) from g where num > n INTO avg; select min(num) from g where num > n INTO min; END$ DELIMITER ;
JDBC调用存储过程的接口与增删改查的不同,JDBC调用存储过程应该使用CallableStatement,简单示例如下:
private static void ps() throws SQLException{ Connection conn = null; CallableStatement cs = null; try{ conn = JdbcUtils.getConnection(); cs = conn.prepareCall("call p1(?,?,?)"); cs.registerOutParameter(2, Types.DOUBLE);//设置out参数 cs.registerOutParameter(3, Types.INTEGER);//设置out参数 cs.setInt(1, 18);//设置in参数 cs.executeUpdate(); System.out.println(cs.getInt(2) + " " + cs.getInt(3)); } finally{ JdbcUtils.free(null, cs, conn); } }
3、批次更新
如果需要对对数据库进行大量数据更新,使用循环多次操作更新是比较浪费性能的,对于这种场景,我们可以使用addBatch()方法来收集SQL,并使用executeBatch()方法将收集的SQL批次更新,例如:
Statement stmt = conn.createStatement(); while(someCondition) { stmt.addBatch("INSERT INTO ..."); } stmt.executeBatch();