前言
JdbcTemplate是spring-jdbc提供的數據庫核心操作類,那對JdbcTemplate進行事務控制呢?
我的環境:spring-boot-2.1.3,druid-1.1.3。
原生Jdbc的事務控制
即,批處理+自動提交的控制方式,
public static void demo(String[] args) throws SQLException, ClassNotFoundException {
String url = "jdbc:mysql://10.1.4.16:3306/szhtest";
String username = "ababab";
String password = "123456";
String sql1 = "insert xx";
String sql2 = "insert xx";
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, username, password);
Statement statement = conn.createStatement();
// 獲取到原本的自動提交狀態
boolean ac = conn.getAutoCommit();
// 批處理多條sql操作
statement.addBatch(sql1);
statement.addBatch(sql2);
// 關閉自動提交
conn.setAutoCommit(false);
try {
// 提交批處理
statement.executeBatch();
// 若批處理無異常,則準備手動commit
conn.commit();
} catch (Exception e) {
e.printStackTrace();
// 批處理拋異常,則rollback
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
// 恢復到原本的自動提交狀態
conn.setAutoCommit(ac);
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
Spring的聲明式事務控制
Bean的類或方法上加@Transactional,事務控制粒度較大,只能控制在方法級別,不能控制到代碼粒度級別。
嘗試JdbcTemplate的事務控制
採取跟原生jdbc事務控制一樣的方法試試,在批處理前關閉自動提交,若批處理失敗則回滾的思路。
@RequestMapping("/druidData1")
public String druidData1() throws SQLException {
String sql1 = "INSERT INTO user_tmp(`id`, `username`) VALUES(22, 222)";
// id=1的主鍵衝突插入失敗
String sql2 = "INSERT INTO user_tmp(`id`, `username`) VALUES(1, 111)";
Connection conn = jdbcTemplate.getDataSource().getConnection();
LOG.info("1:{}", conn);
boolean ac = conn.getAutoCommit();
conn.setAutoCommit(false);
try {
int[] rs2 = jdbcTemplate.batchUpdate(new String[]{sql1, sql2});
conn.commit();
} catch (Throwable e) {
LOG.error("Error occured, cause by: {}", e.getMessage());
conn.rollback();
} finally {
conn.setAutoCommit(ac);
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
LOG.error("Error occurred while closing connectin, cause by: {}", e.getMessage());
}
}
}
return "test";
}
期望結果:id=1的因爲主鍵衝突,所以id=22的也要回滾。
實際結果:id=1的插入失敗,id=22的插入成功,未回滾。
原因分析:自始至終都是同一個connection連接對象,按道理不應該無法控制自動提交,唯一的解釋是jdbcTemplate.batchUpdate()中真正使用的連接對象並非代碼中的conn,於是一方面把conn打印出來,另一方面準備調試jdbcTemplate.batchUpdate()源碼內部,看看是否使用了另外獲取到的connection。
調試流程:jdbcTemplate.batchUpdate()
→execute(new BatchUpdateStatementCallback())
→DataSourceUtils.getConnection(obtainDataSource())
對比兩個connection,確非同一對象,因此對我們的conn進行事務的控制不會影響jdbcTemplate內部真正使用的con,
→緊接着進入源碼376行,回調函數action.doInStatement(stmt)
在回調函數中,真正進行數據庫操作。至此,便明白了這樣的方法爲何不能成功控制jdbcTemplate事務的原因,即我們控制的conn和jdbcTemplate真正使用的con不是同一個對象。那如果Druid數據庫連接池裏只有1個conn呢,這樣的方法會不會成功控制?
於是修改druid配置,將initial-size、max-active、min-idle都設置爲1,這樣,你jdbcTemplate裏獲取到的跟我的conn總該是同一對象了吧?然而,方法運行約1min後,拋出異常:
Failed to obtain JDBC Connection; nested exception is com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 60001, active 1, maxActive 1, creating 0
繼續跟了一下源碼,原來是池子裏最大隻有一個連接conn,而它又未被釋放,導致jdbcTemplate內部再去從池子裏獲取con時,一直在等待已有連接conn的釋放,一直等不到釋放,所以等待了max-wait=60000ms的時間,最後報錯。
所以這樣的控制也是不合理的,那究竟如何控制JdbcTemplate的事務呢?答案就是TransactionTemplate。
TransactionTemplate的編程式事務控制
註冊事務相關bean:TransactionTemplate,如下:
package com.boot.druid.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Druid數據庫連接池配置文件
*/
@Configuration
public class DruidConfig {
private static final Logger logger = LoggerFactory.getLogger(DruidConfig.class);
@Value("${spring.datasource.druid.url}")
private String dbUrl;
@Value("${spring.datasource.druid.username}")
private String username;
@Value("${spring.datasource.druid.password}")
private String password;
@Value("${spring.datasource.druid.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
/**
* Druid 連接池配置
*/
@Bean //聲明其爲Bean實例
public DruidDataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (Exception e) {
logger.error("druid configuration initialization filter", e);
}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}
/**
* JDBC操作配置
*/
@Bean(name = "dataOneTemplate")
public JdbcTemplate jdbcTemplate (@Autowired DruidDataSource dataSource){
return new JdbcTemplate(dataSource) ;
}
/**
* 裝配事務管理器
*/
@Bean(name="transactionManager")
public DataSourceTransactionManager transactionManager(@Autowired DruidDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* JDBC事務操作配置
*/
@Bean(name = "txTemplate")
public TransactionTemplate transactionTemplate (@Autowired DataSourceTransactionManager transactionManager){
return new TransactionTemplate(transactionManager);
}
/**
* 配置 Druid 監控界面
*/
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean srb =
new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
//設置控制檯管理用戶
srb.addInitParameter("loginUsername","root");
srb.addInitParameter("loginPassword","root");
//是否可以重置數據
srb.addInitParameter("resetEnable","false");
return srb;
}
@Bean
public FilterRegistrationBean statFilter(){
//創建過濾器
FilterRegistrationBean frb =
new FilterRegistrationBean(new WebStatFilter());
//設置過濾器過濾路徑
frb.addUrlPatterns("/*");
//忽略過濾的形式
frb.addInitParameter("exclusions",
"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return frb;
}
}
然後注入TransactionTemplate,使用transactionTemplate.execute(new TransactionCallback<> action)或者transactionTemplate.execute(new TransactionCallbackWithoutResult<> action)執行多條sql,最後可以通過transactionStatus的setRollbackOnly()或rollbackToSavepoint(savepoint) 控制事務,如下:
@RequestMapping("/druidData2")
public String runTransactionSamples() {
String sql1 = "INSERT INTO user_tmp(`id`, `username`) VALUES(22, 222)";
String sql2 = "INSERT INTO user_tmp(`id`, `username`) VALUES(1, 111)";
txTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
Object savepoint = transactionStatus.createSavepoint();
// DML執行
try {
int[] rs2 = jdbcTemplate.batchUpdate(new String[]{sql1, sql2});
} catch (Throwable e) {
LOG.error("Error occured, cause by: {}", e.getMessage());
transactionStatus.setRollbackOnly();
// transactionStatus.rollbackToSavepoint(savepoint);
}
return null;
}
});
return "test2";
}
上面是不帶參數的多條sql的事務執行,若是帶參數的多條sql,可以實現如下:
@RequestMapping("/druidData3")
public String runTransactionSamples2() {
String sql1 = "INSERT INTO user_tmp(`id`, `username`) VALUES(?, ?)";
Object[] args1 = new Object[] {22, 222};
String sql2 = "INSERT INTO user_tmp(`id`, `username`) VALUES(?, ?)";
Object[] args2 = new Object[] {1, 111};
txTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
Object savepoint = transactionStatus.createSavepoint();
// DML執行
try {
int rs1 = jdbcTemplate.update(sql1, args1);
int rs2 = jdbcTemplate.update(sql2, args2);
} catch (Throwable e) {
LOG.error("Error occured, cause by: {}", e.getMessage());
transactionStatus.setRollbackOnly();
// transactionStatus.rollbackToSavepoint(savepoint);
}
return null;
}
});
return "test2";
}