jdbc 事务jdbc之事务篇

jdbc 事务jdbc之事务篇
*
事务:一组作为一个原子整体的操作,ALL OR NOTHING语义:要么全部,要么一个都不。就是说这组操作中的任何一个操作失败,代表这组操作失败,仅当所有操作都成功时,才算这个“事务”成功。当事务失败时,其影响应该撤销/回滚。当事务成功时其影响应该持久/提交/保存。用电路图形象示意:一个串连图,任何一个节点断路,两端都不会“通”。关于事务的ACID属性在很多资料中都有介绍这里不再涉及。

一个操作是否应归为某个事务,这完全是根据应用/需求来决定的,理论上你可以把任何数目操作,任何操作归为一个事务中。并且当你认为事务也是一个操作时,就出现了“事务的嵌套”,就是说一个事务中可能含有其他事务!就像电路图:复杂电路图可由简单电路图通过并联或串连的形式组合而成。再服务器上,常常并行运行多个事务(我们可用线程来模拟)。关于并行事务处理这里不讨论它(如何串行化事务,并发执行事务等都是较复杂的问题)。

在程序中出现的事务一般都比较隐晦,什么操作归为一个事务由业务逻辑决定,但我们在实现事务时可能有多种方式。这由我们决定,我把业务性事务称为应用事务(仅个人称呼),我们知道程序在运行时使用内存来模拟现实的“微世界”的,一旦断电或程序结束一切都化为泡影,你将又回到原点,为使你的操作确实产生影响,那么相关状态的更改应该持久化,就是说要把程序的(全部或部分)某个瞬时状态保存到持久性介质上。所以我们要进行IO操作,这里就是将其持久到数据库。
*
**
这里来看看如何实现一个应用事务,
现在认为整个系统是一个对象(为了简化讨论),这个对象内部可能是个很复杂的对象图,任何对象上发生的操作都会改变这个巨对象的状态(对象的状态由其内部所有子对象的状态组合来表示),我们认为某些操作应该归为一个事务,那么就需要持久由这些操作引起的状态变化,即应用事务的影响要保存到数据库去!。
事务都具有一个边界:即什么操作代表事务的开始,什么操作代表事务的结束,当然在应用程序中没有这样的边界,但是数据库中却是存在这样的东东:默认情况下数据库中的更改是永久性的:就是说操作不存在回滚,每一次写数据的操作都会“提交”到数据库。这可认为每个dml操作都是一个事务,但我们可以通过编程来改变这种情况,比如mysql中:set autocommit=0; 就表示关闭自动提交,由我们自己来控制提交和回滚时机。jdbc是封装各种数据库差异的API所有对事务控制的操作在java中都是:connection.setAutoCommit(false);//这在任何数据库系统中都管用。
这里有两个事务我们需要区分:一个是应用事务,一个是数据库事务(数据库事务是一组sql操作)。我们就是要将应用事务的结果通过数据库事务来表现出来,换句话说就是应用事务需要依赖数据库事务API。
迄今我所知道的事务实现可以是这样的:
1.当应用事务开始时即刻开启数据库事务,当应用事务回滚时即刻回滚数据库事务,当应用事务提交时即刻提交数据库事务,即二者总是如影随形。
2.应用事务独立完成,仅当应用事务提交时再完成数据库事务的开始和提交。我在以下的示例中会给出这种实现思路的。
**
***
jdbc中控制事务的操作:
Connection接口的:setAutoCommit(boolean autoCommit);commit();和rollback()总共就三个事务控制的接口操作。当连接第一次获取时,将其自动提交模式设定为假表示我们自己控制事务,每一个事务的提交或回滚是下一次事务的开始!!
当一个事务特别大时我们可能要进行细粒度的控制,不希望是ALL或Nothing,可以步步为营,这样数据库系统现在都引入了保存点的概念,就像游戏通关中的保存点,允许我们选择性的回滚到某个最近或指定的保存点上Connection的setSavepoint([String name])方法就可以完成这个功能,当然可以回退到指定的保存点上:connection.rollback(Savepoint point)。另外connection.releaseSavepoint(Savepoint point)可以释放掉已保存的保存点。要注意并非所有的DBMS都支持保存点,通过查询数据库元数据对象来看它是否支持保存点,如果不支持那么关于保存点的操作会抛出异常的:
if(databaseMetaData.supportsSavepoints()){
}else{
//不支持
.....

}
jdbc是一个接口集,各个数据库厂商实现他们,有些操作可能厂商不支持,所以在使用比较偏或比较高级的jdbc操作时最好确定一下他们是否支持,通过查询元数据就可以知道(datavaseMetaData.supportsXXX())!!

Satement的addBatch(),executeBatch()会将一些非查询性质的语句(包括ddl)作为一个单位来执行的。并非所有DBMS都支持批量更新。注意addBatch方法中不可加入select性质的语句。我们可以看出它这里共享了statement对象,我们知道一般事务不能够跨连接(不考虑分布式事务),所以要使一组sql语句归为一个事务的基本条件是“共享同一个连接”。使用批量更新也不失为事务实现的好方法:
try{
conn.setAutocommit(false);
Statement stmt=conn.createStatement();
stmt.addBatch("update .......");
stmt.addBatch("delete.......");
stmt.addBatch("insert........");
int[] updateCounts=stmt.executeBatch();
conn.commit();
}catch(Exception ex){

conn.rollback();
}finally{
try{
conn.close();
}catch{
}
}

在并行处理事务过程中,事务间可能操作同一个表或数据项,这时就需要同步,或锁机制,数据库一般提供四种事务隔离级别来防止事务间的相互干扰:
隔离级别:a.串行化 b. 可重复读。 c. 读已提交数据 。d. 读未提交数据。

级别的严格程度是递减的,关于这个话题请参考其他 资料。
connection.setTransactionIsolation(int LEVEL);//设定事务的隔离级别 是连接类型的常量值。
***
****
事务操作示例:
/*
事务管理一般使用try/catch/finally块来处理:首先记录自动提交的当前状态(true/false)——因为使用事务时会将连接的自动提交状态改变为false,如果我们要共享一个连接对象那么,使用事务前连接的提交状态需要被记录;然后在Try块中置连接的提交状态为false(即手动提交),之后进行相关的sql语句操作数据库,更新或查询;如果发生故障在catch块中回滚事务:conn.rollback();如果一切正常在try块末尾处提交事务:conn.commit();总在finally块中恢复执行事务前连接的提交状态;
一下我给出一个模板:
*/
package psn.czw.dbo;

import java.sql.Connection;
import java.sql.SQLException;

/**
* @author yiqing95
* 事务操作演示!
*/

public class TranscactionDemo {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
transDemo1();
}

public static void transDemo1(){
/*首先获取连接对象*/
Connection conn=DBUtil.buildConnection(
"E://programPractice//Java6//src//psn//czw//dbo//dbConfig.xml");

/*上面的连接是我重新获取的,所以其提交状态总是true,
* 但当连接是共享其他代码时,为了不干扰后续操作我们需要进行
* 类似进栈,出栈行为,这就像使用java.awt.Graphics对象
* 自己绘图时需要做的。总之共享的对象,当你在使用前需要保存
* 它的状态,使用完毕后恢复它的状态,不要影响他人!*/
boolean autoCommit=true;//用来不保存连接的初始提交状态
try {
autoCommit=conn.getAutoCommit();//获取执行事务前的提交状态

/*其次关闭掉连接对象的默认的自动提交方式/模式 */
conn.setAutoCommit(false);

DBUtil.printResultSet(
DBUtil.execQuery(conn,"select * from t"));

/*使用这个连接对象来执行sql操作*/
DBUtil.execInsert(conn, "insert t(col2,col3) values('a1','b1'),('sdf','sdfs')");

DBUtil.execUpdate(conn,"update t set col2='c3' where id=3");

int a=1/0;

conn.commit();/*提交事务,如果代码能执行到这一步表明一切操作顺利*/

} catch (SQLException e) {
e.printStackTrace();
try {
conn.rollback();//发生了异常,应回滚事务
} catch (SQLException e1) {
e1.printStackTrace();
}
}catch(Exception ex){
//处理除零异常
/*我故意在正常的sql操作中加入了一个非数据库操作
该操作会导致除零异常,这里是为了说明有时的事务并非
全部是数据库操作相关,可能还混有业务相关的操作*/
System.out.println(ex.getMessage());
try {
conn.rollback();//发生了异常,应回滚事务
} catch (SQLException e1) {
e1.printStackTrace();
}
}
finally{
/*
这里可能需要关闭语句对象,关闭连接对象。
在一些情况下连接对象可能被复用,当我们
从别处得到一个连接时,如果他是共享/复用的我们
不应该随便改变他的状态。同样注意一点如果我们从
别处得到了一个连接对象,当使用这个连接对象时
那么我们有可能已经参与了一个事务,(假设我们的
代码不用任何事物相关的API)
*/
try {

DBUtil.printResultSet(
DBUtil.execQuery(conn,"select * from t"));//打印表中数据

conn.setAutoCommit(autoCommit);//重置连接提交状态
conn.close();//可以不关闭,如果你想共享的话
} catch (SQLException e) {
e.printStackTrace();
}
}

}

}
//以上的代码提取一下可以作为一个事务操作模板。
****
*****
上面看到的好像直接操纵DBMS中的事务,但是应用事务可以跨系统,跨领域操作,比如我们可以认为一次文件的写操作也属于一个事务,我们甚至可以把文件操作,数据库操作,网络操作都归为一个事务中,这个时候为了保证事务的ACID属性,实现就复杂多了,关于分布式事务,两阶段提交协议等话题以后有机会在学习吧,现在先讨论另一个事务的实现范例:
按照分层模式,用户启动一个事务操作(比如点击一个按钮)操作流先经过UI层,再经过业务逻辑层,最后传至数据层,接着到数据库系统。因为内存的天然可丢失性,我们不得不把状态保持的持久性的硬盘中,其实设想如果内存足够大,并且不是断电丢失的话那么硬盘是完全可避免使用的。所以我们其实可以在内存中完成整个事务,然后再持久化状态到硬盘。为此我写了一个模拟这种思路的类:


package psn.czw.dbo;

import java.util.Hashtable;

/**
* @author yiqing95
* 该接口代表一个应用事务
* 拥有的操作是begin,commit,rollback
*
*/

public interface IAppTrans {
public void begin();
public void commit();
public void rollback();
}


class MyTrans implements IAppTrans{
private String state;//表示状态

private String duplicate;//副本

public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}

public MyTrans(){
/*
初始化自身的状态,我这里只是示意,实际状态可能非常复杂*/
this.state="I am state of MyTrans";
}


@Override
public void begin() {
/*开始事务时并不直接操纵原始状态
而是先复制一个副本,并在副本上操作
*/
this.duplicate=this.state;
使用副本进行操作(没有提供操作副本的接口呵!)..............略
}
@Override
public void commit() {
/*
提交时是将副本赋给原来的状态 */
this.state=this.duplicate;
}
@Override
public void rollback() {
/*回滚不做任何事情,因为是在副本上操作的并不影响原始状态*/
}

}
这只是个示意类,其实我们在提交的时候可以操作数据库,在commit方法中来持久化状态,比如这样:
public void commit() {
/*
提交时是将副本赋给原来的状态 */
this.state=this.duplicate;
//以下是数据库的事务操作:
conn.setCommit(false);
Satement stmt=conn.createSatement();
stmt.executeUpdate("insert tbl(state) values("+this.state+")");
//或者: stmt.executeUpdate("update tbl set state="+this.state+"
where id=XXXXXX
");
conn.commit();
//失败再回滚等等...
}
这样的设计使得数据库事务的时间是最短的,所以有这样的最佳实践:将应用设计为只有当它准备好与数据库通讯时才开始事务,一旦数据库相关的操作完成后立即结束数据库的事务,在事务开始和结束之间,除了数据库通信以外不做其他工作。

*****
******
因为事务的处理流程大都一致所以可以使用模板设计模式来处理,这里其实不是:
package psn.czw.dbo;

import java.sql.Connection;
import java.sql.SQLException;
/*
* 事务模板类
*/

public class TransTemplate {
Connection conn;
public TransTemplate(){
this.conn=null;/*实际使用时可获取一个连接,或由别处传递*/
}
public TransTemplate(Connection conn){
this.conn=conn;//从外部传入一个连接对象
}

public void execTrans(TransCallback transCB){
boolean autoCommit=true;//用来保存连接的初始提交状态
try {
autoCommit=conn.getAutoCommit();//获取执行事务前的提交状态

/*其次关闭掉连接对象的默认的自动提交方式/模式 */
conn.setAutoCommit(false);
//回调
transCB.doInTrans(this.conn);

conn.commit();/*提交事务,如果代码能执行到这一步表明一切操作顺利*/
System.out.println("事务顺利完成 !");

} catch (SQLException e) {
e.printStackTrace();
try {
conn.rollback();//发生了异常,应回滚事务
} catch (SQLException e1) {
e1.printStackTrace();
}
}
finally{
try {
conn.setAutoCommit(autoCommit);//重置连接提交状态
//conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
interface TransCallback{
public void doInTrans(Connection conn)throws SQLException;

}

在我机子上的使用是成功的:
Connection conn=DBUtil.buildConnection(
"E://programPractice//Java6//src//psn//czw//dbo//dbConfig.xml");

TransTemplate transTemplate=new TransTemplate(conn);
transTemplate.execTrans(new
TransCallback(){

@Override
public void doInTrans(Connection conn) throws SQLException {
Statement stmt=conn.createStatement();
stmt.executeUpdate(
"insert t(col2,col3) values('a1','b1'),('sdf','sdfs')");
stmt.executeUpdate("update t set col2='hello' where id=11");

}

}); //这里用了匿名类,只要把所有与事务相关的操作放在doInTrans方法中即可,这里的设计模仿spring中的模板类思想,但那里的那个功能很牛的,我这里写着自己玩的,这个类很脆弱,不是线程安全的!!

至此关于事务,就练习到这里!

******
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值