Author:相忠良
Email: ugoood@163.com
起始於:June 8, 2018
最後更新日期:June 11, 2018java
聲明:本筆記依據傳智播客方立勳老師 Java Web 的授課視頻內容記錄而成,中間加入了本身的理解。本筆記目的是強化本身學習所用。如有疏漏或不當之處,請在評論區指出。謝謝。
涉及的圖片,文檔寫完後,一次性更新。mysql
發sql時,把多個sql放在Start transaction
和commit
之間便可。spring
試驗準備:sql
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);
如今,a向b轉帳100元,操做以下:數據庫
start transaction; update account set money=money-100 where name='aaa';
關掉鏈接,從新登陸數據庫查看,aaa 帳戶的 money 仍是 1000。
只有下面這樣才行:apache
start transaction; update account set money=money-100 where name='aaa'; update account set money=money+100 where name='bbb'; commit;
執行到 commit,上面2條sql纔算真正執行,而不是回滾,這就是事務(控制多條sql做爲總體執行)。編程
rollback
能夠手動回滾,而不是異常時,事務在數據庫中自動回滾。設計模式
當Jdbc程序向數據庫得到一個Connection對象時,默認狀況下這個Connection對象會自動向數據庫提交在它上面發送的SQL語句。若想關閉這種默認提交方式,讓多條SQL在一個事務中執行,可以使用下列語句:
JDBC控制事務語句:服務器
Connection.setAutoCommit(false);
至關於 start transactionConnection.rollback();
rollbackConnection.commit();
commit程序中控制事務的例子以下:併發
public class Demo1 { /** a--->b 100 */ public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try{ conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); //start transaction; String sql1 = "update account set money=money-100 where name='aaa'"; String sql2 = "update account set money=money+100 where name='bbb'"; st = conn.prepareStatement(sql1); st.executeUpdate(); int x = 1/0; // <-- 產生異常 st = conn.prepareStatement(sql2); st.executeUpdate(); conn.commit(); // commit }finally{ JdbcUtils.release(conn, st, rs); } } }
手動回滾,按下面例子,只想從第二條sql開始回滾,方法就是:
Savepoint
;例子以下:
public class Demo2 { public static void main(String[] args) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; // 回滾點對象 try { conn = JdbcUtils.getConnection(); conn.setAutoCommit(false); // start transaction; 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'"; st = conn.prepareStatement(sql1); st.executeUpdate(); sp = conn.setSavepoint(); // <-- 2. 設置回滾點 st = conn.prepareStatement(sql2); st.executeUpdate(); int x = 1 / 0; // <-- 1. 產生異常 st = conn.prepareStatement(sql3); st.executeUpdate(); conn.commit(); // commit } catch (Exception e) { e.printStackTrace(); conn.rollback(sp); // <-- 3. 回滾 conn.commit(); // <-- 4. 手動回滾後,必定要記得提交事務 } finally { JdbcUtils.release(conn, st, rs); } } }
若一個數據庫號稱支持事務,那它必然支持 ACID;反過來講,若某數據庫支持 ACID,那這個數據庫也是支持事務的。
髒讀:指一個事務讀取了另一個事務未提交的數據。(最危險)
故事:這是很是危險的,假設 A 向 B 轉賬 100 元,對應 sql 語句以下所示:
update account set money=money+100 while name='b';
update account set money=money-100 while name='a';
當第 1 條 sql 執行完,第 2 條還沒執行(A 未提交時),若是此時 B 查詢本身的賬戶,就會發現本身多了 100 元錢。若是 A 等 B 走後再回滾,B 就會損失 100 元。
下面介紹的不可重複讀和幻讀,有些狀況下是沒問題的,但有時會有問題。
不可重複讀:在一個事務內讀取表中的某一行數據,屢次讀取結果不一樣。 也指讀表中同一條數據,結果不一樣。
故事:中國人民銀行生成開啓生成報表這個事務,報送克強總理1000億RMB,在報送近平主席前,生成報表這個事務未結束期間,有客戶存了200億RMB並該客戶完成了他的事務,如今又生成近平主席的報表顯示爲1200億。問題出現了:兩位領導要打架的。困惑就是:哪次查詢時是準確的呢? 這就是不可重複讀所產生的問題。
虛讀(幻讀):是指在一個事務內讀取到了別的事務插入的數據,致使先後讀取不一致。 也指所讀的表的記錄數在變化。
故事:人口普查系統正生成報表,開啓了一個事務。該系統在這個事務中需生成多個報表。可能發生這樣的事:生成第一個報表,顯示中國有10億人,但生成第二個報表期間,有人往數據庫中插入了數據,統計結果顯示有11億人。困惑來了:到底以哪一個爲準呢?這就是幻讀產生的問題。
根據上節介紹的,若無隔離性,數據庫可能出現的三種問題,針對問題的解決,提出了事務隔離級別。隔離級別的提出,主要在解決問題的基礎上,儘量的不過多損失數據庫性能。
數據庫共定義了四種隔離級別:
事務隔離性的設置語句:
set transaction isolation level
設置事務隔離級別select @@tx_isolation
查詢當前事務隔離級別方立勳老師開啓了2個mysql客戶端,進行了模擬。模擬過程這裏不表述了。
編程序時,得到的 connection:
編程中,用JDBC設置隔離級別: conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
示例代碼:
public class Demo3 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; Savepoint sp = null; try{ conn = JdbcUtils.getConnection(); //mysql repeatable read conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); conn.setAutoCommit(false); //start transaction; String sql = "select * from account"; conn.prepareStatement(sql).executeQuery(); Thread.sleep(1000*20); conn.commit(); }finally{ JdbcUtils_DBCP.release(conn, st, rs); } } }
下圖展現了無數據庫鏈接池時的缺點:
下圖是有鏈接池的情形:
有鏈接池後,數據庫就沒必要爲每一個用戶建立鏈接,而僅僅在一開始生成一些鏈接(假如20個),並將這些鏈接放入鏈接池,其餘用戶只從池中拿鏈接,用完後還到池中。(這個故事主要考慮,數據庫本身建立1個鏈接需消耗不少資源,10萬用戶申請,就建立10萬次鏈接,數據庫自己作本職工做就很繁忙,再去頻繁地建立若此多的連接,數據庫極有可能被累死!咱們要作的是儘可能減輕數據庫服務器的負擔。)
故事:
咱們但願執行conn.close();
時,鏈接還回鏈接池,但事實是conn是mysql提供的連接,執行close方法時,那個鏈接將還給mysql,而不是鏈接池。
當發現對象的方法不夠咱們用時,咱們需加強那個方法。辦法有:
一般子類的方式不可行,緣由是很難將父類對象信息導入子類對象中,除非父類對象封裝的信息極少。
包裝設計模式步驟(我本身的經驗,想象一下BufferedReader
的用法,就是用構造函數接收被包裝對象):
包裝模式例子:
class MyConnection implements Connection{ // step 1 private Connection conn; // step 2 public MyConnection(Connection conn){ // step 3 this.conn = conn; } public void close(){ // step 4 list.add(this.conn); } // step 5 @Override public void commit() throws SQLException{ this.conn.commit(); // 調用的是 mysql 提供的 commit 方法 } @Override public void clearWarnings() throws SQLException{ this.conn.clearWarnings(); // 調用的是 mysql 提供的 clearWarnings 方法 } /* ... ... 後面不想加強的方法均照 step 5 處理,極有可能代碼量超大,這也是包裝模式處理此類問題的缺點 */ }
使用經包裝(裝飾)後的conn對象:
MyConnection my = new MyConnection(conn);
當咱們用my
這個連接對象時,它的close方法就是咱們本身寫的方法了。
下面代碼時動態代理方式(這裏僅作個記錄):
proxyConn = (Connection) Proxy.newProxyInstance(this.getClass() .getClassLoader(), conn.getClass().getInterfaces(), new InvocationHandler() { // 此處爲內部類,當close方法被調用時將conn還回池中,其它方法直接執行 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("close")) { pool.addLast(conn); return null; } return method.invoke(conn, args); } });
數據源 = 數據庫鏈接池
常見開源數據庫鏈接池有:
若想用 Apache DBCP,應用程序應增長以下 2 個 jar 文件:
下面是 dbcp-1.2.2 開發包中的 dbcpconfig.properties文件(實驗時,需將該文件 copy 到 src 目錄下),其做用同之前咱們本身寫的 db.properties 同樣,是存放配置 dbcp 鏈接哪一種數據庫、url、用戶名、密碼等信息的一種配置文件。以下:
#鏈接設置 driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/jdbc username=root password= #<!-- 初始化鏈接 --> initialSize=10 #最大鏈接數量 maxActive=50 #<!-- 最大空閒鏈接 --> maxIdle=20 #<!-- 最小空閒鏈接 --> minIdle=5 #<!-- 超時等待時間以毫秒爲單位 6000毫秒/1000等於60秒 --> maxWait=60000 #JDBC驅動創建鏈接時附帶的鏈接屬性屬性的格式必須爲這樣:[屬性名=property;] #注意:"user" 與 "password" 兩個屬性會被明確地傳遞,所以這裏不須要包含他們。 connectionProperties=useUnicode=true;characterEncoding=utf8 #指定由鏈接池所建立的鏈接的自動提交(auto-commit)狀態。 defaultAutoCommit=true #driver default 指定由鏈接池所建立的鏈接的只讀(read-only)狀態。 #若是沒有設置該值,則「setReadOnly」方法將不被調用。(某些驅動並不支持只讀模式,如:Informix) defaultReadOnly= #driver default 指定由鏈接池所建立的鏈接的事務級別(TransactionIsolation)。 #可用值爲下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE defaultTransactionIsolation=READ_COMMITTED
從新設置 JdbcUtils.java,用鏈接池的方式:
package cn.wk.utils; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class JdbcUtils_DBCP { private static DataSource ds = null; static { try { // 讀配置文件 dbcpconfig.properties InputStream in = JdbcUtils_DBCP.class.getClassLoader() .getResourceAsStream("dbcpconfig.properties"); Properties prop = new Properties(); prop.load(in); BasicDataSourceFactory factory = new BasicDataSourceFactory(); ds = factory.createDataSource(prop); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 異常轉換成錯誤 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); // dbcp conn.close() commit() } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代碼 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }
C3P0 的jar包在c3p0-0.9.2-pre1
中,導入以下2個jar包:
C3P0數據源配置文件名爲c3p0-config.xml
,可放在src目錄下,C3P0本身會找到它。
c3p0-config.xml
例子以下:
<c3p0-config> <default-config> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/day16</property> <property name="user">root</property> <property name="password">root</property> <property name="initialPoolSize">10</property> <property name="maxIdleTime">30</property> <property name="maxPoolSize">20</property> <property name="minPoolSize">5</property> <property name="maxStatements">200</property> </default-config> <named-config name="mysql"> <property name="acquireIncrement">50</property> <property name="initialPoolSize">100</property> <property name="minPoolSize">50</property> <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching --> <property name="maxStatements">0</property> <property name="maxStatementsPerConnection">5</property> </named-config> <named-config name="oracle"> <property name="acquireIncrement">50</property> <property name="initialPoolSize">100</property> <property name="minPoolSize">50</property> <property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching --> <property name="maxStatements">0</property> <property name="maxStatementsPerConnection">5</property> </named-config> </c3p0-config>
最上面的<default-config>
是默認配置,使用方法以下:
ComboPooledDataSource ds = new ComboPooledDataSource();
若想用<named-config name="oracle">
的配置,使用方法以下:
ComboPooledDataSource ds = new ComboPooledDataSource("oracle");
看起來很是方便。
完整的 C3P0 鏈接創建代碼JdbcUtils_C3P0
以下:
package cn.wk.utils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import com.mchange.v2.c3p0.ComboPooledDataSource; public class JdbcUtils_C3P0 { private static ComboPooledDataSource ds = null; static { try { ds = new ComboPooledDataSource(); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 異常轉換成錯誤 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代碼 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } }
測試代碼:
public class Demo4 { public static void main(String[] args) throws SQLException, InterruptedException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_C3P0.getConnection(); System.out.println(conn.getClass().getName()); } finally { JdbcUtils_C3P0.release(conn, st, rs); } } }
元數據:數據庫、表、列的定義信息。
Connection.getDatabaseMetaData()
DataBaseMetaData對象
ParameterMetaData對象,獲取 sql 語句參數的元數據。
以上2個元數據對象例子以下:
public class Demo5 { public static void main(String[] args) throws SQLException { Connection conn = JdbcUtils_C3P0.getConnection(); // 獲取數據庫的元數據 DatabaseMetaData meta = conn.getMetaData(); System.out.println(meta.getDatabaseProductName()); // 獲取參數元數據 String sql = "insert into user(id,name) values(?,?)"; PreparedStatement st = conn.prepareStatement(sql); ParameterMetaData para_meta = st.getParameterMetaData(); System.out.println(para_meta.getParameterCount()); System.out.println(para_meta.getParameterType(1)); // mysql不支持得到類型,拋異常 } }
ResultSetMetaData對象(重要,後面案例用到),結果集元數據:
準備:
模擬環境,先弄一個cn.wk.domain.Account
的javabean:
package cn.wk.domain; public class Account { private int id; private String name; private double money; public int getId() {return id;} public void setId(int id) {this.id = id;} public String getName() {return name;} public void setName(String name) {this.name = name;} public double getMoney() {return money;} public void setMoney(double money) {this.money = money;} }
dao 層方法大體代碼:
注意到:crud 變化的是 sql 和 st.set 其他代碼均相同
public void add(Account a) throws SQLException{ Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_DBCP.getConnection(); String sql = "(?,?,?)"; st.setInt(1, a.getId()); st.setString(2, a.getName()); st.setDouble(3, a.getMoney()); st.executeUpdate(); } finally { JdbcUtils_DBCP.release(conn, st, rs); } } public void delete(int id) throws SQLException{ Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = JdbcUtils_DBCP.getConnection(); String sql = "delete from where id=?"; st.setInt(1, id); st.executeUpdate(); } finally { JdbcUtils_DBCP.release(conn, st, rs); } }
如今要作優化,抽出相同的部分。
重寫了cn.wk.utils.JdbcUtils
,重點在該工具類的release方法的後面, 涉及到如下知識點:
本身 = 框架編寫者
package cn.wk.utils; import java.io.InputStream; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class JdbcUtils { private static DataSource ds = null; static { try { // 讀配置文件 dbcpconfig.properties InputStream in = JdbcUtils.class.getClassLoader() .getResourceAsStream("dbcpconfig.properties"); Properties prop = new Properties(); prop.load(in); BasicDataSourceFactory factory = new BasicDataSourceFactory(); ds = factory.createDataSource(prop); } catch (Exception e) { throw new ExceptionInInitializerError(e); // 異常轉換成錯誤 } } public static Connection getConnection() throws SQLException { return ds.getConnection(); // dbcp conn.close() commit() } public static void release(Connection conn, Statement st, ResultSet rs) { // 模板代碼 if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } rs = null; } if (st != null) { try { st.close(); } catch (Exception e) { e.printStackTrace(); } st = null; } if (conn != null) { try { conn.close(); } catch (Exception e) { e.printStackTrace(); } } } /* 抽取 增刪改 的公共代碼 */ // add delete update 都調用下面方法,變化的部分 sql , params // String sql="insert into account(id,name,money) values(?,?,?)"; // object[]{1,"aaa","1000"} public static void update(String sql, Object params[]) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = getConnection(); st = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) st.setObject(i + 1, params[i]); st.executeUpdate(); } finally { release(conn, st, rs); } } // 想替換掉全部 查詢 public static Object query(String sql, Object params[], ResultSetHandler handler) throws SQLException { Connection conn = null; PreparedStatement st = null; ResultSet rs = null; try { conn = getConnection(); st = conn.prepareStatement(sql); for (int i = 0; i < params.length; i++) st.setObject(i + 1, params[i]); rs = st.executeQuery(); // 接下來, 框架製做者不知道該怎樣處理 rs // 方法: 對外暴露個接口,讓調用者實現那個接口(handler),咱們用客戶所實現的接口處理 rs // 調用用戶傳來的 handler return handler.handler(rs); } finally { release(conn, st, rs); } } } // 設計一個接口,對外暴露 interface ResultSetHandler { public Object handler(ResultSet rs); // 讓用戶實現這個方法 } // 框架做者根據現實狀況,提早寫好一些處理器 class BeanHandler implements ResultSetHandler { // 不知道 bean 是啥, 就定義一個變量接收,且用構造函數提供對外訪問方式 private Class clazz; public BeanHandler(Class clazz) { this.clazz = clazz; } @Override public Object handler(ResultSet rs) { try { if (!rs.next()) return null; // 建立出要封裝結果集的 bean Object bean = this.clazz.newInstance(); // 經過元數據技術獲知 rs 裏有啥 ResultSetMetaData meta = rs.getMetaData(); int colNum = meta.getColumnCount(); for (int i = 0; i < colNum; i++) { String name = meta.getColumnName(i + 1); // 結果集每列列名 id Object value = rs.getObject(name); // 1 // 經過 name,反射出 bean 上與 name對應的屬性 Field f = bean.getClass().getDeclaredField(name); f.setAccessible(true); // 強制訪問私有元素 f.set(bean, value); } return bean; } catch (Exception e) { throw new RuntimeException(e); } } } // 返回包含 bean 的 list 集合 class BeanListHandler implements ResultSetHandler { private Class clazz; public BeanListHandler(Class clazz) { this.clazz = clazz; } @Override public Object handler(ResultSet rs) { List list = new ArrayList(); try { ResultSetMetaData meta = rs.getMetaData(); int count = meta.getColumnCount(); while (rs.next()) { Object bean = this.clazz.newInstance(); for (int i = 0; i < count; i++) { String name = meta.getColumnName(i + 1); Object value = rs.getObject(name); Field f = bean.getClass().getDeclaredField(name); // 反射獲取域 f.setAccessible(true); f.set(bean, value); } list.add(bean); } } catch (Exception e) { throw new RuntimeException(e); } return list; } }
模擬使用該框架的 dao 代碼:
package cn.wk.utils; import java.sql.SQLException; import org.junit.Test; import cn.wk.domain.Account; // 假設這是 Dao // 注意到:crud 變化的是 sql 和 st.set 其他代碼均相同 public class Demo7 { @Test public void test() throws SQLException { List<?> list = getAll(); System.out.println(list.size()); } public void add(Account a) throws SQLException { String sql = "insert into account(name,money) values(?,?)"; Object params[] = { a.getName(), a.getMoney() }; JdbcUtils.update(sql, params); } public void delete(int id) throws SQLException { String sql = "delete from account where id=?"; Object params[] = { id }; JdbcUtils.update(sql, params); } public void update(Account a) throws SQLException { String sql = "update account set name=?, money=? where id=?"; Object params[] = { a.getName(), a.getMoney(), a.getId() }; JdbcUtils.update(sql, params); } public Account find(int id) throws SQLException { String sql = "select * from account where id=?"; Object params[] = { id }; return (Account) JdbcUtils.query(sql, params, new BeanHandler( Account.class)); } public List getAll() throws SQLException { String sql = "select * from account"; Object params[] = {}; return (List) JdbcUtils.query(sql, params, new BeanListHandler( Account.class)); } }