事务
事务
现阶段,要进行事务控制,前提是所有的数据操作基于同一个Connetion对象
connection.setAutoCommit(false) 开启事务 start transaction ;
connection.commit() 提交事务 commit
connection.rollback(); 回滚事务 rollback
事务控制要放在service层实现类
用ThreadLocal解决多线程下,事务控制的问题:
1 在工具类中定义一个私有静态的ThreadLocal对象,指明变量类型是Connection;
2 在自定义的开启事务的方法中,先从ThreadLocal获取Conneciton,如果没有从连接池
中获取一个连接,放到ThreadLocal中,并开始事务
3在自定义的提交事务方法中,先从ThreadLocal获取Conneciton,提交事务,将连接归还到
连接池中,从ThreadLocal移除此连接
4在自定义的回滚事务方法中,先从ThreadLocal获取Conneciton,回滚事务,将连接归还到
连接池中,从ThreadLocal移除此连接
写在后面:此处总结不是为了面试,主要是梳理编程思路,在真实项目中,ThreadLocal用的最多的场景是
保存登录用户的信息。
一、准备工作
一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务要处理的问题,把多个对数据库的操作绑定成一个事务,要么都成功,要么都失败。
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);
二、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);//开启事务… mysql中开启是start transaction
//多个JDBC操作
con.commit();//try的最后提交事务 mysql 中提交 commit
} catch() {
con.rollback();//回滚事务 mysql 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层方法中调用了JdbcUtil.beginTransaction()方法后,JdbcUtil要做准备好一个已经调用了setAuthCommitted(false)方法的Connection对象,因为在Service中调用JdbcUtils.beginTransaction()之后,马上就会调用DAO的方法,而在DAO方法中会调用JdbcUtils.getConnection()方法。这说明JdbcUtils要在getConnection()方法中返回刚刚准备好的,已经设置了手动提交的Connection对象。
在JdbcUtils中创建一个Connection con属性,当它为null时,说明没有事务!当它不为null时,表示开启了事务。
- 在没有开启事务时,可以调用“开启事务”方法;
- 在开启事务后,可以调用“提交事务”和“回滚事务”方法;
- getConnection()方法会在con不为null时返回con,在con为null时,从连接池中返回连接。
beginTransaction()
- 判断con是否为null,如果不为null,就抛出异常!
- 如果con为null,那么从连接池中获取一个Connection对象,赋值给con!然后设置它为“手动提交”。
getConnection()
- 判断con是否为null,如果为null说明没有事务,那么从连接池获取一个连接返回;
- 如果不为null,说明已经开始了事务,那么返回con属性返回。这说明在con不为null时,无论调用多少次getConnection()方法,返回的都是同个Connection对象。
commitTransaction()
- 判断con是否为null,如果为null,说明没有开启事务就提交事务,那么抛出异常;
- 如果con不为null,那么调用con的commit()方法来提交事务;
- 调用con.close()方法关闭连接;
- con = null,这表示事务已经结束!
rollbackTransaction()
- 判断con是否为null,如果为null,说明没有开启事务就回滚事务,那么抛出异常;
- 如果con不为null,那么调用con的rollback()方法来回滚事务;
- 调用con.close()方法关闭连接;
- con = null,这表示事务已经结束!
2.4.1、JdbcUtils代码
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完善(必须掌握)
ThreadLocal:本地线程对象,内部变量是线程私有的,专属于每一个线程的。
现在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
。
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();
}
}
}