理解事务处理、事务处理的隔离级别,和使用JDBC进行事务处理 佟强 http://blog.youkuaiyun.com/microtong 2009年12月23日
事务是作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元必须有四个属性(ACID):原子性、一致性、隔离性和持久性,只有这样才能成为一个事务。
事务的并发控制
如果不对事务进行并发控制,并发事务的无序执行将会破坏数据的完整性。事务并发执行可能导致的异常可以分为以下几种情况:
---------------------------------------------------------------------------------------------------------------------------------------
Lost update(丢失更新)
A和B事务并发执行,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)
=================================================================
抛弃框架,如何实现分层架构下JDBC事务的控制 http://blog.youkuaiyun.com/tjcyjd/article/details/6684419
现在很多项目,特别是web项目,为了提高开发效率,基本上都用上了框架,struts1,struts2.,spring,hibernate,springmvc,ibatise等等,在事务处理方面,spring的尤其突出,它对事务做了很好的封装,通过AOP的配置,可以灵活的配置在任何一层。但是很多时候,基于需求和应用方面考虑,直接使用JDBC进行事务控制还是很有必要的。
事务应该是以业务逻辑为基础的;一个完整的业务应该对应业务层里的一个方法,而不应该是多个方法;如果业务执行过程出现异常,则整个事务应该回滚;所以,应该事务控放在业务层里;然而持久层的设计应该遵循一个很重要的原则:持久层应该保证操作的原子性,就是说持久层里的每个方法都应该是不可以分割的,也就是说数据库的连接自始至终都应该是同一个连接,而不是执行完某个Dao操作完毕并且数据库连接关闭后又重新打开一个新的数据库连接执行另一个Dao操作!
上面的说法可能有点抽象,举个简单的例子来说:针对班级(clazze)和学生(student)的操作,要想删除某个班级,就需要先把该班级下的所有学生删除,再删除班级,这两个操作是应该放在同一个事务里的,要么同时删除学生和班级成功,要么同时失败,不能学生的数据被删除没了,而班级却没被删除,这就是上面所说的----原子性。
可能上面的描述还是有点抽象,没关系,我们用代码说话。
先定义两个Dao接口,一个是班级的接口(ClazzeDao),一个是学生的接口(StudentDao),里面只提供删除的功能。
ClazzeDao:
- /**
- * FileName: ClazzeDao.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.dao;
- /**
- * 班级接口
- *
- * @author yjd
- */
- public interface ClazzeDao {
- /** 根据id删除对应的班级 */
- public void deleteClazzeByClazzeId(int clazzeId) throws DaoException;
- }
StudentDao:
- /**
- * FileName: StudentDao.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.dao;
- /**
- * 学生接口
- *
- * @author yjd
- */
- public interface StudentDao {
- /** 根据班级id删除该班级下的所有学生 */
- public void deleteStudentByClazzeId(int clazzeId) throws DaoException;
- }
定义完这两个Dao接口以后,应该是在对应的业务层(ClazzeService)中的删除班级的方法里调用这两个方法的。这样就把它们放在同一个事务中了。在调用前,我们还得做点事,弄个数据库连接工厂类(ConnectionFactory)和事务管理器类(TransactionManager)。
ConnectionFactory:
- /**
- * FileName: ConnectionFactory.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.commom;
- import java.io.IOException;
- import java.sql.Connection;
- import java.sql.SQLException;
- import java.util.Properties;
- import javax.sql.DataSource;
- import com.mchange.v2.c3p0.DataSources;
- /**
- * 数据库连接工厂
- *
- * @author yjd
- */
- public class ConnectionFactory {
- private static Properties prop = new Properties();
- // 数据源
- private static DataSource ds = null;
- // 用来把数据库连接绑定到当前线程上的变量
- private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
- static {
- try {
- prop.load(Thread.currentThread().getContextClassLoader()
- .getResourceAsStream("jdbc.properties"));
- } catch (IOException e) {
- e.printStackTrace();
- System.out.println("在classpath下没有找到jdbc.properties文件");
- }
- // 这里使用的是c3p0连接
- try {
- Class.forName("com.mysql.jdbc.Driver");
- DataSource unpooled = DataSources.unpooledDataSource(prop
- .getProperty("url"), prop.getProperty("user"), prop
- .getProperty("password"));
- ds = DataSources.pooledDataSource(unpooled);
- } catch (ClassNotFoundException e) {
- e.printStackTrace();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- /**
- * 获取数据库的Connection对象
- *
- * @return
- */
- public static synchronized Connection getConnection() {
- Connection conn = tl.get(); // 当前线程取出连接实例
- if (null == conn) {
- try {
- conn = ds.getConnection(); // 从连接池中取出一个连接实例
- tl.set(conn); // 把它绑定到当前线程上
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- return conn;
- }
- public static synchronized TransactionManager getTranManager() {
- return new TransactionManager(getConnection());
- }
- }
- /**
- * FileName: TransactionManager.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.commom;
- import java.sql.Connection;
- import java.sql.SQLException;
- import com.tjcyjd.dao.DaoException;
- /**
- * 事务管理器类
- *
- * @author yjd
- */
- public class TransactionManager {
- private Connection conn;
- protected TransactionManager(Connection conn) {
- this.conn = conn;
- }
- /**
- * 开启事务
- *
- * @throws DaoException
- */
- public void beginTransaction() throws DaoException {
- try {
- if (null != conn && !conn.isClosed()) {
- conn.setAutoCommit(false); // 把事务提交方式改为手工提交
- }
- } catch (SQLException e) {
- throw new DaoException("开户事务时出现异常", e);
- }
- }
- /**
- * 提交事务并关闭连接
- *
- * @throws DaoException
- */
- public void commitAndClose() throws DaoException {
- try {
- conn.commit(); // 提交事务
- System.out.println("提交事务");
- } catch (SQLException e) {
- throw new DaoException("开启事务时出现异常", e);
- } finally {
- close(conn);
- }
- }
- /**
- * 回滚并关闭连接
- *
- * @throws DaoException
- */
- public void rollbackAndClose() throws DaoException {
- try {
- conn.rollback();
- System.out.println("回滚事务");
- } catch (SQLException e) {
- throw new DaoException("回滚事务时出现异常", e);
- } finally {
- close(conn);
- }
- }
- /**
- * 关闭连接
- *
- * @param conn
- * @throws DaoException
- */
- private void close(Connection conn) throws DaoException {
- if (conn != null) {
- try {
- conn.close();
- } catch (SQLException e) {
- throw new DaoException("关闭连接时出现异常", e);
- }
- }
- }
- }
好了,可以开始进行我们的业务了(ClazzeService)。
ClazzeService:
- /**
- * FileName: ClazzeService.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.service;
- import com.tjcyjd.commom.ConnectionFactory;
- import com.tjcyjd.commom.TransactionManager;
- import com.tjcyjd.dao.ClazzeDao;
- import com.tjcyjd.dao.DaoException;
- import com.tjcyjd.dao.DaoFactory;
- import com.tjcyjd.dao.StudentDao;
- /**
- * 班级操作业务类
- *
- * @author yjd
- */
- public class ClazzeService {
- private ClazzeDao clazzeDao = DaoFactory.getInstance("clazzeDao",
- ClazzeDao.class);
- private StudentDao studentDao = DaoFactory.getInstance("studentDao",
- StudentDao.class);
- /**
- * 删除指定ID的班级
- *
- * @param clazzeId
- */
- public void deleteClazze(int clazzeId) {
- TransactionManager tx = ConnectionFactory.getTranManager();
- try {
- tx.beginTransaction();
- // 删除指定班级下的所有学生
- studentDao.deleteStudentByClazzeId(clazzeId);
- // 删除指定班级
- clazzeDao.deleteClazzeByClazzeId(clazzeId);
- // 提交事务并关闭连接
- tx.commitAndClose();
- } catch (DaoException e) {
- e.printStackTrace();
- // 异常回滚
- tx.rollbackAndClose();
- }
- }
- }
上面我们定义的两个Dao,我们没写它们的实现类呢,为了更明白,还是把他们(ClazzeDaoImpl,StudentDaoImpl)贴出来吧。
ClazzeDaoImpl:
- /**
- * FileName: ClazzeDaoImpl.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.dao.impl;
- import java.sql.Connection;
- import java.sql.SQLException;
- import org.apache.commons.dbutils.QueryRunner;
- import com.tjcyjd.commom.ConnectionFactory;
- import com.tjcyjd.dao.ClazzeDao;
- import com.tjcyjd.dao.DaoException;
- /**
- * 班级接口实现类
- *
- * @author yjd
- */
- public class ClazzeDaoImpl implements ClazzeDao {
- private QueryRunner qr = new QueryRunner();
- /** 删除指定班级 */
- public void deleteClazzeByClazzeId(int clazzeId) throws DaoException {
- Connection conn = ConnectionFactory.getConnection();
- // 故意错写sql语句,多了个*。
- String sql = "delete * from clazze where clazze_id=?";
- try {
- qr.update(conn, sql, clazzeId);
- System.out.println("成功执行了deleteStudentByClazzeId方法,但未提交事务");
- } catch (SQLException e) {
- throw new DaoException("删除指定ID的部门时出现异常", e);
- }
- }
- }
StudentDaoImpl:
- /**
- * FileName: StudentDaoImpl.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.dao.impl;
- import java.sql.Connection;
- import java.sql.SQLException;
- import org.apache.commons.dbutils.QueryRunner;
- import com.tjcyjd.commom.ConnectionFactory;
- import com.tjcyjd.dao.DaoException;
- import com.tjcyjd.dao.StudentDao;
- /**
- * 学生接口实现类
- *
- * @author yjd
- */
- public class StudentDaoImpl implements StudentDao {
- private QueryRunner qr = new QueryRunner();
- /** 删除指定班级下的所有学生 */
- public void deleteStudentByClazzeId(int clazzeId) throws DaoException {
- Connection conn = ConnectionFactory.getConnection();
- String sql = "delete from student where clazze_id=?";
- try {
- qr.update(conn, sql, clazzeId);
- System.out.println("成功执行了deleteStudentByClazzeId方法,但未提交事务");
- } catch (SQLException e) {
- throw new DaoException("删除指定ID的部门时出现异常", e);
- }
- }
- }
最后我们写个测试类(ClazzeServiceTest)进行代码的测试。
ClazzeServiceTest:
- /**
- * FileName: ClazzeServiceTest.java
- * CreationTime: 2011-8-14
- * Author: yjd
- * EMail: 908599713@qq.com
- * Site: http://hi.youkuaiyun.com/tjcyjd
- */
- package com.tjcyjd.service;
- /**
- * ClazzeService的测试类
- *
- * @author yjd
- */
- public class ClazzeServiceTest {
- /**
- * 主方法
- *
- * @param args
- */
- public static void main(String[] args) {
- testDeleteDept(2);
- }
- /**
- * 删除指定班级
- */
- public static void testDeleteDept(int clazzeId) {
- ClazzeService sf = new ClazzeService();
- sf.deleteClazze(clazzeId);
- }
- }
项目的整体结构如下图: