一谁榜、什么是事務(wù)
邏輯上的一組操作, 侈沪,要不全部失敗,要不全部成功。
對(duì)數(shù)據(jù)庫(kù)的操作會(huì)先在一張臨時(shí)表上進(jìn)行绣张,一旦發(fā)生錯(cuò)誤纤掸,就回滾撤銷所有操作恰响。全部成功才存盤修改數(shù)據(jù)庫(kù)數(shù)據(jù)税肪。
MySql的事務(wù)管理
1. 啟動(dòng)事務(wù)(對(duì)數(shù)據(jù)的操作在臨時(shí)表中進(jìn)行)
startTransaction
2. 提交,存盤
commit
3. 回滾,撤銷
rollBack
- 在事務(wù)管理中執(zhí)行sql,使用數(shù)據(jù)庫(kù)內(nèi)臨時(shí)表保存负乡,在沒有進(jìn)行事務(wù)提交或者回滾之前牛郑,其它用戶無(wú)法看到事務(wù)操作的結(jié)果
- SQL語(yǔ)言中只有DML才能被事務(wù)管理(insert/update/delete)
JDBC的事務(wù)
1. 啟動(dòng)事務(wù)管理
//首先獲取連接。(以C3P0為基礎(chǔ)準(zhǔn)備一個(gè)JDBCUtils工具類,提供獲取連接池和連接的方法)
Connection connection = JDBCUtils.getConnection();
//啟動(dòng)事務(wù)
connection.setAutoCommit(false);
2. 提交,存盤
connection.commit();
3. 回滾敬鬓,撤銷
connection.rollBack();
DBUtils的事務(wù)管理
1. 啟動(dòng)事務(wù)
//創(chuàng)建QuueryRunner對(duì)象,使用它的無(wú)參構(gòu)造方式,自己給連接淹朋。從而可以通過(guò)連接控制事務(wù)
QueryRunner runner = new QueryRunner();
Connection connection = JDBCUtils.getConnection();
connection.setAutoCommit(false);
2. 提交并釋放資源,異常由框架處理(其實(shí)沒處理)
//connection.commitAndCloseQuietly();
3. 回滾,撤銷并釋放資源,異常由框架處理(其實(shí)沒處理)
//connection.rollbackAndCloseQuietly();
事務(wù)回滾點(diǎn) SavePoint(類似與游戲中的存檔)
當(dāng)事務(wù)特別復(fù)雜笙各,有些情況不會(huì)回滾到事務(wù)的最開始狀態(tài),需要將事務(wù)回滾到指定位置
* 核心API
* connection.setSavepoint();// 設(shè)置回滾點(diǎn)
* connection.rollback(savepoint);//事務(wù)回滾到指定回滾點(diǎn)
- 示例代碼
- 往數(shù)據(jù)庫(kù)中添加數(shù)據(jù)础芍,每1000條保存一次杈抢。
Connection connection = null;
PreparedStatement prepareStatement = null;
Savepoint savepoint = null;
try {
connection = JDBCUtils.getConnection();
// 開啟事務(wù)
connection.setAutoCommit(false);
// 設(shè)置初始回滾點(diǎn)
savepoint = connection.setSavepoint();
String sql = "insert into person values (?,?)";
prepareStatement = connection.prepareStatement(sql);
for (int i = 1; i <= 5000; i++) {
prepareStatement.setInt(1, i);
prepareStatement.setString(2, "name" + i);
prepareStatement.addBatch();
// 模擬錯(cuò)誤
if (i == 3201) {
int x = 11 / 0;
}
// 每隔200條數(shù)據(jù)執(zhí)行一次批處理
if (i % 200 == 0) {
prepareStatement.executeBatch();
prepareStatement.clearBatch();
}
if (i % 1000 == 0) {
// 每一千條數(shù)據(jù),創(chuàng)建一個(gè)回滾點(diǎn)
savepoint = connection.setSavepoint();
}
}
prepareStatement.executeBatch();
prepareStatement.clearBatch();
// 沒有異常,提交事務(wù)
connection.commit();
} catch (Exception e) {
try {
// 發(fā)生異常,事務(wù)回滾到指定回滾點(diǎn)
connection.rollback(savepoint);
// 提交事務(wù)
connection.commit();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
JDBCUtils.release(connection, prepareStatement);
}
二、事務(wù)案例:簡(jiǎn)易轉(zhuǎn)賬案例
1仑性、分析
2惶楼、搭建環(huán)境
1. 數(shù)據(jù)庫(kù)及用戶表單的創(chuàng)建。
CREATE DATABASE day20;
USE day20;
CREATE TABLE USER(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(40),
money DOUBLE
);
INSERT INTO USER VALUES(NULL,'tom',2000);
INSERT INTO USER VALUES(NULL,'jerry',2000);
2. 數(shù)據(jù)庫(kù)驅(qū)動(dòng),c3p0的jar包,c3p0.xml配置文件诊杆,dbutils的jar包,
3. 轉(zhuǎn)賬頁(yè)面
<form action="${pageContext.request.contextPath}/transfer" method="post">
轉(zhuǎn)賬人:<input type="text" name="sender" /><br/>
被轉(zhuǎn)賬人:<input type="text" name="receiver" /><br/>
轉(zhuǎn)賬金額:<input type="text" name="acount" /><br/>
<input type="submit" value="轉(zhuǎn)賬"> <br/>
</form>
3歼捐、分包實(shí)現(xiàn)
3.1、創(chuàng)建web層TransactServlet
Servlet要做的三件事:
- 獲取表單提交的參數(shù)
- 調(diào)用業(yè)務(wù)邏輯
- 分發(fā)轉(zhuǎn)向
public class TransferServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 設(shè)置編碼集
response.setContentType("text/html;charset=UTF-8");
try {
// 獲取參數(shù)
String sender = request.getParameter("sender");
String receiver = request.getParameter("receiver");
String amount = request.getParameter("amount");
// 調(diào)用業(yè)務(wù)層對(duì)象,執(zhí)行轉(zhuǎn)賬
TransferService service = new TransferService();
service.transfer(sender, receiver, amount);
// 寫出響應(yīng)
response.getWriter().write("轉(zhuǎn)賬成功");
} catch (Exception e) {
// 寫出響應(yīng)
response.getWriter().write("轉(zhuǎn)賬失敗:" + e.getMessage());
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
3.2晨汹、創(chuàng)建service層TransferService
與數(shù)據(jù)庫(kù)交互的操作交到dao層完成豹储,service層只處理業(yè)務(wù)邏輯
public class TransferService {
public void transfer(String sender, String receiver, String amount) throws Exception {
Connection connection = null;
try {
// 與數(shù)據(jù)庫(kù)交互的事情交給dao層去操作 ,創(chuàng)建dao對(duì)象
TransferDao dao = new TransferDao();
// 開啟事務(wù)。要想控制事務(wù)dao層的connection連接對(duì)象和這里的連接對(duì)象應(yīng)該一致.因此將這個(gè)connection對(duì)象作為參數(shù)傳遞下去淘这。
//connection = JDBCUtils.getConnection();
//connection.setAutoCommit(false);
//為了不打破分層結(jié)構(gòu),不應(yīng)該將連接Connection的操作放在Service層.因此抽取出來(lái)
JDBCUtils.startTransaction();
// 改變的行數(shù)
int outDao = dao.outDao(sender, amount);
//一旦數(shù)據(jù)庫(kù)操作失敗,影響行數(shù)即為0剥扣,拋出異常帶上對(duì)應(yīng)的信息。
if(outDao != 1) {
throw new RuntimeException("轉(zhuǎn)出失敗");
}
int inDao = dao.inDao(receiver, amount);
if(inDao != 1) {
throw new RuntimeException("轉(zhuǎn)入失敗");
}
// 沒有異常,提交事務(wù)
JDBCUtils.commitAndRelease();
//這里是用try-catch是為了保證有異常與無(wú)異常時(shí)的兩種處理方式
//這里抓住了上面拋出的轉(zhuǎn)賬信息的異常但是為了告訴Servlet這里發(fā)生了異常铝穷,將該異常繼續(xù)向上拋
} catch (Exception e) {
// 發(fā)生異常,回滾撤銷操作
JDBCUtils.rollbackAndRelease();
throw e;
}
}
}
3.3钠怯、創(chuàng)建utils工具類JDBCUtils
提供與數(shù)據(jù)庫(kù)操作相關(guān)的工具:獲取連接池與獲取連接的方法
因?yàn)镈BUtils框架的QueryRunner對(duì)象會(huì)自行釋放資源,因此不提供釋放Connection等資源的方法。
private static DataSource dataSource = new ComboPooledDataSource();
// 獲取連接池對(duì)象
public static DataSource getDataSource() {
return dataSource;
}
// 提供連接
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
3.4曙聂、創(chuàng)建dao層TransferDao
進(jìn)行操作數(shù)據(jù)庫(kù)的動(dòng)作晦炊。
public class TransferDao {
public int outDao(String sender, String amount) throws SQLException {
//對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)進(jìn)行更改
//1.創(chuàng)建QueryRunner對(duì)象
QueryRunner runner = new QueryRunner();
Connection connection = JDBCUtils.getConnectionTL();
//2.執(zhí)行sql語(yǔ)句
String sql = "update user set money=money-? where name=?";
//為了告知service層sql語(yǔ)句是否執(zhí)行成功,返回被影響的行數(shù)
int i = runner.update(connection, sql, amount,sender);
return i;
}
public int inDao(String receiver, String amount) throws SQLException {
//對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)進(jìn)行更改
//1.創(chuàng)建QueryRunner對(duì)象
QueryRunner runner = new QueryRunner(JDBCUtils.getDataSource());
Connection connection = JDBCUtils.getConnectionTL();
//2.執(zhí)行sql語(yǔ)句
String sql = "update user set money=money+? where name=?";
int i = runner.update(connection, sql, amount,receiver);
return i;
}
}
3.5、優(yōu)化代碼宁脊,完善utils工具類
因?yàn)榈谝话嬷衧ervice層執(zhí)行了屬于dao層與數(shù)據(jù)庫(kù)交互的操作断国,這打破了三層結(jié)構(gòu),為了避免這種情況朦佩,應(yīng)當(dāng)考慮將與數(shù)據(jù)庫(kù)交互的相關(guān)操作抽取出來(lái)并思,于是我將控制事務(wù)的代碼抽取到工具類中庐氮。提供開啟事務(wù)语稠,提交事務(wù)和回滾事務(wù)的方法。
但是為了控制事務(wù)弄砍,service層和dao層的connection應(yīng)當(dāng)是同一個(gè)connection仙畦,因此使用了一個(gè)類似于map集合的ThreadLocal類,它的內(nèi)部存在<Thread.currentThread,Connection>的鍵值對(duì)音婶,通過(guò)它保證獲取同一個(gè)connection連接慨畸。
public class JDBCUtils {
private static DataSource dataSource = new ComboPooledDataSource();
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();
// 獲取連接池對(duì)象
public static DataSource getDataSource() {
return dataSource;
}
// 提供連接
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 提供連接,從ThreadLocal中獲取的
public static Connection getConnectionTL() throws SQLException {
// 從ThreadLocal中獲取連接
Connection connection = threadLocal.get();
// 如果連接對(duì)象為空
if (connection == null) {
// 從連接池獲取連接
connection = getConnection();
// 將連接放入ThreadLocal,這樣下一次從ThreadLocal中獲取的時(shí)候就有數(shù)據(jù)了
threadLocal.set(connection);
}
return connection;
}
// 開啟事務(wù)
public static void startTransaction() throws SQLException {
Connection connection = getConnectionTL();
connection.setAutoCommit(false);
}
// 提交事務(wù)并釋放資源
public static void commitAndRelease() throws SQLException {
Connection connection = getConnectionTL();
// 提交事務(wù)
connection.commit();
if (connection != null) {
connection.close();
connection = null;
}
// 把連接對(duì)象和ThreadLocal解綁,可以讓ThreadLocal盡快被釋放,節(jié)約服務(wù)器內(nèi)存資源
threadLocal.remove();
}
// 回滾事務(wù)并釋放資源
public static void rollbackAndRelease() throws SQLException {
Connection connection = getConnectionTL();
// 回滾事務(wù)
connection.rollback();
if (connection != null) {
connection.close();
connection = null;
}
// 把連接對(duì)象和ThreadLocal解綁,可以讓ThreadLocal盡快被釋放,節(jié)約服務(wù)器內(nèi)存資源
threadLocal.remove();
}
}
3.6、 案例實(shí)現(xiàn)_為什么要使用事務(wù)衣式?
如果不考慮事務(wù),會(huì)導(dǎo)致轉(zhuǎn)賬錢丟失的問題.所以必須考慮事務(wù)
如果要避免上述問題,只需要保證所有的操作使用同一個(gè)連接對(duì)象即可
-
解決方法
- 傳遞參數(shù)Connection(打破了三層架構(gòu),因此采用線程綁定)
- 線程綁定ThreadLocal
-
ThreadLocal簡(jiǎn)介
- 作用 : 把一個(gè)操作對(duì)象和當(dāng)前線程綁定在一起. 其內(nèi)部維護(hù)了一個(gè)Map集合.key就是當(dāng)前線程,value就是要綁定的內(nèi)容
- 常用API
- set(T value) : 把一個(gè)對(duì)象和當(dāng)前線程進(jìn)行綁定.等價(jià)于Map.put(Thread.currentThread(),value)
- T get() : 獲取和當(dāng)前線程綁定在一起的對(duì)象.等價(jià)于Map.get(Thread.currentThread())
- remove() : 移除和當(dāng)前線程綁定在一起的對(duì)象.等價(jià)于Map.remove(Thread.currentThread())
4寸士、事務(wù)的一些概念
事務(wù)特性
-
<font color='red'>事務(wù)的四大特性:
- 原子性(Atomicity):事務(wù)的一組操作不可分割檐什,要么都成功,要么都失敗
- 一致性(Consistency):事務(wù)前后數(shù)據(jù)保持完整性.轉(zhuǎn)賬前A和B賬戶總和2000元弱卡,轉(zhuǎn)賬后總和還是2000 元
- 隔離性(Isolation):并發(fā)訪問時(shí),事務(wù)之間是隔離的,一個(gè)事務(wù)不應(yīng)該影響其它事務(wù)的運(yùn)行效果
- 持久性(Durability):當(dāng)事務(wù)一旦提交,事務(wù)數(shù)據(jù)永久存在,無(wú)法改變 </font>
企業(yè)開發(fā)中一定要保證事務(wù)原子性
事務(wù)最復(fù)雜問題都是由事務(wù)隔離性引起的
隔離性
-
不考慮事務(wù)隔離將引發(fā)的問題
- 臟讀:一個(gè)事務(wù)讀取另一個(gè)事務(wù)未提交的數(shù)據(jù).這是數(shù)據(jù)庫(kù)隔離中最重要的問題
- 不可重復(fù)讀:一個(gè)事務(wù)讀取另一個(gè)事務(wù)已提交的數(shù)據(jù)乃正,在一個(gè)事務(wù)中兩次查詢結(jié)果不同(針對(duì)update操作)
- 虛讀:一個(gè)事務(wù)讀取另一個(gè)事務(wù)插入的數(shù)據(jù),造成在一個(gè)事務(wù)中兩次查詢記錄條數(shù)不同(針對(duì)insert操作)
-
數(shù)據(jù)庫(kù)為了解決三類隔離引發(fā)問題,提供了四個(gè)數(shù)據(jù)庫(kù)隔離級(jí)別(所有數(shù)據(jù)庫(kù)通用)
- <font color='red'> Serializable : 串行處理.可以解決三類問題
- Repeatable read :可以解決不可重復(fù)讀婶博、臟讀瓮具,但是會(huì)發(fā)生虛讀.是MySQL的默認(rèn)級(jí)別
- read committed : 可以解決臟讀,會(huì)發(fā)生不可重復(fù)讀、虛讀.是Oracle的默認(rèn)級(jí)別
- read uncommitted : 會(huì)導(dǎo)致三類問題發(fā)生 </font>
- 按照隔離級(jí)別從高到低排序 : Serializable > Repeatable read > read committed > read uncommitted
- 數(shù)據(jù)庫(kù)隔離問題危害的排序 : 臟讀> 不可重復(fù)讀 > 虛讀
- 多數(shù)數(shù)據(jù)庫(kù)廠商都會(huì)采用Repeatable read或read committed兩個(gè)級(jí)別.
-
更改事務(wù)隔離級(jí)別的語(yǔ)句
- set transaction isolation level 設(shè)置事務(wù)隔離級(jí)別
- select @@tx_isolation; 查詢當(dāng)前事務(wù)隔離級(jí)別
隔離級(jí)別引發(fā)問題的小實(shí)驗(yàn)
-
臟讀問題(read uncommitted)
- 開啟兩個(gè)窗口,執(zhí)行一次查詢,獲得一個(gè)結(jié)果
- 將B窗口隔離級(jí)別設(shè)置為read uncommitted
- set session transaction isolation level read uncommitted;
- 在A凡人、B窗口分別開啟一個(gè)事務(wù) start transaction;
- 在A窗口完成轉(zhuǎn)賬操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 在B窗口進(jìn)行查詢,會(huì)讀取到A窗口未提交的轉(zhuǎn)賬結(jié)果
- A窗口進(jìn)行回滾rollback, B窗口查詢結(jié)果恢復(fù)之前
-
不可重復(fù)讀(read committed)
- 開啟兩個(gè)窗口,執(zhí)行一次查詢,獲得一個(gè)結(jié)果
- 將B窗口隔離級(jí)別設(shè)置為read committed
- set session transaction isolation level read committed;
- 在A名党、B窗口分別開啟一個(gè)事務(wù) start transaction;
- 在A窗口完成轉(zhuǎn)賬操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 此時(shí)在B窗口執(zhí)行查詢操作,數(shù)據(jù)不會(huì)發(fā)生改變.避免了臟讀問題
- A窗口執(zhí)行commit,B窗口再次執(zhí)行查詢,會(huì)讀取到A窗口提交的結(jié)果.注意此時(shí)B窗口沒有提交事務(wù),也就是在同一事務(wù)中,讀取到了兩個(gè)結(jié)果.發(fā)生不可重復(fù)讀問題
-
虛讀(Repeatable read)
- 開啟兩個(gè)窗口,執(zhí)行一次查詢,獲得一個(gè)結(jié)果
- 將B窗口隔離級(jí)別設(shè)置為Repeatable read
- set session transaction isolation level repeatable read;
- 在A、B窗口分別開啟一個(gè)事務(wù) start transaction;
- 在A窗口完成轉(zhuǎn)賬操作
- update account set money= money - 200 where name='aaa';
- update account set money= money +200 where name='bbb';
- 此時(shí)在B窗口執(zhí)行查詢操作,數(shù)據(jù)不會(huì)發(fā)生改變.避免了臟讀問題
- A窗口執(zhí)行commit,B窗口再次執(zhí)行查詢,數(shù)據(jù)仍然不會(huì)發(fā)生改變.避免了不可重復(fù)讀.
- 此時(shí)如果在A窗口插入一條數(shù)據(jù),而B窗口可以查詢到,就是發(fā)生了虛讀問題.但是這種情況發(fā)生的幾率非常小.
-
Serializable
- 開啟兩個(gè)窗口,執(zhí)行一次查詢,獲得一個(gè)結(jié)果
- 將B窗口隔離級(jí)別設(shè)置為read serializable
- set session transaction isolation level serializable;
- 在A挠轴、B窗口分別開啟一個(gè)事務(wù) start transaction;
- 在B窗口執(zhí)行查詢操作
- 在A窗口執(zhí)行插入操作.此時(shí)A窗口將會(huì)被卡住,不會(huì)執(zhí)行語(yǔ)句.直到B窗口提交或回滾,釋放數(shù)據(jù)庫(kù)資源
- 在JDBC中,可以通過(guò)Connection.setTransactionIsolation(int level) 來(lái)設(shè)置隔離級(jí)別.如果沒有設(shè)置.會(huì)采用數(shù)據(jù)庫(kù)的默認(rèn)級(jí)別
丟失更新問題和悲觀鎖樂觀鎖機(jī)制【了解】
事務(wù)丟失更新問題 : 兩個(gè)事務(wù)同時(shí)讀取同一條記錄传睹,A先修改記錄,B也修改記錄(B不知道A修改過(guò))忠荞,B提交數(shù)據(jù)后B的修改結(jié)果覆蓋了A的修改結(jié)果蒋歌。
-
解決丟失更新的兩種方式
- 事務(wù)和鎖是不可分開的,鎖一定是在事務(wù)中使用 委煤,當(dāng)事務(wù)關(guān)閉鎖自動(dòng)釋放
- 悲觀鎖
- 假設(shè)丟失更新會(huì)發(fā)生
- 使用數(shù)據(jù)庫(kù)內(nèi)部鎖機(jī)制堂油,進(jìn)行表的鎖定,在A修改數(shù)據(jù)時(shí)碧绞,A就將數(shù)據(jù)鎖定府框,B此時(shí)無(wú)法進(jìn)行修改
- 在mysql中默認(rèn)情況下,當(dāng)你修改數(shù)據(jù)讥邻,自動(dòng)為數(shù)據(jù)加鎖(在事務(wù)中),防止兩個(gè)事務(wù)同時(shí)修改數(shù)據(jù)
- 在mysql內(nèi)部有兩種常用鎖
- 讀鎖(共享鎖)
- 一張表可以添加多個(gè)讀鎖迫靖,如果表被添加了讀鎖(不是當(dāng)前事務(wù)添加的),該表不可以修改
- 語(yǔ)法 : select * from account lock in share mode;
- 共享鎖非常容易發(fā)生死鎖
- 寫鎖(排它鎖)
- 一張表只能加一個(gè)排它鎖兴使,排他鎖和其它共享鎖系宜、排它鎖都具有互斥效果 。
- 如果一張表想添加排它鎖发魄,前提是之前表一定沒有加過(guò)共享鎖和排他鎖
- 語(yǔ)法 : select * from account for update ;
- 讀鎖(共享鎖)
- 樂觀鎖
- 假設(shè)丟失更新不會(huì)發(fā)生
- 使用的不是數(shù)據(jù)庫(kù)鎖機(jī)制盹牧,而是一個(gè)特殊標(biāo)記字段 : 數(shù)據(jù)庫(kù)timestamp 時(shí)間戳字段