理解事务处理、事务处理的隔离级别,和使用JDBC进行事务处理
理解事务处理、事务处理的隔离级别,和使用JDBC进行事务处理
佟强 http://blog.csdn.net/microtong 2009年12月23日
事务是作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元必须有四个属性(ACID):原子性、一致性、隔离性和持久性,只有这样才能成为一个事务。
原子性Atomic
事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
一致性Consistency
只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
隔离Isolation
事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。同时,并发事务的修改必须与其他并发事务的修改相互独立。
持久性Durability
事务完成之后,它对于系统的影响是永久性的。
事务的并发控制
如果不对事务进行并发控制,并发事务的无序执行将会破坏数据的完整性。事务并发执行可能导致的异常可以分为以下几种情况。
Lost update(丢失更新)
A和B事务并发执行,A事务执行更新后,提交;B事务在A事务更新后,B事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了。
Dirty Reads(脏读)
A和B事务并发执行,B事务执行更新后,A事务查询B事务没有提交的数据,B事务回滚,则A事务得到的数据不是数据库中的真实数据。也就是脏数据,即和数据库中不一致的数据。
Non-repeatable Reads(非重复读)
A和B事务并发执行,A事务查询数据,然后B事务更新该数据,A再次查询该数据时,发现该数据变化了。
Second lost updates(第二类丢失更新,可以称为覆盖更新):
是非重复读的一种特殊情况,即A事务更新数据,然后B事务更新该数据,A事务查询发现自己更新的数据变了。
Phantom Reads(幻像读)
A和B事务并发执行,A事务查询数据,B事务插入或者删除数据,A事务再次查询发现结果集中有以前没有的数据或者以前有的数据消失了。
数据库的隔离级别
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。为了兼顾并发效率和异常控制,在标准SQL规范中,定义了4个事务隔离级别。
Read Uncommitted(未提交读):
即使一个更新语句没有提交,别的事务也可以读到这个改变。如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。
Read Committed(已提交读):
更新语句提交以后别的事务才能读到这个改变。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
Repeatable Read(可重复读):
在同一个事务里面先后执行同一个查询语句的时候,确保得到的结果是一样的。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
Serializable(串行化):
事务执行的时候不允许别的事务并发执行。事务串行化执行,事务只能一个接着一个地执行,而不能并发执行。
隔离级别对并发的控制
各隔离级别对各种异常的控制能力如下表所示,其中Y表示会出现该种异常,N表示不会出现该种异常。
丢失更新 | 脏读 | 非重复读 | 覆盖更新 | 幻像读 | |
未提交读 | Y | Y | Y | Y | Y |
已提交读 | N | N | Y | Y | Y |
可重复读 | N | N | N | N | Y |
串行化 | N | N | N | N | N |
JDBC事务处理
JDBC程序员要负责启动和结束事务,从而确保数据的逻辑一致性。程序员必须定义数据修改的顺序,使数据的修改与业务规则保持一致。程序员将这些修改语句放在一个事务中,使数据库引擎能够强制该事务的物理完整性。
Connection接口定义了事务处理相关的方法:
void setAutoCommit(boolean autoCommit)
设置是否自动提交事务,默认为自动提交。setAutoCommit(false)开始一个事务。
void setTransactionIsolation(int level)
设置事务的隔离级别,事务隔离级别影响事务的并发执行能力。
void commit()
提交事务,使修改动作生效。
void rollback()
回滚事务,撤销修改动作。
JDBC事务处理的例子
这个例子实现了系统内转账的功能,将付款账号的金额减去1000元,而收款账号的金额加上1000元。这要求两条update语句处于一个事务中,以确保操作的原子性。
系统采用MySQL数据库,需要注意的是MySQL表的默认类型MyISAM是不支持事务的,需要使用表类型InnoDB来支持事务。账号表的名字是account,字段有账号account_number、开户人姓名name、账户金额money。以下SQL语句创建账号表并插入两条记录。
create table account(
account_number varchar(30) primary key, /*账号*/
name varchar(100), /*开户人姓名*/
money double /*账户金额*/
)type = InnoDB default character set gbk;
/*插入张三的账号,金额是1万元*/
insert into account(account_number,name,money)
values('9558 8101 1174 1234 567','张三',10000.00);
/*插入李四的账号,金额也是1万元*/
insert into account(account_number,name,money)
values('9558 8102 1285 4321 789','李四',10000.00);
以下Java程序(Transaction.java)实现转账的功能,将两条update语句放在一个事务里,确保这两条update语句要么全执行成功,要么全执行失败,从而保证数据的完整性。
package cn.oakcms;
import java.sql.*;
public class Transaction {
public static void main(String[] args) {
Connection conn = null; //连接对象
PreparedStatement pstmt = null; //预编译的SQL语句对象
try{
//加载MySQL驱动程序
Class.forName("com.mysql.jdbc.Driver");
//连接字符串
String url =
"jdbc:mysql://127.0.0.1:3306/mydb?useUnicode=true&characterEncoding=gbk";
//建立数据库连接
conn = DriverManager.getConnection(url,"root","");
//设置事务的隔离级别
conn.setTransactionIsolation(Connection. TRANSACTION_REPEATABLE_READ);
//设置自动提交为false,开始事务
conn.setAutoCommit(false);
//带参数的更新语句
String sql = "update account set money=money+? where account_number=?";
//准备语句
pstmt = conn.prepareStatement(sql);
//绑定参数,执行更新语句,将张三的账户金额减去1000元
pstmt.setDouble(1, -1000.00);
pstmt.setString(2, "9558 8101 1174 1234 567");
pstmt.execute();
//绑定参数,执行更新语句,将李四的账户金额增加1000元
pstmt.setString(1, "一千元"); //绑定了非法参数
pstmt.setString(2, "9558 8102 1285 4321 789");
pstmt.execute(); //将抛出SQL异常
//提交事务
conn.commit();
System.out.println("事务已提交,转账成功!");
//关闭语句、连接
pstmt.close(); conn.close();
}catch(Exception e){
try{
conn.rollback(); //回滚事务
System.out.println("事务回滚成功,没有任何记录被更新!");
}catch(Exception re){
System.out.println("回滚事务失败!");
}
e.printStackTrace();
}finally{
if(pstmt!=null) try{pstmt.close();}catch(Exception ignore){}
if(conn!=null) try{conn.close();}catch(Exception ignore){}
}
}
}
由于程序中第2条更新语句绑定了错误的参数“一千元”,将会抛出SQLException,程序在catch语句块中回滚事务。程序的输入如下:
事务回滚成功,没有任何记录被更新!
com.mysql.jdbc.MysqlDataTruncation: Data truncation:
Truncated incorrect DOUBLE value: 'һǧԪ'
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3513)
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3447)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1951)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2101)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2554)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1761)
at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1021)
at cn.oakcms.Transaction.main(Transaction.java:57)
《Java程序设计》课件
http://www.oakcms.cn/java/
OakCMS内容管理系统
http://www.oakcms.cn