天天看點

Java中銀行轉賬的一些問題

寫在前面,

一個經典的例子

銀行轉賬

public void transfer(String sourceName, String targetName, Float money) {
        //根據名稱查詢兩個賬戶資訊
        Account source = accountDao.findByName(sourceName);
        Account target = accountDao.findByName(targetName);
        //轉出賬戶減錢,轉入賬戶加錢
        source.setMoney(source.getMoney()-money);
        target.setMoney(target.getMoney()+money);
        //更新兩個賬戶
        accountDao.updateAccount(source);
//        int i=1/0; //模拟轉賬異常
        accountDao.updateAccount(target);
    }      
@Test
    public void testTransfer() {
        //3.執行方法
//        as.deleteAccount(4);
        as.transfer("aaa","bbb",100f);
    }      

    測試方法執行後a賬戶将會減少100元,b會增加100元。這是正常的操作然而如果在代碼中

int i=1/0; //模拟轉賬異常      

這一句代碼放開注釋,模拟轉賬異常,結果發現

a減少了100元但b賬戶的金額沒有變化。

原因

Java中銀行轉賬的一些問題

因為這裡面的四個事務與資料庫的操作沒有封裝到一個事務,或者一個連接配接,一次執行中。是以報錯,這是存在非常大的安全隐患。而是4個連接配接,沒有放到一個連接配接中。

第一種解決辦法

用一個ThreadLocal對象把Connection和目前線程綁定,進而一個線程隻有一個連接配接。

事務的控制其實都應該在業務層。

編寫一個連接配接的工具類,它用于從資料源中擷取一個連接配接,并且實作和線程的綁定。

package com.itheima.utils;
import javax.sql.DataSource;
import java.sql.Connection;

/**
 * 連接配接的工具類,它用于從資料源中擷取一個連接配接,并且實作和線程的綁定
 */
public class ConnectionUtils {

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 擷取目前線程上的連接配接
     * @return
     */
    public Connection getThreadConnection() {
        try{
            //1.先從ThreadLocal上擷取
            Connection conn = tl.get();
            //2.判斷目前線程上是否有連接配接
            if (conn == null) {
                //3.從資料源中擷取一個連接配接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.傳回目前線程上的連接配接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }
    /**
     * 把連接配接和線程解綁
     */
    public void removeConnection(){
        tl.remove();
    }
}
      

接下來就是對事務進行操作,要把事務設為手動開啟

package com.itheima.utils;

/**
 * 和事務管理相關的工具類,它包含了,開啟事務,送出事務,復原事務和釋放連接配接
 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }
    /**
     * 開啟事務
     */
    public  void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 送出事務
     */
    public  void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 復原事務
     */
    public  void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 釋放連接配接
     */
    public  void release(){
        try {
            connectionUtils.getThreadConnection().close();//還回連接配接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
      

這樣寫完,因為事務都在業務層操作,是以之前的代碼就改為

package com.itheima.service.impl;

import com.itheima.dao.IAccountDao;
import com.itheima.domain.Account;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager;

import java.util.List;

/**
 * 賬戶的業務層實作類
 *
 * 事務控制應該都是在業務層
 */
public class AccountServiceImpl_OLD implements IAccountService{

    private IAccountDao accountDao;
    //事務工具類
    private TransactionManager txManager;

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

//    @Override
    public List<Account> findAllAccount() {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            List<Account> accounts = accountDao.findAllAccount();
            //3.送出事務
            txManager.commit();
            //4.傳回結果
            return accounts;
        }catch (Exception e){
            //5.復原操作
            txManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //6.釋放連接配接
            txManager.release();
        }

    }

//    @Override
    public Account findAccountById(Integer accountId) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            Account account = accountDao.findAccountById(accountId);
            //3.送出事務
            txManager.commit();
            //4.傳回結果
            return account;
        }catch (Exception e){
            //5.復原操作
            txManager.rollback();
            throw new RuntimeException(e);
        }finally {
            //6.釋放連接配接
            txManager.release();
        }
    }

//    @Override
    public void saveAccount(Account account) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.saveAccount(account);
            //3.送出事務
            txManager.commit();
        }catch (Exception e){
            //4.復原操作
            txManager.rollback();
        }finally {
            //5.釋放連接配接
            txManager.release();
        }

    }

//    @Override
    public void updateAccount(Account account) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.updateAccount(account);
            //3.送出事務
            txManager.commit();
        }catch (Exception e){
            //4.復原操作
            txManager.rollback();
        }finally {
            //5.釋放連接配接
            txManager.release();
        }

    }

//    @Override
    public void deleteAccount(Integer acccountId) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作
            accountDao.deleteAccount(acccountId);
            //3.送出事務
            txManager.commit();
        }catch (Exception e){
            //4.復原操作
            txManager.rollback();
        }finally {
            //5.釋放連接配接
            txManager.release();
        }

    }

//    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //1.開啟事務
            txManager.beginTransaction();
            //2.執行操作

            //2.1根據名稱查詢轉出賬戶
            Account source = accountDao.findAccountByName(sourceName);
            //2.2根據名稱查詢轉入賬戶
            Account target = accountDao.findAccountByName(targetName);
            //2.3轉出賬戶減錢
            source.setMoney(source.getMoney()-money);
            //2.4轉入賬戶加錢
            target.setMoney(target.getMoney()+money);
            //2.5更新轉出賬戶
            accountDao.updateAccount(source);

            int i=1/0;

            //2.6更新轉入賬戶
            accountDao.updateAccount(target);
            //3.送出事務
            txManager.commit();

        }catch (Exception e){
            //4.復原操作
            txManager.rollback();
            e.printStackTrace();
        }finally {
            //5.釋放連接配接
            txManager.release();
        }


    }
}
      

然後再進行測試,發現進行轉賬後,事務控制成功了,轉賬沒有進行。

第二種解決方法

第一種解決方法,會進行大量的配置和工具類的編寫,而且方法之間的依賴太嚴重了。用動态代理的方式來增強方法以及

動态代理的詳解請看​​​Java動态代理介紹​​​ 這時之前的業務層那麼多複雜的方法就不用寫的那麼繁雜,

把方法增強一下寫到一個factory中,之後就用這個來實作,這樣代碼的編寫比較簡單,而且也實作來方法間的解耦。

package com.itheima.factory;

import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用于建立Service的代理對象的工廠
 */
public class BeanFactory {

    private IAccountService accountService;

    private TransactionManager txManager;

    public void setTxManager(TransactionManager txManager) {
        this.txManager = txManager;
    }


    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 擷取Service代理對象
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 添加事務的支援
                     *
                     * @param proxy
                     * @param method
                     * @param args
                     * @return
                     * @throws Throwable
                     */
//                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        if("test".equals(method.getName())){
                            return method.invoke(accountService,args);
                        }

                        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();
                        }
                    }
                });

    }
}
      

但是這種方法的配置是非常複雜的,要解決這個問題就要引入Spring中的AOP概念

為什麼需要Java事務?

事務控制實際上就是控制資料的安全通路。

事務功能:

       主要用于處理操作量大,複雜度高的資料。比如說,在人員管理系統中,你删除一個人員,你既需要删除人員的基本資料,也要删除和該人員相關的資訊,如信箱,文章等等,這樣,這些資料庫操作語句就構成一個事務!

事務四大特性

         一般來說,事務是必須滿足4個條件(ACID)::原子性(Atomicity,或稱不可分割性)、一緻性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。

原子性:一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被復原(Rollback)到事務開始前的狀态,就像這個事務從來沒有執行過一樣。

一緻性:在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精确度、串聯性以及後續資料庫可以自發性地完成預定的工作。

隔離性:資料庫允許多個并發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導緻資料的不一緻。事務隔離分為不同級别,包括讀未送出(Read uncommitted)、讀送出(read committed)、可重複讀(repeatable read)和串行化(Serializable)。

持久性:事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丢失。

事務并發處理可能引起的問題

髒讀(dirty read):一個事務讀取了另一個事務尚未送出的資料,

不可重複讀(non-repeatable read) :一個事務的操作導緻另一個事務前後兩次讀取到不同的資料

幻讀(phantom read) :一個事務的操作導緻另一個事務前後兩次查詢的結果資料量不同。

舉例:

     事務A、B并發執行時,當A事務update後,B事務select讀取到A尚未送出的資料,此時A事務rollback,則B讀到的資料是無效的”髒”資料。

     當B事務select讀取資料後,A事務update操作更改B事務select到的資料,此時B事務再次讀去該資料,發現前後兩次的資料不一樣。

     當B事務select讀取資料後,A事務insert或delete了一條滿足A事務的select條件的記錄,此時B事務再次select,發現查詢到前次不存在的記錄(“幻影”),或者前次的某個記錄不見了。

jdbc操作事務

在JDBC中處理事務,都是通過Connection完成的,同一事務中所有的操作,都在使用同一個Connection對象.JDBC事務預設是開啟的,并且預設送出.,是以jdbc中的每一句sql語句都是一個事務

在connection類中提供了3個控制事務的方法:

(1) setAutoCommit(Boolean autoCommit):設定是否自動送出事務;

(2) commit();送出事務;

(3) rollback();撤消事務;

jdbc操作資料庫

()1(擷取JDBC連接配接

(2) 聲明SQL

(3) 預編譯SQL

(4) 執行SQL

(5) 處理結果集

(6) 釋放結果集

(7) 釋放Statement

(8) 送出事務

(9) 處理異常并復原事務

(10) 釋放JDBC連接配接

事務復原

事務送出