03_事务
文章目录
- 一、准备工作
- 二、JDBC中处理事务
- 2.1、相关方法(必须掌握)
- 2.2、Dao层处理事务
- 2.3、Service层才是处理事务的地方
- 2.3.1、DAO层代码
- 2.3.2、Service层代码
- 2.4、修改JdbcUtils
- 2.4.1、JdbcUtils代码
- 2.4.2、DAO代码
- 2.4.3、Service代码
- 2.5、JdbcUtils完善(必须掌握)
一、准备工作
一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务要处理的问题,把多个对数据库的操作绑定成一个事务,要么都成功,要么都失败。
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);//开启事务…//多个JDBC操作con.commit();//try的最后提交事务
} catch() {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);//禁止自动提交//转账操作//操作1pstmt = conn.prepareStatement(sql1);//设置参数pstmt.setDouble(1, money);pstmt.setString(2, src);//发送SQLpstmt.executeUpdate();//用来测试回滚//int i = 100/0;//操作2pstmt = conn.prepareStatement(sql2);//设置参数pstmt.setDouble(1, money);pstmt.setString(2, dst);//发送SQLpstmt.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);//发送SQLpstmt.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);//发送SQLpstmt.executeUpdate();pstmt.close();}
}
2.3.2、Service层代码
import tech.code2048.dao.AccountDao;
import tech.code2048.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(…);//操作1dao.daoMethod2(…); //操作2JdbcUtils.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 tech.code2048.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);//创建DataSourcedataSource = 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);//发送SQLpstmt.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);//发送SQLpstmt.executeUpdate();pstmt.close();}
}
2.4.3、Service代码
import tech.code2048.dao.AccountDao;
import tech.code2048.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) {//RunnableThread 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);//创建DataSourcedataSource = 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();}}
}