20.JDBC開發(fā)(3)事務(wù)和池(我的JavaEE筆記)

主要內(nèi)容:

  • 事務(wù)
  • 使用數(shù)據(jù)庫連接池優(yōu)化程序性能

一、事務(wù)的概念

事務(wù)指邏輯上的一組操作,組成這組操作的各個單元,要不全部成功,要不全部不成功。數(shù)據(jù)庫開啟事務(wù)命令:

  • start transaction 開啟事務(wù)
  • rollback 回滾事務(wù)
  • commit 提交事務(wù)

當(dāng)我們開啟事務(wù)后,可輸入多條sql語句讓數(shù)據(jù)庫執(zhí)行,但是如果我們在讓sql語句執(zhí)行之后最后沒有使用commit提交事務(wù),則前面執(zhí)行的所有sql語句無效,這就相當(dāng)于回到了開啟事務(wù)之前的狀態(tài),當(dāng)然有時候這種方式并不太好,我們可以自己設(shè)置回滾點,當(dāng)我們sql語句出錯時可以回到設(shè)置的那個點處的狀態(tài)。而rollback可以每次回滾一條語句。

二、使用事務(wù)

  • 當(dāng)jdbc程序向數(shù)據(jù)庫獲得一個Connection對象時,默認(rèn)情況下這個Connection對象會自動向數(shù)據(jù)庫提交在它前面發(fā)送的sql語句。若向關(guān)閉這種默認(rèn)提交方式,讓多條sql在一個事務(wù)中執(zhí)行,可使用下列語句:
    jdbc控制事務(wù)語句
    connection.setAutoCommit(false); start transaction
    connection.rollback(); rollback
    connection.commit(); commit
    設(shè)置事務(wù)回滾點
    Savepoint sp = conn.setSavepoint();
    conn.rollback(sp);
    conn.commit(); //回滾后必須要提交

例:
創(chuàng)建數(shù)據(jù)庫:

create database day16;
CREATE TABLE account(
        id INT PRIMARY KEY AUTO_INCREMENT,
        NAME VARCHAR(40),
        money FLOAT
)CHARACTER SET utf8 COLLATE utf8_general_ci;
INSERT INTO account(NAME,money) VALUES('aaa',1000);
INSERT INTO account(NAME,money) VALUES('bbb',1000);
INSERT INTO account(NAME,money) VALUES('ccc',1000);

Demo1.java

package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import cn.itcast.utils.JdbcUtils;

public class Demo1 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement  ps = null;
        ResultSet result = null;
        try {
            conn = JdbcUtils.getConnection();
            String sql1 = "update account set money=money-100 where name='aaa'";
            String sql2 = "update account set money=money+100 where name='bbb'";
            conn.setAutoCommit(false);//開啟事務(wù)
            ps = conn.prepareStatement(sql1);
            ps.executeUpdate();
            //int x = 1/0;//模擬異常,sql語句不會執(zhí)行
            ps = conn.prepareStatement(sql2);
            ps.executeUpdate();
            System.out.println("ok");
        } catch (Exception e) {
            try {
                conn.rollback();//手動通知數(shù)據(jù)庫手動回滾
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }
    }
}

例:設(shè)置回滾點
Demo2.java

package cn.itcast.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import cn.itcast.utils.JdbcUtils;

public class Demo2 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        Savepoint point = null;
        try{
            conn = JdbcUtils.getConnection();
            String sql1 = "update account set money = money - 100 where name = 'aaa'";
            String sql2 = "update account set money = money + 100 where name = 'bbb'";
            String sql3 = "update account set money = money + 100 where name = 'ccc'";
            conn.setAutoCommit(false);
            ps = conn.prepareStatement(sql1);
            ps.executeUpdate();
            
            point = conn.setSavepoint();//設(shè)置回滾點
            
            ps = conn.prepareStatement(sql2);
            ps.executeUpdate();
            
            //int x = 1/0;
            
            ps = conn.prepareStatement(sql3);
            ps.executeUpdate();
            
            conn.commit();
        }catch(Exception e){
            try {
                conn.rollback(point);//手動通知回滾,同時指定回滾點
                //回滾之后記得提交,上面我們回滾了,就表明最后的提交語句沒有執(zhí)行,那此時如果不提交
                //,數(shù)據(jù)庫在沒有收到提交的情況下,會自動回滾所有的sql語句
                conn.commit();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }   
    }
}

說明:這里如果中間出現(xiàn)異常,則只有第一條語句生效。

三、事務(wù)的特性(ACID)

  • 原子性(Atomicity)
    原子性是指事務(wù)是一個不可分割的工作單位,事務(wù)中的操作要么都是執(zhí)行成功,要么都失敗。

  • 一致性(Consistency)
    事務(wù)必須使數(shù)據(jù)庫從一個一致性狀態(tài)變換到另外一個一致性狀態(tài)。比如,在轉(zhuǎn)賬中賬戶的總額是不變的。

  • 隔離性(Isolation)
    事務(wù)的隔離性是多個用戶并發(fā)訪問數(shù)據(jù)庫時,數(shù)據(jù)庫為每一個用戶開啟的事務(wù),不能被其他事務(wù)的操作數(shù)據(jù)所干擾,多個并發(fā)事務(wù)之間要相互隔離。

  • 持久性(Durability)
    持久性是指一個事務(wù)一旦被提交,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就是永久性的,接下來即使數(shù)據(jù)庫發(fā)生故障也不應(yīng)該對其有任何影響。

四、 事務(wù)的隔離級別

多個線程開啟各自事務(wù)操作數(shù)據(jù)庫中數(shù)據(jù)時,數(shù)據(jù)庫系統(tǒng)要負(fù)責(zé)隔離操作,以保證各個線程在獲取數(shù)據(jù)時的準(zhǔn)確性。

如果不考慮隔離性,可能會引發(fā)如下問題:

  • 臟讀
    指一個事務(wù)讀取了另外一個事務(wù)未提交的數(shù)據(jù)。
    例如:a花錢讓b給辦點事,a向b轉(zhuǎn)賬之后但未提交,但是b此時會發(fā)現(xiàn)賬戶多了錢,然后將事辦完之后a卻不提交,此時b的賬戶的錢就會變回原來的數(shù)目,相當(dāng)于白干活了。

  • 不可重復(fù)讀
    在一個事務(wù)內(nèi)讀取表中的某一行數(shù)據(jù),多次讀取的結(jié)果不同。
    如a開啟一個事務(wù)后,查詢余額為200,此時b轉(zhuǎn)賬100,那么a此時查詢就是300,兩次結(jié)果不一致。當(dāng)然有些時候這樣是正確的,但是有時候卻不是,如在統(tǒng)計時我們不能讓多次的統(tǒng)計結(jié)果不一致。
    和臟讀的區(qū)別:臟讀是讀取前一事務(wù)未提交的數(shù)據(jù),不可重復(fù)讀是重新讀取了前一事務(wù)已經(jīng)提交的數(shù)據(jù)。

  • 虛讀(幻讀)
    是指在一個事務(wù)內(nèi)讀取到了別的事務(wù)插入的數(shù)據(jù),導(dǎo)致前后讀取不一致。
    比如一個表第一次查詢有2條數(shù)據(jù),此時另外一個事務(wù)插入了一條數(shù)據(jù),此時再次查詢就變成了3條數(shù)據(jù),兩次查詢結(jié)果不一致。
    和不可重復(fù)讀的區(qū)別:不可重復(fù)讀是讀取到的數(shù)據(jù)結(jié)果不同,而虛讀是指讀取到多個事務(wù)導(dǎo)致結(jié)果不一致。

五、事務(wù)隔離性的設(shè)置語句

  • 數(shù)據(jù)庫共定義了四種隔離級別:
    Serializable:可避免臟讀、不可重復(fù)讀、虛讀情況的發(fā)生。(串行化)
    Repeatable read:可避免臟讀、不可重復(fù)讀情況的發(fā)生。(可重復(fù)讀)
    Read committed:可避免臟讀情況發(fā)生(讀已提交)。
    Read uncommitted:最低級別,以上情況均無法保證。(讀未提交)

  • set transaction isolation level設(shè)置事務(wù)隔離級別(數(shù)據(jù)庫操作)

  • select @@tx_isolation查詢當(dāng)前事務(wù)隔離級別(數(shù)據(jù)庫操作)

例:設(shè)置隔離級別

Demo3.java

package cn.itcast.demo;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import cn.itcast.utils.JdbcUtils;

public class Demo3 {

    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet result = null;
        try {
            conn = JdbcUtils.getConnection();
            //查詢程序肯定至少要到這個級別
            conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
            conn.setAutoCommit(false);
            String sql = "select * from account where name='aaa'";
            ps = conn.prepareStatement(sql);
            result = ps.executeQuery();
            if(result.next()){
                System.out.println(result.getFloat("money"));
            }
            Thread.sleep(1000*10);
            result = ps.executeQuery();
            if(result.next()){
                System.out.println(result.getFloat("money"));
            }
            
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            JdbcUtils.release(conn, ps, result);
        }
    }

}

六、使用數(shù)據(jù)庫連接池優(yōu)化程序性能

應(yīng)用程序直接獲取連接的缺點:用戶每次都需要向數(shù)據(jù)庫獲得鏈接,而數(shù)據(jù)庫創(chuàng)建連接通常需要消耗相對較大的資源,創(chuàng)建時間也較長。


1.png

這時我們可以使用數(shù)據(jù)庫連接池優(yōu)化程序性能:


2.png
  • 編寫連接池需要實現(xiàn)java.sql.DataSource接口。DataSource接口中定義了兩個重載的getConnection方法:
    Connection getConnection()
    Connection getConnection(String username,String password)

  • 實現(xiàn)DataSource接口,并實現(xiàn)連接池功能的步驟:
    1.在DataSource構(gòu)造函數(shù)中批量創(chuàng)建與數(shù)據(jù)庫的連接,并把創(chuàng)建的連接加入LinkedList對象中。
    2.實現(xiàn)getConnection方法,讓getConnection方法每次調(diào)用時,從LinkedList中取一個Connection返回給用戶。
    3.當(dāng)用戶使用完Connection,調(diào)用Connection.close()方法時,Collection對象應(yīng)保證將自己返回到LinkedList中,而不要把Collection還給數(shù)據(jù)庫。
    Collection保證將自己返回到LinkedList中是此處編程的難點。

示例:模擬數(shù)據(jù)庫連接池
JdbcPool.java

package junit.test;
import java.io.InputStream;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.LinkedList;
import java.util.Properties;
import java.util.logging.Logger;
import javax.sql.DataSource;

public class JdbcPool implements DataSource {
    
    //后面涉及到大量的增刪改查,所以使用LinkedList類
    private static LinkedList<Connection> list = new LinkedList<Connection>();
    
    static {
        try {
            InputStream in = JdbcPool.class.getClassLoader().getResourceAsStream("db.properties");
            Properties properties = new Properties();
            properties.load(in);
            String driver = properties.getProperty("driver");
            String url = properties.getProperty("url");
            String username = properties.getProperty("username");
            String password = properties.getProperty("password");
            
            Class.forName(driver);
            
            for(int i = 0; i < 10; i++){
                Connection conn = DriverManager.getConnection(url, username, password);
                list.add(conn);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        if(list.size() > 0){
            //如果連接池中還有Connection連接則從池中刪除并返回給調(diào)用者
            Connection conn = list.removeFirst();
            return conn;
        }else{
            throw new RuntimeException("數(shù)據(jù)庫正忙");
        }
    }

    其他需要實現(xiàn)的方法我們并不關(guān)心,這里省略
}

說明:

  • 這里有個問題是如果調(diào)用者使用完Connection鏈接之后調(diào)用方法conn.close();那么此鏈接將不會返回給數(shù)據(jù)庫連接池,而是直接返回給了數(shù)據(jù)庫,這樣鏈接會用一個少一個,顯然不行。也就是說不能這樣,或者說close方法不夠用,我們需要增強一下,讓其不要返還給數(shù)據(jù)庫,而是返還給連接池。

  • 對于類的某個方法不能達(dá)到我們的要求時需要對其進(jìn)行增強,而增強的方式有三種:1.寫一個子類,覆蓋其close方法;2.寫一個Connection的包裝類,增強close方法;3.使用動態(tài)代理,返回一個代理對象出去,攔截close方法的調(diào)用,達(dá)到對close方法增強的功能。

  • 第一種不行,因為我們在要返回Connection的時候?qū)ο笾幸呀?jīng)封裝了相關(guān)信息,即使我們寫一個子類也僅僅表明此子類有和父類相同的功能,但是卻沒有父類中已經(jīng)封裝好了的信息,不能對數(shù)據(jù)庫進(jìn)行操作。

  • 第二種:寫一個包裝類。

/*
 * 用包裝設(shè)計模式對某個對象進(jìn)行增強步驟:
 * 1、寫一個類,實現(xiàn)與被增強對象(這里要增強的對象是mysql的連接對象connection)相同的接口(這里的接口是Connection)
 * 2.定義一個變量,指向被增強對象
 * 3、定義一個構(gòu)造方法,接收被增強對象(也就是將我們要增強的對象傳遞進(jìn)來進(jìn)行增強)
 * 4、覆蓋想增強的方法(這里是close方法)、
 * 5、對于不想增強的方法,直接調(diào)用被增強對象的方法,如this.conn.unwrap(iface)
 * */
class MyConnection implements Connection{
    
    private Connection conn ;
    private List pool;//這里我們需要將數(shù)據(jù)庫連接池傳遞進(jìn)來,因為之后我們使用的鏈接都是增強之后的鏈接
    
    public MyConnection() {
    }
    public MyConnection(Connection conn , List pool){
        this.conn = conn;
        this.pool = pool;
    }
    
    //這里我們只是需要增強close方法,其他方法直接調(diào)用父類的方法即可
    @Override
    public void close() throws SQLException {
        pool.add(conn);
    }
    
    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return this.conn.unwrap(iface);
    }
其他方法和上面這個方法類似,此處省略
}

說明:之后返回就不是返回Connection對象了,而是return new MyConnection(conn, list);,但是顯然我們可以看到這種方式太麻煩,因為其中的方法太多。

  • 第三種:使用動態(tài)代理
@Override
    public Connection getConnection() throws SQLException {
        if(list.size() > 0){
            //如果連接池中還有Connection連接則從池中刪除并返回給調(diào)用者
            final Connection conn = list.removeFirst();
            //第一個參數(shù)指的是使用哪個類裝載器,第二個參數(shù)指明我們要對那個對象進(jìn)行增前,第三個參數(shù)指明增強對象完成什么功能
            return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(), 
                    new InvocationHandler() {
                //使用動態(tài)代理之后其實不管之后我們調(diào)用Connection的什么方法(commit、rollback...)其實都是調(diào)用下面的invoke方法
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
                    //如果調(diào)用的方法不是close方法,那么我們使用原來Connection的方法
                    if(!method.getName().equals("close")){
                        return method.invoke(conn, args);
                    }else{
                        //如果調(diào)用close方法,我們將鏈接返還給數(shù)據(jù)庫連接池
                        return list.add(conn);
                    }
                }
            });
        }else{
            throw new RuntimeException("數(shù)據(jù)庫正忙");
        }
    }

說明:其實動態(tài)代理是使用的攔截技術(shù),這里我們不詳細(xì)講,在后面將過濾器會詳細(xì)說明。

那么我們可以對之前的數(shù)據(jù)庫工具類做一些改進(jìn):
JdbcUtils.java

package junit.test;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcUtils {

    private static JdbcPool pool = new JdbcPool();
    public static Connection getConnection() throws SQLException{
        return pool.getConnection();
    }
    
    public static void release(Connection conn, Statement ps , ResultSet result){
        if(result != null){
            try {
                result.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            result = null;
        }
        if(ps != null){
            try {
                ps.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            ps = null;
        }
        if(conn != null){
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • JDBC概述 在Java中,數(shù)據(jù)庫存取技術(shù)可分為如下幾類:JDBC直接訪問數(shù)據(jù)庫、JDO技術(shù)、第三方O/R工具,如...
    usopp閱讀 3,640評論 3 75
  • JDBC簡介 SUN公司為了簡化、統(tǒng)一對數(shù)據(jù)庫的操作,定義了一套Java操作數(shù)據(jù)庫的規(guī)范,稱之為JDBC。JDBC...
    奮斗的老王閱讀 1,638評論 0 51
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,734評論 18 399
  • 本文包括傳統(tǒng)JDBC的缺點連接池原理自定義連接池開源數(shù)據(jù)庫連接池DBCP連接池C3P0連接池Tomcat內(nèi)置連接池...
    廖少少閱讀 16,951評論 0 37
  • 提起了的筆總是會放下,我怕把你寫進(jìn)我的故事里。 多年后也許我還會想起,曾經(jīng)有那么一個人,無關(guān)風(fēng)花雪月,只是我...
    好人壞人閱讀 437評論 0 2

友情鏈接更多精彩內(nèi)容