本系列筆記均是對b站教程https://www.bilibili.com/video/av47952931 的學習筆記,非本人原創

事務
我們在service中加一個轉賬的功能
public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
accountDao.updateAccount(source);
accountDao.updateAccount(target);
}
貌似沒什么問題吧,一套下來就是轉賬的流程。但是實際上這樣寫是會出問題的,就是不符合事務的一致性,可能會出現加錢失敗了,減錢的事務還在繼續。例如將上面的代碼稍作改動:
source.setMoney(source.getMoney() - money);
int a=1/0;
target.setMoney(target.getMoney() + money);
毫無疑問上面是會報錯的,但是這時加錢的操作就不會進行了,但是減錢的操作已經做完了,這就導致了數據的異常。
導致這一現象的原因我們可以從accountDao的代碼片段中看出來:
public void updateAccount(Account account) {
try {
//name和money都是可變的,不變的是id,也就是主鍵,所以查找的時候要查id
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException();
}
}
每一次操作都是調用一次runner.update,而注意我們在bean.xml對runner的配置
<!-- queryRunner不能是單例對象,防止多線程出現問題-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!-- 注入數據源 -->
<constructor-arg name="ds" ref="dataSoure"></constructor-arg>
</bean>
runner是一個多例對象,每一個調用都是一個單獨的runner,對數據庫使用的是不同的連接,所以他們是不同的事務。解決方法就是讓所有操作共用一個連接,而不是像我們在xml中設置的那樣使用多例對象。這里的方法就是使用ThreadLocal對象把Connection和當前線程綁定從而使得一個線程只有一個能控制事務的對象。
關於ThreadLocal,可以看這篇文章:https://www.cnblogs.com/jiading/articles/12337399.html
來看我們的解決方案
首先,我們新建了一個連接的工具類,用於從數據源中獲取連接,並且和線程相綁定
ConnectionUtils.java
package com.jiading.utils;
import javax.sql.DataSource;
import java.sql.Connection;
/*
連接的工具類,用於從數據源中獲取連接並實現和線程的綁定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();//新建一個ThreadLocal對象,它存放的是Connection類型的對象
/*
設置datasource的set函數,以便於之后spring進行注入
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/*
獲取當前線程上的連接
*/
private DataSource dataSource;
//通過對connection的get方法
public Connection getThreadConnection() {
//1. 先從ThreadLocal上獲取
Connection conn = tl.get();
try {
//2. 判斷當前線程上是否有連接,沒有就創建一個
if (conn == null) {
//3.從數據源中獲取一個連接,並和線程綁定
conn = dataSource.getConnection();
//4. 存入連接
tl.set(conn);//現在ThreadLocal就和這個Connection對象綁定了
}
//5. 返回當前線程上的連接
return conn;
}catch(Exception e){
throw new RuntimeException();
}
}
/*
把連接和線程解綁
*/
public void removeConnection(){
tl.remove();
}
}
我們還需要另外一個工具類來幫忙實現和事務相關的操作。
TransactionManager.java:(注:transaction就是事務的意思)
package com.jiading.utils;
import java.sql.Connection;
import java.sql.SQLException;
/*
和事務管理相關的工具類,它包含了開啟事務、提交事務、回滾事務和釋放連接的方法
*/
public class TransactionManager {
/*
需要用到ConnectionUtils,這里設置一個set函數以便於spring注入
*/
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
private ConnectionUtils connectionUtils;
/*
開啟一個連接,並且設置它的自動commit關閉
*/
public void beginTransaction(){
try {
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
/*
手動提交
*/
public void commit(){
try {
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
/*
回滾
*/
public void rollback(){
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void release(){
try {
connectionUtils.getThreadConnection().close();//將連接放回連接池
connectionUtils.removeConnection();//解綁
} catch (SQLException e) {
e.printStackTrace();
}
}
}
這里的工具類只負責事務相關的操作,它默認在一個線程中通過connectionUtils.getThreadConnection()獲取的connection對象都是同一個,所以它才能如此放心地在每一個方法中都調用一遍connectionUtils.getThreadConnection()來獲取對象。而對於ConnectionUtils,它和單例對象不一樣,單例對象不能滿足多線程使用的要求;但是也和多例對象不一樣,多例對象不能區分是不同線程調用函數一個線程內多次調用。
對於Dao層,相應地方法的實現也變了:
package com.jiading.dao.impl;
import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.utils.ConnectionUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.SQLException;
import java.util.List;
public class AccountDaoImpl implements IAccountDao {
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
private QueryRunner runner;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
private ConnectionUtils connectionUtils;//需要使用這個連接工具類
public List<Account> findAllAccount() {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account", new BeanListHandler<Account>(Account.class));
} catch (Exception e) {
throw new RuntimeException();
}
}
public Account findAccountById(Integer accountId) {
try {
return runner.query(connectionUtils.getThreadConnection(),"select * from account where id=?", new BeanHandler<Account>(Account.class),accountId);
} catch (Exception e) {
throw new RuntimeException();
}
}
public void saveAccount(Account acc) {
try {
runner.update(connectionUtils.getThreadConnection(),"insert into account(name,money) values(?,?)",acc.getName(),acc.getMoney());
} catch (Exception e) {
throw new RuntimeException();
}
}
public void updateAccount(Account account) {
try {
//name和money都是可變的,不變的是id,也就是主鍵,所以查找的時候要查id
runner.update(connectionUtils.getThreadConnection(),"update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
} catch (Exception e) {
throw new RuntimeException();
}
}
public void deleteAccount(Integer accountId) {
try {
runner.update(connectionUtils.getThreadConnection(),"delete from account where id=?",accountId);
} catch (Exception e) {
throw new RuntimeException();
}
}
public Account findAccountByName(String AccountName) {
/*
有唯一結果就返回
沒有結果就返回Null
如果結果集合超過一個就拋出異常
*/
try {
List<Account>accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where name=?", new BeanListHandler<Account>(Account.class), AccountName);
if(accounts==null || accounts.size()==0){
return null;
}
if(accounts.size()>1){
throw new RuntimeException("結果集不唯一");
}
return accounts.get(0);
} catch (Exception e) {
throw new RuntimeException();
}
}
}
這里有個地方要注意,因為我們是采用工具類獲取連接對象的並傳入runner了,所以不能再向runner中注入connection了,所以runner.query()的參數多了一個,相應的bean.xml中也要修改:
<!-- queryRunner不能是單例對象,防止多線程出現問題-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
這里要注意,Dao中並沒有用到我們寫的那個事務的工具類,因為Dao是持久層,只負責sql語句的執行,而我們所說的事務是和我們的業務相關的,它應該是在service中:
package com.jiading.service.impl;
import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;
import java.util.List;
/*
事務的控制應該是在業務層的,而不是持久層
*/
public class AccountServiceImpl implements IAccountService {
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
private IAccountDao accountDao;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
private TransactionManager txManager;
public List<Account> findAllAccount() {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
List<Account> allAccount = accountDao.findAllAccount();
//3.提交事務
txManager.commit();
//4.返回結果
return allAccount;
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
public Account findAccountById(Integer accountId) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
Account accountById = accountDao.findAccountById(accountId);
//3.提交事務
txManager.commit();
//4.返回結果
return accountById;
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
public void saveAccount(Account acc) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
accountDao.saveAccount(acc);
//3.提交事務
txManager.commit();
//4.返回結果
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
public void updateAccount(Account account) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
accountDao.updateAccount(account);
//3.提交事務
txManager.commit();
//4.返回結果
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
public void deleteAccount(Integer accountId) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
accountDao.deleteAccount(accountId);
//3.提交事務
txManager.commit();
//4.返回結果
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
/*
這樣寫是會出問題的,就是不符合事務的一致性,可能會出現加錢失敗了,減錢的事務還在繼續
注意:一個連接對應的是一個事務。解決方法就是讓所有操作共用一個連接,而不是像我們在xml中設置的那樣使用多例對象
解決方法:
使用ThreadLocal對象把Connection和當前線程綁定從而使得一個線程只有一個能控制事務的對象
*/
accountDao.updateAccount(source);
accountDao.updateAccount(target);
//3.提交事務
txManager.commit();
//4.返回結果;
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
}
我們還需要將自己寫的工具類加入xml中以便於spring進行注入:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置數據源 -->
<bean id="dataSoure" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 連接數據庫的必備信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/jd_learning"></property>
<property name="user" value="root"></property>
<property name="password" value="<密碼>"></property>
</bean>
<!-- queryRunner不能是單例對象,防止多線程出現問題-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<bean id="accountDao" class="com.jiading.dao.impl.AccountDaoImpl">
<property name="runner" ref="runner"></property>
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
<!-- 注入dao對象-->
<property name="accountDao" ref="accountDao"></property>
<property name="txManager" ref="txManager"></property>
</bean>
<!-- 配置connection工具類 ConnectionUtils-->
<bean id="connectionUtils" class="com.jiading.utils.ConnectionUtils">
<property name="dataSource" ref="dataSoure"></property>
</bean>
<!-- 配置事務管理器-->
<bean id="txManager" class="com.jiading.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>
這樣我們就在允許多線程的前提下實現了數據庫的事務,但是顯然,這么寫是有一些問題的:
- 項目變得非常地復雜
- 重復代碼太多,特別是service中,每一個操作都要完成全套的事務相關操作
對於這些問題,我們可以使用動態代理進行解決
動態代理
Java動態代理的實現請看這篇文章:https://www.cnblogs.com/jiading/p/12343777.html
動態代理實現
BeanFactory.java:
package com.jiading.factory;
import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/*
用於創建service的代理對象的工廠
*/
public class BeanFactory {
public void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private IAccountService accountService;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
private TransactionManager txManager;
/*
獲取service的代理對象
*/
public IAccountService getAccountService(){
IAccountService accountServiceProxy=(IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
添加事務的支持
*/
Object rtValue=null;
try {
//1.開啟事務
txManager.beginTransaction();
//2.執行操作
rtValue = method.invoke(accountService, args);
//3.提交事務
txManager.commit();
//4.返回結果
return rtValue;
} catch (Exception e) {
//5. 回滾事務
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6. 釋放連接
txManager.release();
}
}
});
return accountServiceProxy;
}
}
這應該比較好理解,就是將service的一個方法放在一個事務中,這樣service中的transfer的各個步驟就是在一個事務中了
相應地,service中的事務控制就可以刪除了。這就使得service對象的編寫只需要考慮業務需求即可
AccountServiceImpl.java
package com.jiading.service.impl;
import com.jiading.dao.IAccountDao;
import com.jiading.domain.Account;
import com.jiading.service.IAccountService;
import com.jiading.utils.TransactionManager;
import java.util.List;
/*
事務的控制應該是在業務層的,而不是持久層
*/
public class AccountServiceImpl implements IAccountService {
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
private IAccountDao accountDao;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
private TransactionManager txManager;
@Override
public List<Account> findAllAccount() {
List<Account> allAccount = accountDao.findAllAccount();
return allAccount;
}
@Override
public Account findAccountById(Integer accountId) {
Account accountById = accountDao.findAccountById(accountId);
return accountById;
}
@Override
public void saveAccount(Account acc) {
accountDao.saveAccount(acc);
}
@Override
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
@Override
public void deleteAccount(Integer accountId) {
accountDao.deleteAccount(accountId);
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
Account source = accountDao.findAccountByName(sourceName);
Account target = accountDao.findAccountByName(targetName);
source.setMoney(source.getMoney() - money);
target.setMoney(target.getMoney() + money);
accountDao.updateAccount(source);
accountDao.updateAccount(target);
}
}
還需要到bean.xml中配置一下
因為不需要在service中使用事務控制了,所以就不用在z其中注入事務控制器了:
<bean id="accountService" class="com.jiading.service.impl.AccountServiceImpl">
<!-- 注入dao對象-->
<property name="accountDao" ref="accountDao"></property>
</bean>
需要配置一下代理類和beanFactory:
<!--配置代理的service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!-- 配置beanFactory-->
<bean id="beanFactory" class="com.jiading.factory.BeanFactory">
<property name="accountService" ref="accountService"></property>
<property name="txManager" ref="txManager"></property>
</bean>
這樣做的好處之前已經說過了,但是有沒有什么缺點呢?有的,就是實現和配置起來很復雜。使用接下來我們就要介紹spring中的AOP,我們之后可以使用spring框架已經實現的功能,僅需要做配置就好
