天天看點

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

事務 & 資料庫連接配接池 & DBUtils

事務

Transaction,其實指的一組操作,裡面包含許多個單一的邏輯。

隻要有一個邏輯沒有執行成功,那麼都算失敗。 所有的資料都回歸到最初的狀态(復原)。

為什麼要有事務?

為了確定邏輯的成功。例子:銀行的轉賬。

1、使用指令行方式示範事務。

(1)開啟事務指令:start transaction;

(2)送出,復原事務指令:

commit:送出事務,資料将會寫到磁盤上的資料庫

rollback:資料復原,回到最初的狀态。

(3)關閉自動送出功能

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

(4)示範事務

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

2、使用代碼方式示範事務

代碼裡面的事務,主要是針對連接配接來的。

(1)通過conn.setAutoCommit(false) 來關閉自動送出的設定

(2)送出事務:conn.commit();

(3)復原事務:conn.rollback();

public static void main(String[] args){
		
		Connection conn = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		
		try {
			conn = JDBCUtil.getConn();
			
			//連接配接,事務預設就是自動送出的。 關閉自動送出。
			conn.setAutoCommit(false);
			
			String sql = "update account set money = money + ? where id = ?";
			ps = conn.prepareStatement(sql);
			
			//扣錢,扣ID為1 的100塊錢
			ps.setInt(1, -100);
			ps.setInt(2, 1);
			ps.executeUpdate();
			
			// 模拟中間出現異常
			int a = 10 / 0 ;
			
			//加錢,給ID為2 加100塊錢
			ps.setInt(1, 100);
			ps.setInt(2, 2);
			ps.executeUpdate();
			
			//成功: 送出事務。
			conn.commit();
			
		} catch (SQLException e) {
			try {
				//事變: 復原事務
				conn.rollback();
			} catch (SQLException e1) {
				e1.printStackTrace();
			}
			e.printStackTrace();
			
		} finally {
			JDBCUtil.release(conn, ps, rs);
		}
	}
           

3、事務的特性

(1)原子性

事務中包含的邏輯,不可分割。

(2)一緻性

事務執行前後,資料完整性。

語句全部執行成功,或者全部失敗。

(3)隔離性

事務與事務之間沒有影響,互相隔離。

(4)持久性

事務執行成功,那麼資料應該持久儲存到磁盤上。

4、事務的安全隐患

不考慮隔離級别設定,那麼會出現以下問題。

讀的問題:髒讀,不可重讀讀,幻讀。

  • 髒讀:A事務讀到了B事務還未送出的資料。
  • 不可重複讀:A事務讀到了B事務送出的資料,造成了前後兩次查詢結果不一緻。

5、讀未送出 示範

(1)設定A視窗的隔離級别為:讀未送出

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

(2)AB兩個視窗都分别開啟事務

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

6、讀已送出 示範

(1)設定A視窗的隔離級别為:讀已送出

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

(2)AB兩個視窗都開啟事務,在B視窗執行更新操作。

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

(3)在A視窗兩次執行的查詢結果不一緻。

一次是在B視窗送出事務之前,一次是在B視窗送出事務之後。

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

這個隔離級别能夠屏蔽,髒讀的現象,但是引發了另一個問題,不可重複讀。

7、可串行化

(1)如果有一個連接配接的隔離級别設定為了串行化,那麼誰先打開了事務,誰就有了先執行的權利,誰後打開事務,誰就隻能得着,等前面的那個事務,送出或者復原後,才能執行。

但是這種隔離級别一般比較少用,容易造成性能上的問題,效率比較低。

(2)按效率劃分,從高到低:

讀未送出 > 讀已送出 > 可重複讀 > 可串行化

(3)按攔截程度,從高到底

可串行化 > 可重複讀 > 讀已送出 > 讀未送出

8、事務總結

(1)需要掌握的:

在代碼裡面會使用事務:

  • conn.setAutoCommit(false);
  • conn.commit();
  • conn.rollback();

事務隻是針對連接配接連接配接對象,如果再開一個連接配接對象,那麼還是預設自動送出事務。

(2)需要了解的:

存在安全隐患

讀的問題:

  • 髒讀:A事務讀到了B事務未送出的資料。
  • 不可重複讀:A事務讀到了B事務已送出的資料,造成前後兩次查詢結果不一緻。
  • 幻讀:A事務讀到了B事務insert的資料,造成前後查詢結果不一緻。

寫的問題:

  • 丢失更新:A事務更新資料,先送出,B事務也更新對應資料,後送出,造成A事務的送出被覆寫。

(3)隔離級别:

  • 讀未送出

    引發了:髒讀

  • 讀已送出

    解決了:髒讀

    引發了:不可重複讀

  • 可重複讀

    解決了:髒讀,不可重複讀

    未解決:幻讀

  • 可串行化

    解決了:髒讀,不可重複讀,幻讀

    問題全部解決,效率卻很低,事務排隊執行

MySQL:預設的隔離級别是:可重複讀。

Oracle:預設的隔離級别是:讀已送出。

(4)丢失更新:

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

(5)解決丢失更新

  • 悲觀鎖:可以在查詢的時候,加入 for update。
    事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils
  • 樂觀鎖:要求程式員自己控制。
    事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

資料庫連接配接池

資料庫的連接配接對象建立工作,比較消耗性能。

可以一開始先在記憶體中開辟一塊空間(集合),開始先往池子裡面放置 多個連接配接對象。

後面需要連接配接的話,直接從池子裡面去。不要去自己建立連接配接了。使用完畢,要記得歸還連接配接。確定連接配接對象能循環利用。

事務 & 資料庫連接配接池 & DBUtils事務 & 資料庫連接配接池 & DBUtils

1、自定義資料庫連接配接池

代碼實作:

public class MyDataSource implements DataSource {

    // 預設初始化資料庫連接配接池中有20個連接配接對象
    private static List<Connection> list = new ArrayList<>(20);
    // 單例模式,設定一個執行個體化對象
    private static MyDataSource mds = new MyDataSource();

    // 類加載時,初始化資料庫連接配接池中的連接配接對象
    static {
        for (int i = 0; i < 20; i++) {
            try {
                Connection conn = DBUtil.getConnection();
                mds.list.add(conn);
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
        System.out.println("靜态代碼塊執行:" + mds.getList().size());
    }

    public static MyDataSource getMds() {
        return mds;
    }

    public static List<Connection> getList() {
        return list;
    }

    // 構造方法私有化,確定隻有一個執行個體化對象
    private MyDataSource() {

    }

    // 從連接配接池中取出一個資料庫連接配接對象
    @Override
    public Connection getConnection() throws SQLException {
        Connection conn = null;
        if (list.size() > 1) {
            Connection connRem = list.remove(0);

            // 抛出包裝類對象
            conn = new ConnectionWrap(connRem, list);
        } else {  // 池子裡的連接配接對象快用完了,再添加1個
            list.add(list.get(0));
            Connection connRem = list.remove(0);
            conn = new ConnectionWrap(connRem, list);
        }
        return conn;
    }

	// 額外的方法,歸還連接配接對象
	public static void addBack(Connection conn) {
		list.add(conn);
	}

	// 這裡還有接口實作的其他方法,不用管它
}
           

出現的問題:

  • 需要額外記住 addBack方法
  • 單例。
  • 無法面向接口程式設計。

怎麼解決? 以addBack為切入點。

解決自定義資料庫連接配接池出現的問題:

由于多了一個addBack()方法,是以使用這個連接配接池的地方,需要額外記住這個方法,并且還不能面向接口程式設計。

我們打算修改接口中的那個close方法。 原來的Connection對象的close方法,是真的關閉連接配接。

打算修改這個close方法,以後在調用close,并不是真的關閉,而是歸還連接配接對象。

如何擴充某一個方法?

原有的方法邏輯,不是我們想要的,想修改成自己的邏輯。

  • 直接改源碼,可是無法實作。sun公司已經不允許修改源碼了。
  • 繼承,必須得知道這個接口的具體實作是誰。也很難做到。
  • 那就使用裝飾者模式,額外添加某個功能。 (還有更進階的,動态代理,這裡不講)

2、DBCP開源連接配接池

先導入jar檔案

(1)不使用配置檔案的方式:

// dbcp不使用配置檔案的方式
public class Test01 {
    public static void main(String[] args) {

        // 建立資料源對象
        BasicDataSource dataSource = new BasicDataSource();

        // 設定一系列配置資訊
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/study_test");
        dataSource.setUsername("root");
        dataSource.setPassword("666666");

        // 開始連接配接
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            // 擷取連接配接對象
            conn = dataSource.getConnection();

            // 擷取預編譯的資料庫連接配接對象
            String sql = "select ename, job, sal from emp";
            ps = conn.prepareStatement(sql);

            // 執行sql語句
            rs = ps.executeQuery();

            // 處理查詢結果集
            while (rs.next()) {
                String ename = rs.getString("ename");
                String job = rs.getString("job");
                double sal = rs.getDouble("sal");

                System.out.println(ename + "  " + job + "  " + sal);
            }

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            DBClose.close(conn, ps, rs);
        }
    }
}
           

(2)使用配置檔案的方式:

// dpcp使用配置檔案的方式
public class Test02 {
    public static void main(String[] args) {

        /*// 建立資料源對象
        BasicDataSource dataSource = new BasicDataSource();
        // 綁定配置檔案的資訊
        dataSource.setConnectionProperties("dbcpconfig.properties");
        // 經過測試,此方法行不通*/

        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;

        try {

            // 擷取屬性配置檔案
            InputStream in = new FileInputStream("src/dbcpconfig.properties");
            Properties properties = new Properties();
            properties.load(in);
            // 綁定配置檔案的資訊
            DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);

            conn = dataSource.getConnection();

            String sql = "select ename, job, sal from emp";
            ps = conn.prepareStatement(sql);

            rs = ps.executeQuery();

            while (rs.next()) {
                String ename = rs.getString("ename");
                String job = rs.getString("job");
                double sal = rs.getDouble("sal");

                System.out.println(ename + "  " + job + "  " + sal);
            }
        } catch (SQLException | FileNotFoundException throwables) {
            throwables.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放資源
            DBClose.close(conn, ps, rs);
        }
        
    }
}
           

3、C3P0開源連接配接池

先導入jar檔案到lib目錄。

(1)不使用配置檔案的方式

// c3p0不使用配置檔案的方式
public class Test01 {
    public static void main(String[] args) {

        Connection conn = null;
        PreparedStatement ps = null;

        try {
            // 建立資料源對象
            ComboPooledDataSource cpds = new ComboPooledDataSource();

            // 設定資訊
            cpds.setDriverClass("com.mysql.jdbc.Driver");
            cpds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/study_test");
            cpds.setUser("root");
            cpds.setPassword("666666");

            // 擷取連接配接
            conn = cpds.getConnection();

            // 擷取預編譯的資料庫操作對象
            String sql = "update t_user set pwd = ? where id = ?";
            ps = conn.prepareStatement(sql);

            // 給占位符傳值
            ps.setString(1, "123");
            ps.setString(2, "1");

            // 執行sql語句
            int affectLine = ps.executeUpdate();

            // 處理結果
            System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");

        } catch (SQLException | PropertyVetoException throwables) {
            throwables.printStackTrace();
        } finally {
            DBClose.close(conn, ps, null);
        }
    }
}
           

(2)使用配置檔案的方式

// c3p0使用配置檔案的方式
public class Test02 {
    public static void main(String[] args) {

        Connection conn = null;
        PreparedStatement ps = null;

        try {
            // 建立預設的資料源對象
            // 會自動綁定配置檔案c3p0-config.xml,名字必須為這個,不能寫錯
            ComboPooledDataSource cpds = new ComboPooledDataSource();

            // 也可以指定連接配接oracle資料庫
            //ComboPooledDataSource cpds1 = new ComboPooledDataSource("oracle");

            // 擷取連接配接
            conn = cpds.getConnection();

            // 擷取預編譯的資料庫操作對象
            String sql = "update t_user set pwd = ? where id = ?";
            ps = conn.prepareStatement(sql);

            // 給占位符傳值
            ps.setString(1, "123");
            ps.setString(2, "1");

            // 執行sql語句
            int affectLine = ps.executeUpdate();

            // 處理結果
            System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        } finally {
            DBClose.close(conn, ps, null);
        }
    }
}
           

DBUtils

1、增删改

DBUtils隻是幫我們簡化了CRUD(增删改查)操作,連接配接資料庫并不是它做的。

(1)針對增加,删除,更新操作

  • queryRunner.update(sql, params);

    使用update()方法,形式參數為語句sql,變長占位符的傳值

測試代碼:

public class Test01 {
    public static void main(String[] args) {

        try {
            // 建立連接配接池對象
            ComboPooledDataSource dataSource = new ComboPooledDataSource();

            // 建立DBUtil操作對象,并綁定到連接配接池
            QueryRunner queryRunner = new QueryRunner(dataSource);

            // 執行sql語句,增删改,用update()方法
            // 增
            /*String sql = "insert into t_user values(null, ?, ?)";
            int affectLine = queryRunner.update(sql, "wangwu", "abc123");*/

            // 删
            /*String sql = "delete from t_user where id = ?";
            int affectLine = queryRunner.update(sql, 6);*/

            // 改
            String sql = "update t_user set pwd = ? where id = ?";
            int affectLine = queryRunner.update(sql, "acb", 5);

            // 處理更新結果
            System.out.println(affectLine == 1 ? "更新成功" : "更新失敗");

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

    }
}
           

2、查詢

針對查詢操作

  • queryRunner.query(sql, rsh, params);

注意:接收類User的屬性字段名字,必須對應資料庫表中的列名,這樣底層才能自動封裝,否則值會為null。

測試代碼:

public class Test02 {
    public static void main(String[] args) {

        // 建立資料庫連接配接池對象
        ComboPooledDataSource dataSource = new ComboPooledDataSource();

        // 建立DbUtil操作對象,并綁定到連接配接池
        QueryRunner queryRunner = new QueryRunner(dataSource);

        try {
            /*// 執行sql語句
            String sql = "select id, username, pwd from t_user where id = ?";
            // 傳回一個使用者的資訊,手動封裝到user中
            User user = queryRunner.query(sql, new ResultSetHandler<User>() {
                @Override
                public User handle(ResultSet rs) throws SQLException {
                    User u = new User();

                    while (rs.next()) {
                        int id = rs.getInt("id");
                        String username = rs.getString("username");
                        String pwd = rs.getString("pwd");

                        u.setId(id);
                        u.setUsername(username);
                        u.setPwd(pwd);
                    }
                    return u;
                }
            }, 1);

            System.out.println(user);*/

            // ------------------------------------------------------------------------

            // 如果查詢結果集是1行資料,使用BeanHandler更簡單,不用手動去封裝
            /*String sql = "select * from t_user where id = ?";
            User user = queryRunner.query(sql, new BeanHandler<User>(User.class), 1);
            System.out.println(user);*/

            // 如果查詢結果集是多行資料,使用BeanListHandler,傳回一個list集合
            // 底層:通過位元組碼擷取執行個體對象
            String sql = "select * from t_user where username like ?";
            List<User> users = queryRunner.query(sql, new BeanListHandler<User>(User.class), "%a%");

            for (User u : users) {
                System.out.println(u);
            }

        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

    }
}
           

3、ResultSetHandler 常用的實作類

使用頻率最高的兩個:

  • BeanHandler, 查詢到的單個資料封裝成一個對象
  • BeanListHandler, 查詢到的多個資料封裝成一個List<對象>

以下其它的實作類也可能會用到:

  • ArrayHandler,查詢到的單行資料封裝成一個數組。
  • ArrayListHandler,查詢到的多行資料封裝成一個集合,集合裡面的元素是數組。
  • MapHandler,查詢到的單個資料封裝成一個map。
  • MapListHandler,查詢到的多個資料封裝成一個集合,集合裡面的元素是map。
  • ColumnListHandler
  • KeyedHandler
  • ScalarHandler

總結

1、事務

使用指令行示範

使用代碼示範

讀的問題:髒讀,不可重複讀,幻讀

寫的問題:丢失更新

悲觀鎖

樂觀鎖

4個隔離級别:讀未送出,讀已送出,可重複讀,可串行化。

2、開源資料庫連接配接池

DBCP

不使用配置

使用配置.properties檔案

C3P0

不使用配置

使用配置.xml檔案,檔案名固定(必須掌握)

自定義連接配接池

單例模式

裝飾者模式

3、DBUtils

簡化了我們的CRUD,裡面定義了通用的CRUD方法。

queryRunner.update(sql, params);

queryRunner.query(sql, rsh, params);

繼續閱讀