Spring學習總結(十一):Spring中的事務介紹
一、事務的介紹
1、什麼是事務
事務是資料庫操作的最基本單元,是邏輯上的一組操作。這組操作的結果要麼成功,要麼失敗。以取錢為例來說明事務:如果小明去ATM機取200塊錢,正常結果就是銀行卡扣掉200塊錢,ATM出200塊錢,然後小明拿走。這兩個步驟可以整體看作一個事務,並且要麼都執行,要麼都不執行。如果銀行扣掉200塊錢,而ATM故障無法出錢,這對客戶來說是無法容忍的。如果銀行扣錢失敗但ATM出了200被小明拿走,這對銀行來說就是損失。為了不讓雙方都有損失,最好的方法就是上述兩個步驟要麼都執行,要麼都是不執行,如果任何一方出現故障或失敗,則整個取錢的過程就會回滾,回到最初的狀態。這裡就該事務登場來解決此類問題。
2、事務的特性——ACID
(1)原子性(Atomicity):事務是由一系列動作組成的原子操作,原子是不可再分的。事務的原子性確保了動作的執行結果要麼全部完成,要麼全部不完成。
(2)一致性(Consistency):事務的一致性是指在一個事務執行之前和執行之後資料庫都必須處於一致性狀態。如果事務成功地完成,那麼系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那麼系統中的所有變化將自動地回滾,系統返回到原始狀態。比如說tom和jerry各有500塊錢,他們加在一起就是1000塊錢,現在tom給jerry轉賬100,此時tom就有400,而Jerry有600,但他們加在一起還是1000。
(3)隔離性(Isolation):在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。事務檢視資料更新時,資料所處的狀態要麼是另一事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視到中間狀態的資料。
(4)永續性(Durability):只要事務成功結束,它對資料庫所做的更新就必須永久儲存下來。即使發生系統崩潰,重新啟動資料庫系統後,資料庫還能恢復到事務成功結束時的狀態
3、併發場景下事務產生的問題
(1)髒讀:髒讀是指事務A讀取到了事務B還沒有提交的資料。假設有1000塊錢,事務A開啟事務,此時事務B開啟事務並從中取出100塊錢,但沒有提交。如果此時事務A讀取資料,那麼得到的是1000,這就是所謂的髒讀。
(2)不可重複讀:不可重複讀是指在一個事務裡多次讀取同一個資料,但是得到的資料結果不一致。假設有1000塊錢,事務A開啟事務,得到結果為1000,此時事務B開啟事務並從中取出100塊錢並提交,事務A再次讀取,就變成了900。也就是說,事務B在事務A多次讀取的過程中,對資料進行更新提交,導致事務A多次讀取得到的結果不一致。
(3)幻讀:幻讀是指在一個事務裡面的操作中出現了未被操作的資料。當事務A在讀取資料時,事務B插入了資料,導致事務A第二次讀取資料的時候資料不一致,如同發生幻覺一般。
4、Spring中的事務的隔離級別
事務隔離級別是為了解決上面幾種問題而誕生的。事務隔離級別越高,在併發下會產生的問題就越少,但同時付出的效能消耗也將越大,因此很多時候必須在併發性和效能之間做一個權衡。所以要根據自己專案的併發情況選擇合適的事務隔離級別
事務隔離級別有4種,Spring會提供給使用者5種:
(1)DEFAULT
預設隔離級別,每種資料庫支援的事務隔離級別不一樣,如果Spring配置事務時將isolation設定為這個值的話,那麼將使用底層資料庫的預設事務隔離級別。MySQL使用"select @@tx_isolation"來檢視預設的事務隔離級別。
(2)READ_UNCOMMITTED
讀未提交,即能夠讀取到沒有被提交的資料,所以很明顯這個級別的隔離機制無法解決髒讀、不可重複讀、幻讀中的任何一種,因此很少使用。
(3)READ_COMMITED
讀已提交,即能夠讀到那些已經提交的資料,自然能夠防止髒讀,但是無法限制不可重複讀和幻讀。
(4)REPEATABLE_READ
重複讀取,即在資料讀出來之後加鎖,類似"select * from XXX for update",明確資料讀取出來就是為了更新用的,所以要加一把鎖,防止別人修改它。REPEATABLE_READ的意思也類似,讀取了一條資料,這個事務不結束,別的事務就不可以改這條記錄,這樣就解決了髒讀、不可重複讀的問題,但是幻讀的問題還是無法解決。
(5)SERLALIZABLE
序列化,最高的事務隔離級別,不管多少事務,挨個執行完一個事務的所有子事務之後才可以執行另外一個事務裡面的所有子事務,這樣就解決了髒讀、不可重複讀和幻讀的問題了
如下表:
隔離級別 | 髒讀可能性 | 不可重複讀可能性 | 幻讀可能性 | 加鎖讀 |
READ_UNCOMMITTED | 是 | 是 | 是 | 否 |
READ_COMMITTED | 否 | 是 | 是 | 否 |
REPEATABLE_READ | 否 | 否 | 是 | 否 |
SERIALIZABLE | 否 | 否 | 否 | 是 |
注意:大多數資料庫預設的事務隔離級別是Read committed,比如Sql Server , Oracle。Mysql的預設隔離級別是Repeatable read。
二、簡單案例——轉賬
(1)建立外部配置檔案jdbc.properties
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.userName=root
jdbc.password=root
(2)建立bean.xml,配置資料來源、jdbcTemplate以及註解掃描的包。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--引入context名稱空間-->
<!-- 配置註解掃描的包 -->
<context:component-scan base-package="com.yht.example8"></context:component-scan>
<!--引入外部屬性檔案-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置連線池-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driverClass}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.userName}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 配置jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
(3)建立t_account表並插入資料。
create table t_account(
name varchar(20),
money int);
insert into t_account values("tom", 1000);
insert into t_account values("jerry", 1000);
(4)建立UserService類
@Service
public class UserService {
@Autowired
private IUserDao userDao;
public void tranferAccount(){
userDao.reduceMoney();
userDao.addMoney();
}
}
(5)建立IUserDao和UserDaoImpl類
public interface IUserDao {
void reduceMoney();
void addMoney();
}
@Repository
public class UserDaoImpl implements IUserDao {
@Autowired
public JdbcTemplate jdbcTemplate;
@Override
public void reduceMoney() {
String sql = "update t_account set money=money-? where name=?";
jdbcTemplate.update(sql, 100, "tom");
}
@Override
public void addMoney() {
String sql = "update t_account set money=money+? where name=?";
jdbcTemplate.update(sql, 100, "jerry");
}
}
(6)進行單元測試。
@Test
public void testAccount(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.tranferAccount();
}
說明:以上程式碼是一個正常轉賬的流程,最終也能達到預期的效果。但是,如果在UserService的tranferAccount()中轉賬的兩個步驟之間出現異常,那麼轉賬是否會成功呢?
轉賬之前資料庫中的資訊:
在tranferAccount()模擬一個異常:
public void tranferAccount(){
userDao.reduceMoney();
int i = Integer.parseInt("123a");
userDao.addMoney();
}
此時資料庫中的結果:
從結果中可以看出轉賬失敗了,tom少了100,但是jerry的money卻沒有增加,這在實際生活中是不被允許的。在接下來的文章中,將通過Spring中的事務管理操作來解決該問題。