JavaWeb學習筆記(十六)—— 事務

1、事務概述

1.1 什麼是事務

  銀行轉帳!張三轉10000塊到李四的帳戶,這其實須要兩條SQL語句:html

  給張三的帳戶減去10000元;java

  給李四的帳戶加上10000元。mysql

  若是在第一條SQL語句執行成功後,在執行第二條SQL語句以前,程序被中斷了(多是拋出了某個異常,也多是其餘什麼緣由),那麼李四的帳戶沒有加上10000元,而張三卻減去了10000元。這確定是不行的!web

  你如今可能已經知道什麼是事務了吧!事務中的多個操做,要麼徹底成功,要麼徹底失敗!不可能存在成功一半的狀況!也就是說給張三的帳戶減去10000元若是成功了,那麼給李四的帳戶加上10000元的操做也必須是成功的;不然給張三減去10000元,以及給李四加上10000元都是失敗的!sql

  總結:事務指邏輯上的一組操做,組成這組操做的各個單元,要不所有成功,要不所有不成功數據庫

1.2 事務的四大特性(ACID)

  事務的四大特性是:併發

  • 原子性(Atomicity):事務中全部操做是不可再分割的原子單位。事務中全部操做要麼所有執行成功,要麼所有執行失敗。
  • 一致性(Consistency):事務執行後,數據庫狀態與其它業務規則保持一致。如轉帳業務,不管事務執行成功與否,參與轉帳的兩個帳號餘額之和應該是不變的。
  • 隔離性(Isolation):隔離性是指在併發操做中,不一樣事務之間應該隔離開來,使每一個併發中的事務不會相互干擾。
  • 持久性(Durability):一旦事務提交成功,事務中全部的數據操做都必須被持久化到數據庫中,即便提交事務後,數據庫立刻崩潰,在數據庫重啓時,也必須能保證經過某種機制恢復數據。

1.3 MySQL中的事務

  在默認狀況下,MySQL每執行一條SQL語句,都是一個單獨的事務。若是須要在一個事務中包含多條SQL語句,那麼須要開啓事務和結束事務。jsp

  • 開啓事務:start transaction
  • 結束事務:commitrollback

  在執行SQL語句以前,先執行strat transaction,這就開啓了一個事務(事務的起點),而後能夠去執行多條SQL語句,最後要結束事務,commit表示提交,即事務中的多條SQL語句所作出的影響會持久化到數據庫中(從開啓事務到事務提交,中間的全部的sql都認爲有效,真正的更新數據庫)。或者rollback,表示回滾,即回滾到事務的起點,以前作的全部操做都被撤消了(從開啓事務到事務回滾,中間的全部的sql操做都認爲無效,數據庫沒有被更新)!post

  下面演示zs給li轉帳10000元的示例:性能

START TRANSACTION;
UPDATE account SET balance=balance-10000 WHERE id=1;
UPDATE account SET balance=balance+10000 WHERE id=2;
-- 回滾結束,事務執行失敗
ROLLBACK ; 

START TRANSACTION;
UPDATE account SET balance=balance-10000 WHERE id=1;
UPDATE account SET balance=balance+10000 WHERE id=2;
-- 提交結束,事務執行成功
COMMIT ;

START TRANSACTION;
UPDATE account SET balance=balance-10000 WHERE id=1;
UPDATE account SET balance=balance+10000 WHERE id=2;
-- 退出,MySQL會自動回滾事務。
quit ;

2、JDBC事務

  在jdbc中處理事務,都是經過Connection完成的!當Jdbc程序向數據庫得到一個Connection對象時,默認狀況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行,可以使用下列的JDBC控制事務語句:

  • Connection.setAutoCommit(false):設置是否爲自動提交事務,若是true(默認值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,若是設置false,那麼就至關於開啓了事務了(start transaction);
  • Connection.rollback();//回滾事務(rollback)
  • Connection.commit();//提交事務(commit)

  jdbc處理事務的代碼格式:

try {
  con.setAutoCommit(false);//開啓事務…
  ….
  …
  con.commit();//try的最後提交事務
} catch() {
  con.rollback();//回滾事務
}
  注意: 控制事務的connnection必須是同一個,即執行sql的connection與開啓事務的connnection必須是同一個才能對事務進行控制

3、事務的隔離級別

3.1 事務的併發讀問題

  • 髒讀:一個事務讀取到另外一個事務未提交數據(髒讀是不能容許的)
    事務1:張三給李四轉帳100元
    事務2:李四查看本身的帳戶
    
    t1:事務1:開始事務
    t2:事務1:張三給李四轉帳100元
    t3:事務2:開始事務
    t4:事務2:李四查看本身的帳戶,看到帳戶多出100元(髒讀)
    t5:事務2:提交事務
    t6:事務1:回滾事務,回到轉帳以前的狀態
  • 不可重複讀:一個事務讀到了另外一個事務已經提交的update的數據,致使在同一個事務中的屢次查詢結果不一致。

    事務1:酒店查看兩次1048號房間狀態
    事務2:預訂1048號房間
    
    t1:事務1:開始事務
    t2:事務1:查看1048號房間狀態爲空閒
    t3:事務2:開始事務
    t4:事務2:預約1048號房間
    t5:事務2:提交事務
    t6:事務1:再次查看1048號房間狀態爲使用
    t7:事務1:提交事務
    對同一記錄的兩次查詢結果不一致!
  • 幻讀(虛讀):一個事務讀到了另外一個事務已經提交的insert的數據,致使在同一個事務中的屢次查詢結果不一致。

    事務1:對酒店房間預訂記錄兩次統計
    事務2:添加一條預訂房間記錄
    
    t1:事務1:開始事務
    t2:事務1:統計預訂記錄100條
    t3:事務2:開始事務
    t4:事務2:添加一條預訂房間記錄
    t5:事務2:提交事務
    t6:事務1:再次統計預訂記錄爲101記錄
    t7:事務1:提交
    對同一表的兩次查詢不一致!

【不可重複讀和幻讀的區別】

  • 不可重複讀是讀取到了另外一事務的更新;
  • 幻讀是讀取到了另外一事務的插入(MySQL中沒法測試到幻讀);

3.2 四大隔離級別

  4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工做,根據不一樣的隔離級別,能夠致使不一樣的結果。不一樣事務隔離級別可以解決的數據併發問題的能力是不一樣的。

【SERIALIZABLE(串行化)】

  • 不會出現任何併發問題,由於它是對同一數據的訪問是串行的,非併發訪問的;
  • 性能最差(至關於鎖表)

【REPEATABLE READ(可重複讀)(MySQL)】

  • 防止髒讀和不可重複讀,不能處理幻讀問題;
  • 性能比SERIALIZABLE好

【READ COMMITTED(讀已提交數據)(Oracle)】

  • 防止髒讀,沒有處理不可重複讀,也沒有處理幻讀;
  • 性能比REPEATABLE READ好

【READ UNCOMMITTED(讀未提交數據)】

  • 可能出現任何事務併發問題
  • 性能最好

3.3 MySQL隔離級別

  mysql數據庫默認的事務隔離級別是:Repeatable read(可重複讀)

【mysql數據庫查詢當前事務隔離級別】

select @@tx_isolation

  例如:

  

【mysql數據庫設置事務隔離級別】

set transaction isolation level 隔離級別名

  例如:

  

使用MySQL數據庫演示不一樣隔離級別下的併發問題

一、當把事務的隔離級別設置爲read uncommitted時,會引起髒讀、不可重複讀和虛讀

  A窗口
    set transaction isolation level  read uncommitted;--設置A用戶的數據庫隔離級別爲Read uncommitted(讀未提交)
    start transaction;--開啓事務
    select * from account;--查詢A帳戶中現有的錢,轉到B窗口進行操做
    select * from account--發現a多了100元,這時候A讀到了B未提交的數據(髒讀)

  B窗口
    start transaction;--開啓事務
    update account set money=money+100 where name='A';--不要提交,轉到A窗口查詢

二、當把事務的隔離級別設置爲read committed時,會引起不可重複讀和虛讀,但避免了髒讀

  A窗口
    set transaction isolation level  read committed;
    start transaction;
    select * from account;--發現a賬戶是1000元,轉到b窗口
    select * from account;--發現a賬戶多了100,這時候,a讀到了別的事務提交的數據,兩次讀取a賬戶讀到的是不一樣的結果(不可重複讀)
  B窗口
    start transaction;
    update account set money=money+100 where name='aaa';
    commit;--轉到a窗口

三、當把事務的隔離級別設置爲repeatable read(mysql默認級別)時,會引起虛讀,但避免了髒讀、不可重複讀

  A窗口
    set transaction isolation level repeatable read;
    start transaction;
    select * from account;--發現表有4個記錄,轉到b窗口
    select * from account;--可能發現表有5條記錄,這時候發生了a讀取到另一個事務插入的數據(虛讀)
  B窗口
    start transaction;
    insert into account(name,money) values('ggg',1000);
    commit;--轉到a窗口

四、當把事務的隔離級別設置爲Serializable時,會避免全部問題

  A窗口
    set transaction isolation level Serializable;
    start transaction;
    select * from account;--轉到b窗口

  B窗口
    start transaction;
    insert into account(name,money) values('ggg',1000);--發現不能插入,只能等待a結束事務才能插入

3.4 JDBC設置隔離級別

con. setTransactionIsolation(int level)

  參數可選值以下:

  • Connection.TRANSACTION_READ_UNCOMMITTED;
  • Connection.TRANSACTION_READ_COMMITTED;
  • Connection.TRANSACTION_REPEATABLE_READ;
  • Connection.TRANSACTION_SERIALIZABLE。

4、JDBC開發中事務的處理

  在開發中,對數據庫的多個表或者對一個表中的多條數據執行更新操做時要保證對多個更新操做要麼同時成功,要麼都不成功,這就涉及到對多個更新操做的事務管理問題了。好比銀行業務中的轉帳問題,A用戶向B用戶轉帳100元,假設A用戶和B用戶的錢都存儲在Account表,那麼A用戶向B用戶轉帳時就涉及到同時更新Account表中的A用戶的錢和B用戶的錢,用SQL來表示就是:

update account set money=money-100 where name='A'
update account set money=money+100 where name='B'

4.1 原始版

[transfer.jsp]

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>transfer</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/transfer" method="post">
        轉出帳戶: <input type="text" name="out"/><br/>
        轉入帳戶: <input type="text" name="in"/></br/>
        轉帳金額: <input type="text" name="money"/><br/>
        <input type="submit" value="確認轉帳"/>
    </form>
</body>
</html>

  

【web層】

public class TransferServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 接收轉帳的參數
        String out = request.getParameter("out");
        String in = request.getParameter("in");
        String moneyStr = request.getParameter("money");
        double money = Double.parseDouble(moneyStr);

        // 調用業務層的轉帳方法
        TransferService service = new TransferService();
        boolean isTransferSuccess = service.transfer(out, in, money);

        response.setContentType("text/html;charset=UTF-8");
        if (isTransferSuccess) {
            response.getWriter().write("轉帳成功");
        } else {
            response.getWriter().write("轉帳失敗");
        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}

【service層】

public class TransferService {
    public boolean transfer(String out, String in, double money) {
        TransferDao dao = new TransferDao();

        boolean isTransferSuccess = true;

        try {
            // 轉出錢的方法
            dao.out(out, money);
            int i = 1 / 0;
            // 轉入錢的方法
            dao.in(in,money);
        } catch (Exception e) {
            isTransferSuccess = false;
            e.printStackTrace();
        }
        return isTransferSuccess;
    }
}

 【Dao層】

public class TransferDao {

    public void out(String out, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        Connection conn = JdbcUtils.getConnection();
        String sql = "update t_account set money=money-? where name=?";
        qr.update(conn, sql, money, out);
    }

    public void in(String in, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        Connection conn = JdbcUtils.getConnection();
        String sql = "update t_account set money=money+? where name=?";
        qr.update(conn, sql, money, in);
    }
}
  上述處理沒有加入事務處理,當咱們加入int i = 1/0語句時,程序就會報錯,轉帳失敗,可是數據庫中就會出現tom的錢減了100,而lucy的錢卻由於程序報錯而沒有加上,違背了事務的一致性,這是不能容許的。
 
   所以咱們須要加入事務控制,在開發中,咱們通常都是 在業務層加入事務控制。修改service層:
 1 public class TransferService {
 2     public boolean transfer(String out, String in, double money) {
 3         TransferDao dao = new TransferDao();
 4 
 5         boolean isTransferSuccess = true;
 6         Connection conn = null;
 7 
 8         try {
 9             // 開啓事務
10             conn = JdbcUtils.getConnection();
11             conn.setAutoCommit(false);
12             // 轉出錢的方法
13             dao.out(out, money);
14             int i = 1 / 0;
15             // 轉入錢的方法
16             dao.in(in, money);
17             // 事務的提交不建議放在這
18             // conn.commit();
19         } catch (Exception e) {
20             isTransferSuccess = false;
21             // 回滾事務
22             try {
23                 conn.rollback();
24             } catch (SQLException e1) {
25                 e1.printStackTrace();
26             }
27             e.printStackTrace();
28         } finally{
29             try {
30                 // 事務的提交建議放在finally裏
31                 conn.commit();
32             } catch (SQLException e) {
33                 e.printStackTrace();
34             }
35         }
36         return isTransferSuccess;
37     }
38 }

  這裏可能會有一個疑問:若是把commit放在finally裏面的話,若是程序報錯,回滾完後還提交。

  注意回滾與提交的區別:回滾自己內部不包含提交的功能,回滾是「滾」到事務開啓的地方,即第11行。程序報錯,「滾」到第11行,而後再提交,這時候能夠認爲第13-16行之間的代碼都沒執行過。

  若是把commit放在第18行,那麼finally的代碼就能夠不用寫了。這時候的狀況是:第14行代碼報錯,程序就進入catch代碼塊,接着進行回滾操做,」滾「到第11行,這時第18行的commit是沒有執行的,事務尚未結束,可是回滾完成以後,catch的代碼已經執行完畢,即方法結束了,方法結束後,鏈接池會自動幫你關閉connection,這時候事務就結束了。並且別人再拿到connection的時候,就從新開啓了一個新的事務了。

  

  修改完service層後,再次執行轉帳功能(tom給lucy轉100),這是發現,雖然咱們已經加入事務的處理了,可是轉帳失敗的時候,tom少了100元,lucy仍然不變,跟以前的狀況同樣。

  問題的緣由是:開啓事務的時候,是從鏈接池中拿的一個connection,而在操做TransferDao的時候,又從池子中拿了一個connection,這兩個connection並非同一個!!而dao中轉入、轉出兩個方法中的connection也不是同一個。以上的操做,就有了3個不一樣的connection,這是不容許的。注意:控制事務、開啓事務、以及操做每條sql的connection必須是同一個!下面對原始版進行改進。

4.2 改進版

  思路:咱們在service中拿到了connection,爲保證後面的操做是同一個connection,咱們能夠將service中的connection以參數的形式傳遞給dao層,這樣就保證了connection是同一個。

【service層】

public class TransferService {
    public boolean transfer(String out, String in, double money) {
        TransferDao dao = new TransferDao();

        boolean isTransferSuccess = true;
        Connection conn = null;

        try {
            // 開啓事務
            conn = JdbcUtils.getConnection();
            conn.setAutoCommit(false);
            // 轉出錢的方法
            dao.out(conn,out, money);
            int i = 1 / 0;
            // 轉入錢的方法
            dao.in(in,conn, money);
            // 事務的提交不建議放在這
            // conn.commit();
        } catch (Exception e) {
            isTransferSuccess = false;
            // 回滾事務
            try {
                conn.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally{
            try {
                // 事務的提交建議放在finally裏
                conn.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return isTransferSuccess;
    }
}
【Dao層】
public class TransferDao {

    public void out(Connection conn, String out, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        //Connection conn = JdbcUtils.getConnection();
        String sql = "update t_account set money=money-? where name=?";
        qr.update(conn, sql, money, out);
    }

    public void in(String in, Connection conn, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        //Connection conn = JdbcUtils.getConnection();
        String sql = "update t_account set money=money+? where name=?";
        qr.update(conn, sql, money, in);
    }
}

4.3 最終版——使用ThreadLocal

  在上面改進版的代碼中,咱們是在service中調用dao的方法,並把connection對象傳遞過去,這就保證了connection是同一個。可是這種方式很差, 咱們在開發中使用分層的目的就是使每一個層之間的邏輯更清楚。在改進版中, service層出現了connection,connection是數據庫的鏈接資源,它應該是在dao層出現的 ,這就涉及到了層與層之間的」污染「了,因此並很差。

  如今的矛盾在於:開啓事務,須要用connection開,執行sql,也要用connection執行,並且這兩個connection必須是同一個。可是事務的操做又必須在service層控制,而在service層還不想看到connection。接下來介紹ThreadLocal。

   首先咱們須要的一點是:全部的方法調用,用的都是同一個線程(除非你開啓一個新的線程)。在本案例的轉帳功能中,web層-service層-dao層之間的方法調用,都用的是同一個線程的。在操做事務時,咱們可用嘗試將connection放在一個Map中,map的key就是當前線程,value就是connection,這樣就能保證service層和dao層用的是同一個connection。其實ThreadLocal就是利用這樣的原理。

  ThreadLocal內部實際上是個Map來保存數據。雖然在使用ThreadLocal時只給出了值,沒有給出鍵,其實它內部使用了當前線程作爲鍵。

class MyThreadLocal<T> {
    private Map<Thread,T> map = new HashMap<Thread,T>();
    public void set(T value) {
        map.put(Thread.currentThread(), value);
    }
    
    public void remove() {
        map.remove(Thread.currentThread());
    }
    
    public T get() {
        return map.get(Thread.currentThread());
    }
}

  ThreadLocal類只有三個方法:

  • void set(T value)保存值;
  • T get()獲取值;
  • void remove()移除值。

【改進JdbcUtils】

public class JdbcUtils {

    // 得到Connection——從鏈接池中獲取
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource();

    // 建立ThreadLocal,<Connection>表示存放的值爲Connection類
    private static ThreadLocal<Connection> tl = new ThreadLocal();

    // 得到當前線程上綁定的connection
    public static Connection getCurrentConnection() throws SQLException {
        // 從ThreadLocal尋找當前線程是否有對應的Connection
        Connection conn = tl.get();
        if (conn == null) {
            // 得到新的connection
            conn = getConnection();
            // 將conn資源綁定到ThreadLocal上
            tl.set(conn);
        }
        return conn;
    }

    // 開啓事務
    public static void startTransaction() throws SQLException {
        Connection conn = getCurrentConnection();
        conn.setAutoCommit(false);
    }

    // 回滾事務
    public static void rollback() throws SQLException {
        getCurrentConnection().rollback();
    }

    // 提交事務
    public static void commit() throws SQLException {
        getCurrentConnection().commit();
    }

    public static DataSource getDataSource() {
        return dataSource;
    }

    // 獲取鏈接
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    // 釋放鏈接
    public static void release(Connection connection, PreparedStatement pstmt, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (pstmt != null) {
            try {
                pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

【service層】

public class TransferService {
    public boolean transfer(String out, String in, double money) {
        TransferDao dao = new TransferDao();

        boolean isTransferSuccess = true;

        try {
            // 開啓事務
            //conn = JdbcUtils.getConnection();
            //conn.setAutoCommit(false);
            JdbcUtils.startTransaction();
            // 轉出錢的方法
            dao.out(out, money);
            int i = 1 / 0;
            // 轉入錢的方法
            dao.in(in, money);
            // 事務的提交不建議放在這
            // conn.commit();
        } catch (Exception e) {
            isTransferSuccess = false;
            // 回滾事務
            try {
                JdbcUtils.rollback();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally{
            try {
                // 事務的提交建議放在finally裏
                JdbcUtils.commit();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return isTransferSuccess;
    }
}

【Dao層】——數據庫鏈接對象再也不須要service層傳遞過來,而是直接從JdbcUtils提供的getCurrentConnection方法去獲取

public class TransferDao {

    public void out(String out, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        Connection conn = JdbcUtils.getCurrentConnection();
        String sql = "update t_account set money=money-? where name=?";
        qr.update(conn, sql, money, out);
    }

    public void in(String in, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        Connection conn = JdbcUtils.getCurrentConnection();
        String sql = "update t_account set money=money+? where name=?";
        qr.update(conn, sql, money, in);
    }
}

  這樣在service層對事務的處理看起來就更加優雅了。ThreadLocal類在開發中使用得是比較多的,程序運行中產生的數據要想在一個線程範圍內共享,只須要把數據使用ThreadLocal進行存儲便可

 

參考:https://www.cnblogs.com/xdp-gacl/p/3984001.html
相關文章
相關標籤/搜索