主要內(nèi)容:
- 事務(wù)
- 使用數(shù)據(jù)庫連接池優(yōu)化程序性能
一蛋褥、事務(wù)的概念
事務(wù)指邏輯上的一組操作彼宠,組成這組操作的各個(gè)單元,要不全部成功诗力,要不全部不成功凰浮。數(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í)候這種方式并不太好惫周,我們可以自己設(shè)置回滾點(diǎn),當(dāng)我們sql語句出錯(cuò)時(shí)可以回到設(shè)置的那個(gè)點(diǎn)處的狀態(tài)康栈。而rollback可以每次回滾一條語句。
二喷橙、使用事務(wù)
- 當(dāng)jdbc程序向數(shù)據(jù)庫獲得一個(gè)Connection對象時(shí)啥么,默認(rèn)情況下這個(gè)Connection對象會(huì)自動(dòng)向數(shù)據(jù)庫提交在它前面發(fā)送的sql語句。若向關(guān)閉這種默認(rèn)提交方式贰逾,讓多條sql在一個(gè)事務(wù)中執(zhí)行悬荣,可使用下列語句:
jdbc控制事務(wù)語句
connection.setAutoCommit(false); start transaction
connection.rollback(); rollback
connection.commit(); commit
設(shè)置事務(wù)回滾點(diǎn)
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語句不會(huì)執(zhí)行
ps = conn.prepareStatement(sql2);
ps.executeUpdate();
System.out.println("ok");
} catch (Exception e) {
try {
conn.rollback();//手動(dòng)通知數(shù)據(jù)庫手動(dòng)回滾
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, ps, result);
}
}
}
例:設(shè)置回滾點(diǎn)
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è)置回滾點(diǎn)
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);//手動(dòng)通知回滾疙剑,同時(shí)指定回滾點(diǎn)
//回滾之后記得提交氯迂,上面我們回滾了践叠,就表明最后的提交語句沒有執(zhí)行,那此時(shí)如果不提交
//嚼蚀,數(shù)據(jù)庫在沒有收到提交的情況下禁灼,會(huì)自動(dòng)回滾所有的sql語句
conn.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, ps, result);
}
}
}
說明:這里如果中間出現(xiàn)異常,則只有第一條語句生效轿曙。
三弄捕、事務(wù)的特性(ACID)
原子性(Atomicity)
原子性是指事務(wù)是一個(gè)不可分割的工作單位,事務(wù)中的操作要么都是執(zhí)行成功导帝,要么都失敗守谓。一致性(Consistency)
事務(wù)必須使數(shù)據(jù)庫從一個(gè)一致性狀態(tài)變換到另外一個(gè)一致性狀態(tài)。比如您单,在轉(zhuǎn)賬中賬戶的總額是不變的斋荞。隔離性(Isolation)
事務(wù)的隔離性是多個(gè)用戶并發(fā)訪問數(shù)據(jù)庫時(shí),數(shù)據(jù)庫為每一個(gè)用戶開啟的事務(wù)虐秦,不能被其他事務(wù)的操作數(shù)據(jù)所干擾平酿,多個(gè)并發(fā)事務(wù)之間要相互隔離。持久性(Durability)
持久性是指一個(gè)事務(wù)一旦被提交羡疗,它對數(shù)據(jù)庫中數(shù)據(jù)的改變就是永久性的染服,接下來即使數(shù)據(jù)庫發(fā)生故障也不應(yīng)該對其有任何影響。
四叨恨、 事務(wù)的隔離級別
多個(gè)線程開啟各自事務(wù)操作數(shù)據(jù)庫中數(shù)據(jù)時(shí)柳刮,數(shù)據(jù)庫系統(tǒng)要負(fù)責(zé)隔離操作,以保證各個(gè)線程在獲取數(shù)據(jù)時(shí)的準(zhǔn)確性痒钝。
如果不考慮隔離性秉颗,可能會(huì)引發(fā)如下問題:
臟讀
指一個(gè)事務(wù)讀取了另外一個(gè)事務(wù)未提交的數(shù)據(jù)。
例如:a花錢讓b給辦點(diǎn)事送矩,a向b轉(zhuǎn)賬之后但未提交蚕甥,但是b此時(shí)會(huì)發(fā)現(xiàn)賬戶多了錢,然后將事辦完之后a卻不提交栋荸,此時(shí)b的賬戶的錢就會(huì)變回原來的數(shù)目菇怀,相當(dāng)于白干活了。不可重復(fù)讀
在一個(gè)事務(wù)內(nèi)讀取表中的某一行數(shù)據(jù)晌块,多次讀取的結(jié)果不同爱沟。
如a開啟一個(gè)事務(wù)后,查詢余額為200匆背,此時(shí)b轉(zhuǎn)賬100呼伸,那么a此時(shí)查詢就是300,兩次結(jié)果不一致钝尸。當(dāng)然有些時(shí)候這樣是正確的括享,但是有時(shí)候卻不是搂根,如在統(tǒng)計(jì)時(shí)我們不能讓多次的統(tǒng)計(jì)結(jié)果不一致。
和臟讀的區(qū)別:臟讀是讀取前一事務(wù)未提交的數(shù)據(jù)铃辖,不可重復(fù)讀是重新讀取了前一事務(wù)已經(jīng)提交的數(shù)據(jù)剩愧。虛讀(幻讀)
是指在一個(gè)事務(wù)內(nèi)讀取到了別的事務(wù)插入的數(shù)據(jù),導(dǎo)致前后讀取不一致澳叉。
比如一個(gè)表第一次查詢有2條數(shù)據(jù)隙咸,此時(shí)另外一個(gè)事務(wù)插入了一條數(shù)據(jù),此時(shí)再次查詢就變成了3條數(shù)據(jù)成洗,兩次查詢結(jié)果不一致五督。
和不可重復(fù)讀的區(qū)別:不可重復(fù)讀是讀取到的數(shù)據(jù)結(jié)果不同,而虛讀是指讀取到多個(gè)事務(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();
//查詢程序肯定至少要到這個(gè)級別
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)用程序直接獲取連接的缺點(diǎn):用戶每次都需要向數(shù)據(jù)庫獲得鏈接,而數(shù)據(jù)庫創(chuàng)建連接通常需要消耗相對較大的資源舔株,創(chuàng)建時(shí)間也較長莺琳。
這時(shí)我們可以使用數(shù)據(jù)庫連接池優(yōu)化程序性能:
編寫連接池需要實(shí)現(xiàn)
java.sql.DataSource
接口。DataSource接口中定義了兩個(gè)重載的getConnection
方法:
Connection getConnection()
Connection getConnection(String username,String password)
實(shí)現(xiàn)
DataSource
接口载慈,并實(shí)現(xiàn)連接池功能的步驟:
1.在DataSource
構(gòu)造函數(shù)中批量創(chuàng)建與數(shù)據(jù)庫的連接惭等,并把創(chuàng)建的連接加入LinkedList
對象中。
2.實(shí)現(xiàn)getConnection
方法办铡,讓getConnection
方法每次調(diào)用時(shí)辞做,從LinkedList
中取一個(gè)Connection
返回給用戶。
3.當(dāng)用戶使用完Connection寡具,調(diào)用Connection.close()
方法時(shí)秤茅,Collection
對象應(yīng)保證將自己返回到LinkedList
中,而不要把Collection
還給數(shù)據(jù)庫。
Collection保證將自己返回到LinkedList中是此處編程的難點(diǎn)晒杈。
示例:模擬數(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ù)庫正忙");
}
}
其他需要實(shí)現(xiàn)的方法我們并不關(guān)心孔厉,這里省略
}
說明:
這里有個(gè)問題是如果調(diào)用者使用完Connection鏈接之后調(diào)用方法
conn.close();
那么此鏈接將不會(huì)返回給數(shù)據(jù)庫連接池拯钻,而是直接返回給了數(shù)據(jù)庫帖努,這樣鏈接會(huì)用一個(gè)少一個(gè),顯然不行粪般。也就是說不能這樣拼余,或者說close方法不夠用,我們需要增強(qiáng)一下亩歹,讓其不要返還給數(shù)據(jù)庫匙监,而是返還給連接池。對于類的某個(gè)方法不能達(dá)到我們的要求時(shí)需要對其進(jìn)行增強(qiáng)小作,而增強(qiáng)的方式有三種:1.寫一個(gè)子類亭姥,覆蓋其close方法;2.寫一個(gè)Connection的包裝類顾稀,增強(qiáng)close方法达罗;3.使用動(dòng)態(tài)代理,返回一個(gè)代理對象出去静秆,攔截close方法的調(diào)用粮揉,達(dá)到對close方法增強(qiáng)的功能。
第一種不行抚笔,因?yàn)槲覀冊谝祷谻onnection的時(shí)候?qū)ο笾幸呀?jīng)封裝了相關(guān)信息扶认,即使我們寫一個(gè)子類也僅僅表明此子類有和父類相同的功能,但是卻沒有父類中已經(jīng)封裝好了的信息殊橙,不能對數(shù)據(jù)庫進(jìn)行操作辐宾。
第二種:寫一個(gè)包裝類。
/*
* 用包裝設(shè)計(jì)模式對某個(gè)對象進(jìn)行增強(qiáng)步驟:
* 1蛀柴、寫一個(gè)類螃概,實(shí)現(xiàn)與被增強(qiáng)對象(這里要增強(qiáng)的對象是mysql的連接對象connection)相同的接口(這里的接口是Connection)
* 2.定義一個(gè)變量,指向被增強(qiáng)對象
* 3鸽疾、定義一個(gè)構(gòu)造方法吊洼,接收被增強(qiáng)對象(也就是將我們要增強(qiáng)的對象傳遞進(jìn)來進(jìn)行增強(qiáng))
* 4、覆蓋想增強(qiáng)的方法(這里是close方法)制肮、
* 5冒窍、對于不想增強(qiáng)的方法,直接調(diào)用被增強(qiáng)對象的方法豺鼻,如this.conn.unwrap(iface)
* */
class MyConnection implements Connection{
private Connection conn ;
private List pool;//這里我們需要將數(shù)據(jù)庫連接池傳遞進(jìn)來综液,因?yàn)橹笪覀兪褂玫逆溄佣际窃鰪?qiáng)之后的鏈接
public MyConnection() {
}
public MyConnection(Connection conn , List pool){
this.conn = conn;
this.pool = pool;
}
//這里我們只是需要增強(qiáng)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);
}
其他方法和上面這個(gè)方法類似儒飒,此處省略
}
說明:之后返回就不是返回Connection對象了谬莹,而是return new MyConnection(conn, list);
,但是顯然我們可以看到這種方式太麻煩,因?yàn)槠渲械姆椒ㄌ唷?/p>
- 第三種:使用動(dòng)態(tài)代理
@Override
public Connection getConnection() throws SQLException {
if(list.size() > 0){
//如果連接池中還有Connection連接則從池中刪除并返回給調(diào)用者
final Connection conn = list.removeFirst();
//第一個(gè)參數(shù)指的是使用哪個(gè)類裝載器附帽,第二個(gè)參數(shù)指明我們要對那個(gè)對象進(jìn)行增前埠戳,第三個(gè)參數(shù)指明增強(qiáng)對象完成什么功能
return (Connection) Proxy.newProxyInstance(JdbcPool.class.getClassLoader(), conn.getClass().getInterfaces(),
new InvocationHandler() {
//使用動(dòng)態(tài)代理之后其實(shí)不管之后我們調(diào)用Connection的什么方法(commit、rollback...)其實(shí)都是調(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ù)庫正忙");
}
}
說明:其實(shí)動(dòng)態(tài)代理是使用的攔截技術(shù),這里我們不詳細(xì)講喳钟,在后面將過濾器會(huì)詳細(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;
}
}
}