事務
在實際程序開發中,因業務的不同,總是會涉及到對數據庫的多次操作。例如:銀行轉賬!
張三轉10000塊到李四的賬戶,這其實需要兩條SQL語句:
l 給張三的賬戶減去10000元;
l 給李四的賬戶加上10000元。
如果在第一條SQL語句執行成功後,在執行第二條SQL語句之前,程序被中斷了(可能是拋出了某個異常,也可能是其他什麼原因),那麼李四的賬戶沒有加上10000元,而張三卻減去了10000元。這肯定是不行的!
所以事務就是保證對數據庫的多個操作,要麼完全成功,要麼完全失敗。
總結一句話不成功便成仁事務的四大特性(ACID)
l 原子性(Atomicity):事務中所有操作是不可再分割的原子單位。事務中所有操作要麼全部執行成功,要麼全部執行失敗。
l 一致性(Consistency):事務執行後,數據庫狀態與其它業務規則保持一致。如轉賬業務,無論事務執行成功與否,參與轉賬的兩個賬號餘額之和應該是不變的。
l 隔離性(Isolation):隔離性是指在併發操作中,不同事務之間應該隔離開來,使每個併發中的事務不會相互干擾。
l 持久性(Durability):一旦事務提交成功,事務中所有的數據操作都必須被持久化到數據庫中,即使提交事務後,數據庫馬上崩潰,在數據庫重啓時,也必須能保證通過某種機制恢復數據。
Mysql中的事務:
在默認情況下,MySQL每執行一條SQL語句,都是一個單獨的事務。如果需要在一個事務中包含多條SQL語句,那麼需要開啓事務和結束事務。
l 開啓事務:starttransaction;
l 結束事務:commit(提交)或rollback(回滾)。
下面演示zs給li轉賬10000元的示例:
START TRANSACTION;
UPDATE account SET balance=balance-10000WHERE id=1;
UPDATE account SET balance=balance+10000WHERE id=2;
ROLLBACK------------------------------------------------------------------------------------
START TRANSACTION;
UPDATE account SET balance=balance-10000WHERE id=1;
UPDATE account SET balance=balance+10000WHERE id=2;
COMMIT執行結果:提交結束,事務執行成功
------------------------------------------------------------------------------------
START TRANSACTION;
UPDATE account SET balance=balance-10000WHERE id=1;
UPDATE account SET balance=balance+10000WHERE id=2;
quit執行結果:退出,mysql會自動回滾。
----------------------------------------------------------
jdbc中的事務
Connection有三個與事務相關的方法:
- setAutoCommit(bookean) :設置是否爲自動提交事務,如果true(默認值就是true)表示自動提交,也就是每條執行的SQL語句都是一個單獨的事務,如果設置false,那麼就相當於開啓了事務了;
- commit() :提交結束事務
- rollback() : 回滾結束事務
public void transfer(boolean b) {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = JdbcUtils.getConnection();
//手動提交,開啓事務
con.setAutoCommit(false);
String sql = "update account set balance=balance+? where id=?";
pstmt = con.prepareStatement(sql);
//操作
pstmt.setDouble(1, -10000);
pstmt.setInt(2, 1);
pstmt.executeUpdate();
// 在兩個操作中拋出異常
if(b) {
throw new Exception();
}
pstmt.setDouble(1, 10000);
pstmt.setInt(2, 2);
pstmt.executeUpdate();
//提交事務
con.commit();
} catch(Exception e) {
//回滾事務
if(con != null) {
try {
con.rollback();
} catch(SQLException ex) {}
}
throw new RuntimeException(e);
} finally {
//關閉
JdbcUtils.close(con, pstmt);
}
}
jdbc中的保存點
保存點是JDBC3.0的東西!當要求數據庫服務器支持保存點方式的回滾。可以通過boolean b = con.getMetaData().supportsSavepoints(); 這個方法進行效驗。
保存點的作用是允許事務回滾到指定的保存點位置。在事務中設置好保存點,然後回滾時可以選擇回滾到指定的保存點,而不是回滾整個事務!
注意,回滾到指定保存點並沒有結束事務!!!只有回滾了整個事務纔算是結束事務了!
Connection 類設置保存點,以及回滾到指定保存點的方法:- 設置保存點 :Savepoint.setSavepoint();//使用到Savepoint
- 回滾到指定保存點 :voidrollback(Savepoint);
/*
* 李四對張三說,如果你給我轉1W,我就給你轉100W。
* ==========================================
*
* 張三給李四轉1W(張三減去1W,李四加上1W)
* 設置保存點!
* 李四給張三轉100W(李四減去100W,張三加上100W)
* 查看李四餘額爲負數,那麼回滾到保存點。
* 提交事務
*/
@Test
public void fun() {
Connection con = null;
PreparedStatement pstmt = null;
try {
con = JdbcUtils.getConnection();
//手動提交
con.setAutoCommit(false);
String sql = "update account set balance=balance+? where name=?";
pstmt = con.prepareStatement(sql);
//操作1(張三減去1W)
pstmt.setDouble(1, -10000);
pstmt.setString(2, "zs");
pstmt.executeUpdate();
//操作2(李四加上1W)
pstmt.setDouble(1, 10000);
pstmt.setString(2, "ls");
pstmt.executeUpdate();
// 設置保存點
Savepoint sp = con.setSavepoint();
//操作3(李四減去100W)
pstmt.setDouble(1, -1000000);
pstmt.setString(2, "ls");
pstmt.executeUpdate();
//操作4(張三加上100W)
pstmt.setDouble(1, 1000000);
pstmt.setString(2, "zs");
pstmt.executeUpdate();
//操作5(查看李四餘額)
sql = "select balance from account where name=?";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, "ls");
ResultSet rs = pstmt.executeQuery();
rs.next();
double balance = rs.getDouble(1);
<span style="white-space:pre"> </span>//如果李四餘額爲負數,那麼回滾到指定保存點
if(balance < 0) {
con.rollback(sp);<span style="white-space:pre"> </span>//發現李四餘額小於0,回滾到指定還原點!即撤銷了李四給張三轉賬100萬的操作
System.out.println("張三,你上當了!");
}
//提交事務
con.commit();<span style="white-space:pre"> </span>//注意,一定要提交事務,因爲回滾到指定保存點不會結束事務!保存點之前的操作沒有被回滾,只能提交了才能真正把沒有回滾的操作執行了。
} catch(Exception e) {
//回滾事務
if(con != null) {
try {
con.rollback();
} catch(SQLException ex) {}
}
throw new RuntimeException(e);
} finally {
//關閉
JdbcUtils.close(con, pstmt);
}
}
事務的隔離級別
我也沒搞清楚。我覺得沒有必要死記。在這就說說我的理解:我認爲就是控制併發事務問題的東東。
五大併發事務問題:兩類更新,三類讀
1 髒讀:讀取到另一個事務未提交數據;
2 不可重複讀:兩次讀取不一致;
3 幻讀(虛讀):讀到另一事務已提交數據。
例子:
1 髒讀(dirty read):讀到未提交更新數據
時間 |
轉賬事務A |
取款事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶餘額爲1000元 |
T4 |
|
取出500元把餘額改爲500元 |
T5 |
查看賬戶餘額爲500元(髒讀) |
|
T6 |
|
撤銷事務,餘額恢復爲1000元 |
T7 |
匯入100元把餘額改爲600元 |
|
T8 |
提交事務 |
|
A事務查詢到了B事務未提交的更新數據,A事務依據這個查詢結果繼續執行相關操作。但是接着B事務撤銷了所做的更新,這會導致A事務操作的是髒數據。(這是絕對不允許出現的事情)
2 虛讀(幻讀)(phantom read):讀到已提交插入數據
時間 |
統計金額事務A |
轉賬事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
統計總存款數爲10000元 |
|
T4 |
|
新增一個存款賬戶,存款爲100元 |
T5 |
|
提交事務 |
T6 |
再次統計總存款數爲10100元 |
|
A事務第一次查詢時,沒有問題,第二次查詢時查到了B事務已提交的新插入數據,這導致兩次查詢結果不同。(在實際開發中,很少會對相同數據進行兩次查詢,所以可以考慮是否允許虛讀)
3 不可重複讀(unrepeatable read):讀到已提交更新數據
時間 |
取款事務A |
轉賬事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶餘額爲1000元 |
T4 |
查詢賬戶餘額爲1000元 |
|
T5 |
|
取出100元,把餘額改爲900元 |
T6 |
|
提交事務 |
T7 |
查詢賬戶餘額爲900元(與T4讀取的一不一致) |
|
不可重複讀與虛讀有些相似,都是兩次查詢的結果不同。後者是查詢到了另一個事務已提交的新插入數據,而前者是查詢到了另一個事務已提交的更新數據。
隔離級別 |
髒讀 |
不可重複讀 |
虛讀 |
第一類丟失更新 |
第二類丟失更新 |
READ UNCOMMITTED (讀未提交數據) |
允許 |
允許 |
允許 |
不允許 |
允許 |
READ COMMITTED (讀已提交數據) |
不允許 |
允許 |
允許 |
不允許 |
允許 |
REPEATABLE READ (可重複讀) |
不允許 |
不允許 |
允許 |
不允許 |
不允許 |
SERIALIZABLE (串行化) |
不允許 |
不允許 |
不允許 |
不允許 |
不允許 |
1 SERIALIZABLE(串行化)
當數據庫系統使用SERIALIZABLE隔離級別時,一個事務在執行過程中完全看不到其他事務對數據庫所做的更新。當兩個事務同時操作數據庫中相同數據時,如果第一個事務已經在訪問該數據,第二個事務只能停下來等待,必須等到第一個事務結束後才能恢復運行。因此這兩個事務實際上是串行化方式運行。
2 REPEATABLE READ(可重複讀)
當數據庫系統使用REPEATABLE READ隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,但是不能看到其他事務對已有記錄的更新。
3 READ COMMITTED(讀已提交數據)
當數據庫系統使用READ COMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務已經提交的新插入的記錄,而且還能看到其他事務已經提交的對已有記錄的更新。
4 READ UNCOMMITTED(讀未提交數據)
當數據庫系統使用READUNCOMMITTED隔離級別時,一個事務在執行過程中可以看到其他事務沒有提交的新插入的記錄,而且還能看到其他事務沒有提交的對已有記錄的更新。
MySQL的默認隔離級別爲REPEATABLE READ
連接池
可以理解爲用來管理connection的容器。將connection交予池來管理,這可以重複使用Connection。有了池,所以我們就不用自己來創建Connection,而是通過池來獲取Connection對象。
當使用完Connection後,調用Connection的close()方法也不會真的關閉Connection,而是把Connection“歸還”給池。池就可以再利用這個Connection對象了。
JDBC的數據庫連接池接口:DateSource
Java爲數據庫連接池提供了公共的接口:javax.sql.DataSource,各個廠商可以讓自己的連接池實現這個接口。這樣應用程序可以方便的切換不同廠商的連接池!
DBCP
DBCP是Apache提供的一款開源免費的數據庫連接池!
連接池類 BaseicDataSource
如何使用:
1.導包:commons-dbcp-1.4.jar 依賴 commons-pool-1.6.jar 別忘了驅動包(3個)
2.代碼:
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.dbcp.BasicDataSource;
import org.junit.Test;
public void fun1() throws SQLException {
BasicDataSource ds = new BasicDataSource();
ds.setUsername("root");
ds.setPassword("123");
ds.setUrl("jdbc:mysql://localhost:3306/mydb1");
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setMaxActive(20); <span style="white-space:pre"> </span>//最大連接數
ds.setMaxIdle(10);<span style="white-space:pre"> </span>//最大空閒數
ds.setInitialSize(10);<span style="white-space:pre"> </span>//初始化連接數
ds.setMinIdle(2);<span style="white-space:pre"> </span>//最小空閒數
ds.setMaxWait(1000);<span style="white-space:pre"> </span>//最大等待毫秒數
Connection con = ds.getConnection();
System.out.println(con.getClass().getName());
con.close();<span style="white-space:pre"> </span>//關閉連接只是把連接歸還給池,更多詳細設置請參考幫助文檔的BasicDateSorice類。
}
C3P0
C3P0也是開源免費的連接池!C3P0被很多人看好!
連接池類:ComboPooledDataSource
如何使用:1.導包:c3p0-0.9.2-pre1.jar 、 c3p0-oracle-thin-extras-0.9.2-pre1.jar 、mchange-commons-0.2.jar 和驅動
2.可以加入配置文件,也可以不加。如果使用配置文件,配置文件必須命名爲c3p0-config.xml 放在src下。
3.代碼
public void fun1() throws PropertyVetoException, SQLException {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb1");
ds.setUser("root");
ds.setPassword("123");
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setAcquireIncrement(5);<span style="white-space:pre"> </span>//每次增量
ds.setInitialPoolSize(20);<span style="white-space:pre"> </span>//初始化連接數
ds.setMinPoolSize(2);<span style="white-space:pre"> </span>//最少連接數
ds.setMaxPoolSize(50);<span style="white-space:pre"> </span>//最多連接數
Connection con = ds.getConnection();
System.out.println(con);
con.close();
}
使用配置文件。
配置文件c3p0-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<default-config><span style="white-space:pre"> </span><!-- 默認配置 -->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="acquireIncrement">3</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">2</property>
<property name="maxPoolSize">10</property>
</default-config>
<named-config name="oracle-config"><span style="white-space:pre"> </span><!-- 命名配置 -->
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mydb1</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="user">root</property>
<property name="password">123</property>
<property name="acquireIncrement">3</property>
<property name="initialPoolSize">10</property>
<property name="minPoolSize">2</property>
<property name="maxPoolSize">10</property>
</named-config>
</c3p0-config>
代碼
public void fun2() throws PropertyVetoException, SQLException {
ComboPooledDataSource ds = new ComboPooledDataSource(); <span style="white-space:pre"> </span>//不用定配置文件名稱,因爲配置文件名必須是c3p0-config.xml,這裏使用的是默認配置。
Connection con = ds.getConnection();
System.out.println(con);
con.close();
}
public void fun2() throws PropertyVetoException, SQLException {
ComboPooledDataSource ds = new ComboPooledDataSource("orcale-config") ;<span style="white-space:pre"> </span>//使用命名爲orcale-config的配置
Connection con = ds.getConnection();
System.out.println(con);
con.close();
}
DBUtils
是Apache Commons組件的項目,開源免費。對jdbc進行簡單的封裝。
主要類:
DbUitls ---- 都是靜態方法,一系列的close()。
QueryRunner ---- 增刪改查都靠它
update() : 執行insert update delete
query() :執行select 語句
batch() :執行批處理
ResultSetHandler ---- 結果集處理接口,其提供的實現類可以把結果集轉換成不同類型。
- MapHandler ---- 單行處理器!把結果集轉換成Map<String,Object>,其中列名爲鍵
- MapListHandler ---- 多行處理器!把結果集轉換成List<Map<String,Object>>
- BeanHandler ---- 單行處理器!把結果集轉換成Bean,該處理器需要Class參數,即Bean的類型
- BeanListHandler ---- 多行處理器!把結果集轉換成List<Bean>
- ColumnListHandler ---- 多行單列處理器!把結果集轉換成List<Object>,使用ColumnListHandler時需要指定某一列的名稱或編號,例如:new ColumListHandler(“name”)表示把name列的數據放到List中。
- ScalarHandler ---- 單行單列處理器!把結果集轉換成Object。一般用於聚集查詢,例如select count(*) from tab_student。
詳細請看DBUtils的API,在DBUtils包的apidocs文件夾下!!!!
如何使用:
1.導包:commons-dbutils-1.6.jar 依賴 commons-logging-1.2.jar 照舊驅動包
2.代碼
基本使用:
<span style="white-space:pre"> </span>@Test
public void fun1() throws SQLException {
QueryRunner qr = new QueryRunner();
String sql = "insert into user values(?,?,?)";
qr.update(JdbcUtils.getConnection(), sql, "u1", "zhangSan", "123");
}
@Test
public void fun2() throws SQLException {
QueryRunner qr = new QueryRunner(JdbcUtils.getDataSource());//這種方式在創建QueryRunner時傳遞池對象,那麼在調用update()時不用在傳遞connection了。
String sql = "insert into user values(?,?,?)";
qr.update(sql, "u1", "zhangSan", "123");
}
</pre><pre>
QueryRunner之查詢
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.junit.Test;
public class JdbcDemo {
@Test
public void fun1() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select * from tab_student where number=?";
Map<String,Object> map = qr.query(sql, new MapHandler(), "S_2000");<span style="white-space:pre"> </span>//把一行記錄轉換成一個Map,其中鍵爲列名稱,值爲列值
System.out.println(map);
}
@Test
public void fun2() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select * from tab_student";
List<Map<String,Object>> list = qr.query(sql, new MapListHandler());<span style="white-space:pre"> </span>//把轉換集轉換成List<Map>,其中每個Map對應一行記錄
for(Map<String,Object> map : list) {
System.out.println(map);
}
}
@Test
public void fun3() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select * from tab_student where number=?";
Student stu = qr.query(sql, new BeanHandler<Student>(Student.class), "S_2000");<span style="white-space:pre"> </span>//把結果集轉換成一個Bean對象,在使用BeanHandler時需要指定Class,即Bean的類型
System.out.println(stu);
}
@Test
public void fun4() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select * from tab_student";
List<Student> list = qr.query(sql, new BeanListHandler<Student>(Student.class));<span style="white-space:pre"> </span>//把結果集轉換成List<Bean>,其中每個Bean對應一行記錄
for(Student stu : list) {
System.out.println(stu);
}
}
@Test
public void fun5() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select * from tab_student";
List<Object> list = qr.query(sql, new ColumnListHandler("name"));<span style="white-space:pre"> </span>//多行單例處理器,即獲取name列數據
for(Object s : list) {
System.out.println(s);
}
}
@Test
public void fun6() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "select count(*) from tab_student";
Number number = (Number)qr.query(sql, new ScalarHandler());<span style="white-space:pre"> </span>//單行單列處理器,一般用於聚合查詢,在使用ScalarHandler時可以指定列名,如果不指定,默認爲第1列。
<span style="white-space:pre"> </span>//對聚合函數的查詢結果,有的驅動返回的是Long,有的返回的是BigInteger,所以這裏我們把它轉換成Number,Number是Long和BigInteger的父類!然後我們再調用Number的intValue()或longValue()方法就OK了。
int cnt = number.intValue();<span style="white-space:pre"> </span>
System.out.println(cnt);
}
}
QueryRunner之批處理
@Test
public void fun10() throws SQLException {
DataSource ds = JdbcUtils.getDataSource();
QueryRunner qr = new QueryRunner(ds);
String sql = "insert into tab_student values(?,?,?,?)";
Object[][] params = new Object[10][];<span style="white-space:pre"> </span>//表示 要插入10行記錄,注意,這裏是二維數組,這個二維數組有10個一維數組。
for(int i = 0; i < params.length; i++) {
params[i] = new Object[]{"S_300" + i, "name" + i, 30 + i, i%2==0?"男":"女"};
}
qr.batch(sql, params);<span style="white-space:pre"> </span>//執行批處理