事务
事务
本篇整理事务的概念、JDBC 中处理事务的相关方法,并围绕「事务应该在 Service 层处理」这一原则,逐步改造 DAO 层、Service 层与 JdbcUtils 工具类(含基于 ThreadLocal 的多线程完善)。
1. 事务概述
1.1 什么是事务
事务是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务要处理的问题是:把多个对数据库的操作绑定成一个事务,要么都成功,要么都失败。
1.2 准备工作
DROP DATABASE if exists mydb2;
CREATE DATABASE mydb2;
USE mydb2;
DROP TABLE IF EXISTS account;
CREATE TABLE account (
id int(11) NOT NULL AUTO_INCREMENT,
cardnum varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
money decimal(10, 0) NULL DEFAULT NULL,
PRIMARY KEY (id) USING BTREE
);
INSERT INTO account VALUES (1, '10001', 5000);
INSERT INTO account VALUES (2, '10002', 5000);2. JDBC 中处理事务
在 JDBC 中处理事务,都是通过 Connection 完成的;同一事务中所有的操作,都在使用同一个 Connection 对象。
重要
2.1 与 2.5 必须掌握,2.2 ~ 2.4 了解分析过程即可。
2.1 相关方法(必须掌握)
以下的方法都是 Connection 中的方法:
setAutoCommit(boolean):参数为true(默认值就是true)表示自动提交;con.setAutoCommit(false)表示开启事务;commit():提交事务;rollback():回滚事务。
JDBC 处理事务的代码格式:
try {
con.setAutoCommit(false);//开启事务…
//多个JDBC操作
con.commit();//try的最后提交事务
} catch (Exception e) {
con.rollback();//回滚事务
}2.2 DAO 层处理事务
public class AccountDao {
//转账
/*
* src:源卡号
* dst:目的卡号
* */
/*
* 使用传统的JDBC方式进行转账操作
* */
public void trans(String src, String dst, double money) {
Connection conn = null;
PreparedStatement pstmt = null;
String sql1 = "update account set money=money-? where cardNum=?";
String sql2 = "update account set money=money+? where cardNum=?";
try {
//获取连接
conn = JdbcUtils.getConnection();
//开启事务
conn.setAutoCommit(false);//禁止自动提交
//转账操作
//操作1
pstmt = conn.prepareStatement(sql1);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, src);
//发送SQL
pstmt.executeUpdate();
//用来测试回滚
//int i = 100/0;
//操作2
pstmt = conn.prepareStatement(sql2);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, dst);
//发送SQL
pstmt.executeUpdate();
//提交事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
try {
//回滚事务
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
//释放资源
JdbcUtils.close(conn, null, null);
}
}
}2.3 Service 层才是处理事务的地方
我们要清楚一件事:DAO 中不是处理事务的地方,因为DAO 中的每个方法都是对数据库的一次操作,而 Service 中的方法才对应一个业务逻辑。也就是说,我们需要在 Service 的一个方法中调用 DAO 的多个方法,而这些方法应该在一个事务中。
- DAO 层中的方法只进行最细粒度的增删改查;
- Service 层处理业务,对 DAO 层的方法进行组合。
怎么才能让 DAO 的多个方法使用相同的 Connection 呢?方法不能再自己获得 Connection,而是由外界传递进去:
public void daoMethod1(Connection con, …) {//操作1 减去xx
}
public void daoMethod2(Connection con, …) {//操作2 加上xx
}2.3.1 DAO 层代码
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
/*
* 提款withdrawal
* */
public void withdrawal(Connection conn, String cardNum, double money) throws SQLException {
String sql = "update account set money=money-? where cardNum=?";
PreparedStatement pstmt = null;
//操作
pstmt = conn.prepareStatement(sql);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, cardNum);
//发送SQL
pstmt.executeUpdate();
pstmt.close();
}
/*
* 存款deposit
* */
public void deposit(Connection conn, String cardNum, double money) throws SQLException {
String sql = "update account set money=money+? where cardNum=?";
PreparedStatement pstmt = null;
//操作
pstmt = conn.prepareStatement(sql);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, cardNum);
//发送SQL
pstmt.executeUpdate();
pstmt.close();
}
}2.3.2 Service 层代码
import com.qfedu.dao.AccountDao;
import com.qfedu.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.SQLException;
public class AccountService {
private AccountDao accountDao = new AccountDao();
//转账操作
public void trans(String src, String dst, double money) {
Connection connection = null;
try {
//获取连接
connection = JdbcUtils.getConnection();
//开启事务
connection.setAutoCommit(false);
//提款
accountDao.withdrawal(connection, src, money);
//用来测试回滚(演示回滚时放开下面这行注释,转账会因异常而回滚)
//int i = 100/0;
//存款
accountDao.deposit(connection, dst, money);
//提交事务
connection.commit();
} catch (Exception e) { //注意这里异常的类型
e.printStackTrace();
try {
//回滚
connection.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
} finally {
//释放资源
JdbcUtils.close(connection, null, null);
}
}
}注
在 Service 中不应该出现 Connection,它应该只在 DAO 中出现。因为它是 JDBC 的东西,JDBC 的东西是用来连接数据库的,连接数据库是 DAO 的事情;但是事务是 Service 的事儿,不能放到 DAO 中。
2.4 修改 JdbcUtils
我们把对事务的开启和关闭放到 JdbcUtils 中,在 Service 中调用 JdbcUtils 的方法来完成事务的处理,这样 Service 中就不会再出现 Connection 这一「禁忌」了。
DAO 中的方法不用再让 Service 传递 Connection,DAO 会主动从 JdbcUtils 中获取 Connection 对象。这样,JdbcUtils 就成为了 DAO 和 Service 的中介。
我们在 JdbcUtils 中添加 beginTransaction()、rollbackTransaction() 和 commitTransaction() 方法。Service 中的代码如下:
public class XXXService {
private XXXDao dao = new XXXDao();
public void serviceMethod() {
try {
JdbcUtils.beginTransaction();//开启事务
dao.daoMethod1(...);//操作1
dao.daoMethod2(...); //操作2
JdbcUtils.commitTransaction();//提交事务
} catch(Exception e) {
JdbcUtils.rollbackTransaction();//回滚
}
}
}要保证两个方法中获取的 conn 是同一个,DAO 代码如下:
public void daoMethod1(…) {
Connection con = JdbcUtils.getConnection();
}
public void daoMethod2(…) {
Connection con = JdbcUtils.getConnection();
}在 Service 层方法中调用 JdbcUtils.beginTransaction() 之后,马上就会调用 DAO 的方法,而 DAO 方法中会调用 JdbcUtils.getConnection()。因此 JdbcUtils 要准备好一个已经调用了 setAutoCommit(false) 的 Connection 对象,并在 getConnection() 方法中返回这个刚刚准备好的、已设置为手动提交的 Connection 对象。
具体做法是:在 JdbcUtils 中创建一个 Connection con 属性,当它为 null 时说明没有事务,当它不为 null 时表示开启了事务。各方法的设计如下:
| 方法 | 设计要点 |
|---|---|
beginTransaction() | 判断 con 是否为 null:不为 null 说明事务已开启,抛出异常;为 null 则从连接池获取一个 Connection 赋值给 con,并设置为「手动提交」 |
getConnection() | 判断 con 是否为 null:为 null 说明没有事务,从连接池获取一个连接返回;不为 null 说明已开启事务,直接返回 con。也就是说在 con 不为 null 时,无论调用多少次 getConnection(),返回的都是同一个 Connection 对象 |
commitTransaction() | 判断 con 是否为 null:为 null 说明没有开启事务就提交事务,抛出异常;不为 null 则调用 con 的 commit() 提交事务,调用 con.close() 关闭连接,并将 con 置为 null(事务结束) |
rollbackTransaction() | 判断 con 是否为 null:为 null 说明没有开启事务就回滚事务,抛出异常;不为 null 则调用 con 的 rollback() 回滚事务,调用 con.close() 关闭连接,并将 con 置为 null(事务结束) |
2.4.1 JdbcUtils 代码
注意
下面的实现用静态成员变量保存事务中的 Connection,在多线程环境下会互相干扰(一个线程开启的事务会阻塞其他线程),2.5 节会用 ThreadLocal 加以完善。
package com.qfedu.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JdbcUtils {
private static DataSource dataSource;
private static Connection connection;
static {
try {
Properties prop = new Properties();
InputStream in = JdbcUtils.class.getResourceAsStream("/jdbc.properties");
prop.load(in);
//创建DataSource
dataSource = DruidDataSourceFactory.createDataSource(prop);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取连接
public static Connection getConnection() throws SQLException {
if(connection != null) {
return connection;
}
return dataSource.getConnection();
}
//开启事务
public static void beginTransaction() throws SQLException {
if(connection != null) {
throw new SQLException("事务已经开启,在没有结束当前事务时,不能再开启事务!");
}
connection = dataSource.getConnection();
connection.setAutoCommit(false);
}
//提交事务
public static void commitTransaction() throws SQLException {
if(connection == null) {
throw new SQLException("当前没有事务,所以不能提交事务!");
}
connection.commit();
connection.close();
connection = null;
}
//回滚事务
public static void rollbackTransaction() throws SQLException {
if(connection == null) {
throw new SQLException("当前没有事务,所以不能回滚事务!");
}
connection.rollback();
connection.close();
connection = null;
}
//释放资源
public static void close(Connection connection, Statement statement, ResultSet rSet) {
try {
if(rSet != null) {
rSet.close();
}
if(statement != null) {
statement.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}2.4.2 DAO 代码
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class AccountDao {
/*
* 提款withdrawal
* */
public void withdrawal(String cardNum, double money) throws SQLException {
String sql = "update account set money=money-? where cardNum=?";
PreparedStatement pstmt = null;
Connection conn = JdbcUtils.getConnection();
//操作
pstmt = conn.prepareStatement(sql);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, cardNum);
//发送SQL
pstmt.executeUpdate();
pstmt.close();
}
/*
* 存款deposit
* */
public void deposit(String cardNum, double money) throws SQLException {
String sql = "update account set money=money+? where cardNum=?";
PreparedStatement pstmt = null;
Connection conn = JdbcUtils.getConnection();
//操作
pstmt = conn.prepareStatement(sql);
//设置参数
pstmt.setDouble(1, money);
pstmt.setString(2, cardNum);
//发送SQL
pstmt.executeUpdate();
pstmt.close();
}
}2.4.3 Service 代码
import com.qfedu.dao.AccountDao;
import com.qfedu.utils.JdbcUtils;
import java.sql.Connection;
import java.sql.SQLException;
public class AccountService {
private AccountDao accountDao = new AccountDao();
//转账操作
public void trans(String src, String dst, double money) {
try {
//开启事务
JdbcUtils.beginTransaction();
//取款
accountDao.withdrawal(src, money);
//用来测试回滚
//int i = 100/0;
//存款
accountDao.deposit(dst, money);
//提交事务
JdbcUtils.commitTransaction();
} catch (Exception e) { //注意这里异常的类型
e.printStackTrace();
try {
//回滚
JdbcUtils.rollbackTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}2.5 JdbcUtils 完善(必须掌握)
现在的 JdbcUtils 有一个问题:如果有两个线程,第一个线程调用了 beginTransaction() 方法,另一个线程再调用 beginTransaction() 方法时,因为 con 已经不再为 null,所以就会抛出异常。
我们希望 JdbcUtils 可以在多线程环境下使用。最好的方法是为每个线程提供一个 Connection,这样每个线程都可以开启自己的事务了。
ThreadLocal 类只有三个方法:
void set(T value):保存值;T get():获取值;void remove():移除值。
ThreadLocal 内部其实是一个 Map 来保存数据。虽然使用 ThreadLocal 时只给出了值,没有给出键,但它内部使用了当前线程作为键。
ThreadLocal 使用示例:
public class MyTestThreadLocal {
private static ThreadLocal<String> tl = new ThreadLocal<>();
/**
* set() 在当前线程中存放数据
* get() 获取当前线程的数据 - 无法获取其他线程的数据
* remove() 删除当前线程的数据
* @param args
*/
public static void main(String[] args) {
//Runnable
Thread th1 = new Thread(() -> {
tl.set("哈哈哈哈.....");
for (int i = 0; i < 100; i++) {
if(i == 5) {
tl.remove();
}
System.out.println(Thread.currentThread().getName() + ":" + tl.get());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
th1.setName("线程1");
Thread th2 = new Thread(() -> {
tl.set("嘿嘿嘿嘿.....");
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + tl.get());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
th2.setName("线程2");
//启动线程
th1.start();
th2.start();
}
}使用 ThreadLocal 修改 JdbcUtils,用 ThreadLocal<Connection> 替代原来的静态 Connection 属性,使每个线程持有自己的连接:
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JdbcUtils {
private static DataSource dataSource;
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
static {
try {
Properties prop = new Properties();
InputStream in = JdbcUtils.class.getResourceAsStream("/jdbc.properties");
prop.load(in);
//创建DataSource
dataSource = DruidDataSourceFactory.createDataSource(prop);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
//获取连接
public static Connection getConnection() throws SQLException {
Connection connection = tl.get();
if(connection != null) {
return connection;
}
return dataSource.getConnection();
}
//开启事务
public static void beginTransaction() throws SQLException {
Connection connection = tl.get();
if(connection != null) {
throw new SQLException("事务已经开启,在没有结束当前事务时,不能再开启事务!");
}
connection = dataSource.getConnection();
connection.setAutoCommit(false);
tl.set(connection);
}
//提交事务
public static void commitTransaction() throws SQLException {
Connection connection = tl.get();
if(connection == null) {
throw new SQLException("当前没有事务,所以不能提交事务!");
}
connection.commit();
connection.close();
tl.remove();
}
//回滚事务
public static void rollbackTransaction() throws SQLException {
Connection connection = tl.get();
if(connection == null) {
throw new SQLException("当前没有事务,所以不能回滚事务!");
}
connection.rollback();
connection.close();
tl.remove();
}
//释放资源
public static void close(Connection connection, Statement statement, ResultSet rSet) {
try {
if(rSet != null) {
rSet.close();
}
if(statement != null) {
statement.close();
}
if(connection != null) {
connection.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}3. 小结
- JDBC 事务核心三步:
setAutoCommit(false)开启事务、commit()提交事务、rollback()回滚事务; - 事务要放在 Service 层处理,DAO 只做最细粒度的单次增删改查;
- 同一事务中的多个操作必须使用同一个 Connection;多线程环境下,用 ThreadLocal 为每个线程维护独立的连接与事务。
