我写这篇文章的目的,为了使大家更好的理解和摸清事务的规律,希望对新手学习事务这块内容时有所帮助。
在我们开发一个应用时,很多时候我们的一个业务操作会对数据库进行多次操作,有时候我们需要保证这么一系列的操作要么全部成功,要么全部失败,其实这个这个概念就是我们今天要谈论的事务。
现在我们开发应用一般都采用三层结构,如果我们控制事务的代码都放在DAO(DataAccessObject)对象中,在DAO对象的每个方法当中去打开事务和关闭事务,当Service对象在调用DAO时,如果只调用一个DAO,那我们这样实现则效果不错,但往往我们的Service会调用一系列的DAO对数据库进行多次操作,那么,这个时候我们就无法控制事务的边界了,因为实际应用当中,我们的Service调用的DAO的个数是不确定的,可根据需求而变化,而且还可能出现Service调用Service的情况,看来手工来控制事务对于一个稍微严谨一点的系统来说完全是不现实的。
那么现在我们有什么好的解决办法吗?还记得EJB引以为傲的声明式事务吗,虽然它现在已经慢慢没落,但是它的思想被后人所吸取,我们的Spring框架是一个轻量级框架,它同样的实现了声明式事务的支持,使我们能够通过配置及可插拔的方式的完成整个应用的事务的管理。
谈到Sping事务,我们今天要说到的一个东东是ThreadLocal,早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。简单的说,ThreadLocal是为每个线程保存一份变量,各个线程访问自己对应的变量,所以我们就可以不使用synchronized关键字同样可以实现线程同步,要了解关于ThreadLocal的详细信息,请参看http://hi.baidu.com/cjjic02/blog/item/1ba41813aabde8886438dbe5.html
为了简单明了,今天我们先抛开AOP,还是先用手工的方式通过ThreadLocal来管理连接,废话不多说,先来看代码
TransactionHelper
package com.hwadee.demo;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public final class TransactionHelper {
//使用ThreadLocal持有当前线程的数据库连接
private final static ThreadLocal<Connection> connection_holder = new ThreadLocal<Connection>();
//连接配置,来自connection.properties
private final static Properties connectionProp = new Properties();
static{
//加载配置文件
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("connection.properties");
try {
connectionProp.load(is);
is.close();
//加载驱动程序
Class.forName(connectionProp.getProperty("driverClassName"));
} catch (IOException e) {
throw new RuntimeException(e.getMessage(),e);
}catch(ClassNotFoundException e){
throw new RuntimeException("驱动未找到",e);
}
}
//获取当前线程中的数据库连接
private static Connection getCurrentConnection()
{
Connection conn = connection_holder.get();
if(conn == null){
conn = createNotAutoCommitConnection();
connection_holder.set(conn);
}
return conn;
}
//执行SQL语句
public static int executeNonQuery(String sql) throws SQLException{
Connection conn = getCurrentConnection();
return conn.createStatement().executeUpdate(sql);
}
//提交事务
public static void commit(){
Connection conn = getCurrentConnection();
try {
conn.commit();
conn.close();
connection_holder.set(null);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(),e);
}
}
//回滚事务
public static void rollback(){
Connection conn = getCurrentConnection();
try {
conn.rollback();
conn.close();
connection_holder.set(null);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(),e);
}
}
//创建一个不自动Commit的数据库连接
private static Connection createNotAutoCommitConnection() {
try {
Connection conn = DriverManager.getConnection(connectionProp.getProperty("url")+";databaseName="+ connectionProp.getProperty("databaseName")
,connectionProp.getProperty("username")
,connectionProp.getProperty("password"));
conn.setAutoCommit(false);
return conn;
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(),e);
}
}
}
这个类实现了基本的连接管理与执行SQL语句的方法,可以在多线程环境下运行
程序入口
package com.hwadee.demo;
import java.sql.SQLException;
public class MainModule {
public static void main(String[] args) {
try{
insert1();
insert2();
//方法1和2都无异常,提交事务,任何一个方法出现异常都将导致事务回滚。
TransactionHelper.commit();
}catch(SQLException e){
TransactionHelper.rollback();
throw new RuntimeException(e.getMessage(),e);
}catch(RuntimeException e){
TransactionHelper.rollback();
throw new RuntimeException(e.getMessage(),e);
}
}
static void insert1() throws SQLException{
String sql = "insert into department values(1,'市场部')";
TransactionHelper.executeNonQuery(sql);
}
static void insert2() throws SQLException{
String sql = "insert into department values(2,'研发部')";
TransactionHelper.executeNonQuery(sql);
//throw new RuntimeException("回滚");
}
}
连接字符串配置,请将此文件放入classpath根目录中
connection.properties
url=jdbc:sqlserver://localhost:1433
databaseName=pubs
username=sa
password=password
driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
建表语句
USE [pubs]
go
CREATE TABLE [Department](
[DEPT_ID] [int] primary key,
[DEPT_NAME] [varchar](50)
)
GO
好了现在运行这个应用,可以正常的插入两条数据,接下来,取消insert2方法里面的注释,再运行看看效果。
static void insert2() throws SQLException{
String sql = "insert into department values(2,'研发部')";
TransactionHelper.executeNonQuery(sql);
throw new RuntimeException("回滚");
}
很重要的一点是要想实现事务,我们必须用同一个数据库连接执行这些语句,最终才能做到统一的提交和回滚。
我们可以这样假设
insert1和insert2为不同DAO的方法
仔细观察,我们的insert1和insert2并没有负责打开连接和关闭连接。而是间接的调用TransactionHelper.executeNonQuery(sql);
这样使我们执行的所有方法都是使用同一个连接进行数据库操作。
其实这个例子只是想告诉大家要实现声明式事务的一部分内容,这个例子只能实现简单的单事务模型,要实现更复杂的事务传播模型如嵌套等,还需要我们使用更多的技术,如AOP等等。先写到这里,希望对大家有所帮助!