事務控制


事務4個特性:ACID

⑴ 原子性(Atomicity)

原子性是指事務包含的所有操作要么全部成功,要么全部失敗回滾,這和前面兩篇博客介紹事務的功能是一樣的概念,因此事務的操作如果成功就必須要完全應用到數據庫,如果操作失敗則不能對數據庫有任何影響。

 ⑵ 一致性(Consistency)

一致性是指事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態,也就是說一個事務執行之前和執行之后都必須處於一致性狀態。

拿轉賬來說,假設用戶A和用戶B兩者的錢加起來一共是5000,那么不管A和B之間如何轉賬,轉幾次賬,事務結束后兩個用戶的錢相加起來應該還得是5000,這就是事務的一致性。

⑶ 隔離性(Isolation)

隔離性是當多個用戶並發訪問數據庫時,比如操作同一張表時,數據庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個並發事務之間要相互隔離。

即要達到這么一種效果:對於任意兩個並發的事務T1和T2,在事務T1看來,T2要么在T1開始之前就已經結束,要么在T1結束之后才開始,這樣每個事務都感覺不到有其他事務在並發地執行。

⑷ 持久性(Durability)

持久性是指一個事務一旦被提交了,那么對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。例如我們在使用JDBC操作數據庫時,在提交事務方法后,提示用戶事務操作完成,當我們程序執行完成直到看到提示后,就可以認定事務以及正確提交,即使這時候數據庫出現了問題,也必須要將我們的事務完全執行完成,否則就會造成我們看到提示事務處理完畢,但是數據庫因為故障而沒有執行事務的重大錯誤。

事務控制一般分為兩種方式:

1、編程式事務控制

2、注解式事務控制

首先看一個例子,不做事務控制會怎樣?

 private int saveUser(int i){
        User user1 = new User();
        user1.setUserName("哈哈龍"+i);
        user1.setPassWord("hahalong001");
        user1.setAddress("長安");
        user1.setEmail("hahalong@163.com");
        user1.setGender("1");
        user1.setIdentity("321736165504567129");
        return userMapper.insertUser(user1);
    }

    private int updateUser(int i){
        User user1 = new User();
        user1.setUserName("哈哈龍"+i);
        user1.setPassWord("up-hahalong001");
        user1.setAddress("長安");
        user1.setEmail("up-hahalong@163.com");
        user1.setGender("1");
        user1.setIdentity("321736165504567129");
        return userMapper.updateUserByUserName(user1);
    }

    /**
     * 不做事務控制
     */
    public void withoutTransControl(){
        saveUser(1);
        int i = 6/0;
        updateUser(2);
    }

結果,新增成功了,更新沒有執行。違反事務原子性。

 

1、編程式事務控制:

 @Autowired
    private TransactionTemplate transactionTemplate;
/**
     * 編程式事務控制
     */
    public boolean programTransControl(){
        Boolean isSuccess = transactionTemplate.execute(new TransactionCallback<Boolean>() {
            public Boolean doInTransaction(TransactionStatus status) {
                Boolean result = true;
                try {
                    saveUser(2);
                    updateUser(3);
                    int i = 6/0;
                } catch (Exception e) {
                    status.setRollbackOnly();
                    result = false;
                }
                return result;
            }
        });
        return isSuccess;
    }

運行結果,數據沒有插入,也沒有更新。

2、注解事務控制

 @Transactional
    public void anotationTransControl(){
        saveUser(2);
        int i = 6/0;
        updateUser(3);
    }

運行后,數據沒有插入,也沒有更新。但是如果捕獲異常,這種情況下會怎樣?

 @Transactional
    public void anotationTransControl2(){
        try{
            saveUser(2);
            int i = 6/0;
            updateUser(3);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

運行后:插入成功了!!!

此時兩種方案

(1)再捕獲異常的地方,再次拋異常。注意:可以指定哪些異常可以回滾

//@Transactional(rollbackFor = {Exception.class,RuntimeException.class})
    @Transactional
    public void anotationTransControl3(){
        try{
            saveUser(3);
            int i = 6/0;
            updateUser(4);
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

(2)在捕獲異常的地方代碼回滾

@Transactional
public void anotationTransControl4(){
    try{
        saveUser(3);
        int i = 6/0;
        updateUser(4);
    }catch (Exception e){
        e.printStackTrace();
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

兩種方法,運行后,數據沒有插入,也沒有更新。總結:在做注解事務控制,一定要注意異常捕獲時的事務處理。

 

事務隔離級別:

首先了解下不考慮事務隔離性,發生的幾種問題:

1、臟讀:臟讀是指在一個事務處理過程里讀取了另一個未提交的事務中的數據。

例如:用戶A向用戶B轉賬100元。

線程1:A  轉100->B,此時還未提交事務;

線程2:B 賬戶余額記錄,發現賬戶多了100,立馬通知B有人給你100,B好開心。。。

線程1:方法有異常,回滾事務;B余額並未多了100.。。

2、不可重復讀:不可重復讀是指在對於數據庫中的某個數據,一個事務范圍內多次查詢卻返回了不同的數據值,這是由於在查詢間隔,被另一個事務修改並提交了。

例如:用戶A向用戶B轉賬100元。

線程2:B 查詢賬戶余額記錄,為X;

線程1:A  轉100->B,提交事務;

線程2:B 再次查詢賬戶余額記錄,為X+100,嗯?與上次不一樣,幾個意思,再查下。。。;

不可重復讀和臟讀的區別是,臟讀是某一事務讀取了另一個事務未提交的臟數據,而不可重復讀則是讀取了前一事務提交的數據。

3、虛讀(幻讀)

幻讀是事務非獨立執行時發生的一種現象。

eg:怪獸工廠有一批小怪物,凜冬將至,要蘇醒禍及人間了!!

線程1:夜王,小的們別睡了,跟哥去打人,睡眠狀態weak_up = 1  改為  weak_up =0;

線程2:老天爺,嗯,怪物種類有點單一,數量稀少,我再給你造幾個,插入幾條怪物數據,weak_up = 1 

線程1:夜王高舉大旗,威風凜凜,咦,怎么還有小的在睡覺?懷疑人生。。。

幻讀和不可重復讀都是讀取了另一條已經提交的事務(這點就臟讀不同),所不同的是不可重復讀查詢的都是同一個數據項,而幻讀針對的是一批數據整體(比如數據的個數)。

隔離級別是指若干個並發的事務之間的隔離程度。TransactionDefinition 接口中定義了五個表示隔離級別的常量:

  • TransactionDefinition.ISOLATION_DEFAULT:這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀,不可重復讀和幻讀,因此很少使用該隔離級別。比如PostgreSQL實際上並沒有此級別。
  • TransactionDefinition.ISOLATION_READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的數據【推薦】。該級別可以防止臟讀依然防止不了不可重復度和幻讀;
  • TransactionDefinition.ISOLATION_REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,並且每次返回的記錄都相同。該級別可以防止臟讀和不可重復讀。
  • TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀

隔離級別越高,帶來的安全級別越高,但性能也越差。此事古難全,權衡把握。。。。

下面給一個常在代碼中犯的一個錯誤,即加入在業務上username是不重復的,我們會經常先查詢是否存在,存在更新,不存在就插入。

請看:

提供一個存儲過程:

create procedure saveOrUpdateUsers_produce()
BEGIN 
DECLARE v_count int; 

set v_count = 0;
select count(1) into v_count from c_tbl_users where username='王澤中';

if v_count > 0 THEN
    update c_tbl_users set email='wangzezhong1@163.com',mobile='18646542313',address='江蘇省南京市鼓樓區'
  where username='王澤中'; 
else
    insert into c_tbl_users (username,pwd,gender,email,mobile,identity,address)
  VALUES('王澤中','wang123','1','wangzezhong@163.com','18646542312','321123198804211819','江蘇省南京市雨花區');
end if;
END; 

代碼中調用:

 <insert id="callProduce">
      call saveOrUpdateUsers_produce();
    </insert>
 @Transactional
    public void callProduce(){
        userMapper.callProduce();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

開啟兩個線程執行

@Test
    public void test6(){
        new Thread(()->{userService.callProduce();}).start();
        new Thread(()->{userService.callProduce();}).start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

當前數據庫中無王澤中數據,執行后,請看:

這種寄希望sql控制來實現唯一性是不可取的,上面的例子是一個幻讀問題,是不是加了隔離級別就可以了呢?

刪掉數據,加上隔離級別,再執行>>

 @Transactional(isolation = Isolation.SERIALIZABLE)
    //@Transactional
    public void callProduce(){
        userMapper.callProduce();
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

結果:

哎呀成功了,手舞足蹈,高手寂寞。。。額等下,單機你可以控制,分布式的多台機器,你怎么辦?

所以,總結一點:對於業務上唯一性的數據,要在表上設置唯一鍵,不要通過sql去控制。

表設計上,也許需要保留數據歷史記錄,如果保留了,就沒法設置唯一鍵了嘛。對於這點,對於非業務的數據,優先考慮創建歷史備份表,而不是讓無用的數據充斥在我們的業務表中。

事務傳播行為:

Spring中七種事務傳播行為

  • TransactionDefinition.PROPAGATION_REQUIRED:如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。這是默認值。
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW:創建一個新的事務,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
  • TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出異常。
  • TransactionDefinition.PROPAGATION_MANDATORY:如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
  • TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於TransactionDefinition.PROPAGATION_REQUIRED。

解析:

1、PROPAGATION_REQUIRED:

樣例演示:

    @Transactional(propagation = Propagation.REQUIRED)
    public void addRequired(User user){
        userMapper.insertUser(user);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void addRequiredException(User user){
        userMapper.insertUser(user);
        throw new RuntimeException();
    }

場景一:外圍不加事務

public void test7(){
        User user1 = new User();
        user1.setUserName("三狗子");
        user1.setPassWord("xiaoribiao001");
        user1.setAddress("長安");
        user1.setEmail("xiaoribiao@163.com");
        user1.setGender("1");
        user1.setIdentity("321736165502567129");
        userService.addRequired(user1);

        User user2 = new User();
        user2.setUserName("雅少");
        user2.setPassWord("xiaojiandun001");
        user2.setAddress("長安");
        user2.setEmail("xiaojiandun@163.com");
        user2.setGender("1");
        user2.setIdentity("321716165502567129");
        userService.addRequired(user2);
        throw new RuntimeException();
    }

場景二:外圍不加事務

public void test7(){
        User user1 = new User();
        user1.setUserName("三狗子");
        user1.setPassWord("xiaoribiao001");
        user1.setAddress("長安");
        user1.setEmail("xiaoribiao@163.com");
        user1.setGender("1");
        user1.setIdentity("321736165502567129");
        userService.addRequired(user1);

        User user2 = new User();
        user2.setUserName("雅少");
        user2.setPassWord("xiaojiandun001");
        user2.setAddress("長安");
        user2.setEmail("xiaojiandun@163.com");
        user2.setGender("1");
        user2.setIdentity("321716165502567129");
        userService.addRequiredException(user2);
    }

場景三:外圍加事務

    @Transactional(propagation = Propagation.REQUIRED)
    public void test7(){
        User user1 = new User();
        user1.setUserName("三狗子");
        //....
        userService.addRequired(user1);

        User user2 = new User();
        user2.setUserName("雅少");
        //.....
        userService.addRequired(user2);
        throw new RuntimeException();
    }

場景四:外圍加事務

    @Transactional(propagation = Propagation.REQUIRED)
    public void test7(){
        User user1 = new User();
        user1.setUserName("三狗子");
        userService.addRequired(user1);

        User user2 = new User();
        user2.setUserName("雅少");      
        userService.addRequiredException(user2);
    }

場景五:外圍加事務

    @Transactional(propagation = Propagation.REQUIRED)
    public void test7(){
        User user1 = new User();
        user1.setUserName("三狗子");       
        userService.addRequired(user1);

        User user2 = new User();
        user2.setUserName("雅少");      
        try{
            userService.addRequiredException(user2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

Propagation.REQUIRED修飾的內部方法和外圍方法均屬於同一事務,只要一個方法回滾,整個事務均回滾

2、PROPAGATION_REQUIRES_NEW

樣例演示:

UserService.java,新增方法

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addRequiresNew(User user){
        userMapper.insertUser(user);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addRequiresNewException(User user){
        userMapper.insertUser(user);
        throw new RuntimeException();
    }

新建UserService2.java,原因稍后會列舉一個異常不回滾的例子。

在UserService2中編寫如下:

 private User createUser(String userName){
        User user1 = new User();
        user1.setUserName(userName);
        user1.setPassWord("xiaoribiao001");
        user1.setAddress("長安");
        user1.setEmail("xiaoribiao@163.com");
        user1.setGender("1");
        user1.setIdentity("321736165502567129");
        return user1;
    }

場景一:

    public void doHandleNoTrans_1(){
        User user1 = createUser("三狗子");
        userService.addRequiresNew(user1);
        User user2 = createUser("雅少");
        userService.addRequiresNew(user2);
        throw new RuntimeException();
    }

場景二:

public void doHandleNoTrans_2(){
        User user1 = createUser("三狗子");
        userService.addRequiresNew(user1);
        User user2 = createUser("雅少");
        userService.addRequiresNewException(user2);
    }

場景三:

    @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_3(){
        User user1 = createUser("三狗子");
        userService.addRequired(user1);
        User user2 = createUser("雅少");
        userService.addRequiresNew(user2);
        throw new RuntimeException();
    }

場景四

 @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_4(){
        User user1 = createUser("三狗子");
        userService.addRequired(user1);
        User user2 = createUser("雅少");
        userService.addRequiresNewException(user2);
    }

場景五:

    @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_5(){
        User user1 = createUser("三狗子");
        userService.addRequired(user1);
        User user2 = createUser("雅少");
        try{
            userService.addRequiresNewException(user2);
        }catch (RuntimeException e){
            e.printStackTrace();
        }
    }

結果:

這里發現一個現象,就是加事務的方法如果在同一個類中,事務傳播性是最外圍的方法的傳播性值。

下面分析下例子,在UserService.java中添加如下方法:

    @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans(){
        User user1 = createUser("三狗子");
        addRequired(user1);
        User user2 = createUser("雅少");
        try{
            addRequiresNewException(user2);
        }catch (RuntimeException e){
            e.printStackTrace();
        }
    }

本意是想,三狗子成功,雅少事務獨立,異常回滾,然后外圍方法捕獲異常,不回滾三狗子。在運行結束后,啊。。。。。與預想的完全不一樣!!!

分析:代碼等同與下面3個方法,注意這里調用與PROPAGATION_REQUIRED場景5不同,其實是addRequired和addRequiredException方法是不會新建事務的,盡管在方法上面加了注解。為什么呢?因為Spring事務是基於動態代理AOP對bean管理和切片,它為每一個Class生成一個代理對象,只有代理對象之間進行調用,才會觸發切面邏輯。

同一個Class方法之間調用是原對象方法,不通過代理對象,所以此時是無法通過注解保證事務性的。所以當事務傳播性變化時,最好在不同的類中實現。

@Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_bak1(){
        User user1 = createUser("三狗子");
        addRequired(user1);//不會有新事務開啟,Spring攔截器不會攔截。。。
        User user2 = createUser("雅少");

        try{
            addRequiredException(user2);
        }catch (RuntimeException e){
            e.printStackTrace();
        }
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_bak2(){
        User user1 = createUser("三狗子");
        addRequired(user1);
        User user2 = createUser("雅少");

        try{
            addRequired(user2);
            throw new RuntimeException();
        }catch (RuntimeException e){
            e.printStackTrace();
        }
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void doHandleTrans_bak3(){
        User user1 = createUser("三狗子");
        userMapper.insertUser(user1);
        User user2 = createUser("雅少");

        try{
            userMapper.insertUser(user2);
            throw new RuntimeException();
        }catch (RuntimeException e){
            e.printStackTrace();
        }
    }

哇,真相大白,額,小伙子,你的理論基礎呢,叨叨個沒完,源代碼在哪里?TransactionAspectSupport.class,有興趣可以看看。。。

 

3、PROPAGATION_NESTED

UserService.java

 @Transactional(propagation = Propagation.NESTED)
    public void addNested(User user){
        userMapper.insertUser(user);
    }

    @Transactional(propagation = Propagation.NESTED)
    public void addNestedException(User user){
        userMapper.insertUser(user);
        throw new RuntimeException();
    }

外部無事務,UserService2.java

場景一:

public void doHandleNoTransNested1(){
        User user1 = createUser("三狗子");
        userService.addNested(user1);
        User user2 = createUser("雅少");
        userService.addNested(user2);
        throw new RuntimeException();
    }

場景二:

    public void doHandleNoTransNested2(){
        User user1 = createUser("三狗子");
        userService.addNested(user1);
        User user2 = createUser("雅少");
        userService.addNestedException(user2);
    }

結論:外圍方法未開啟事務的情況下Propagation.NESTEDPropagation.REQUIRED作用相同,修飾的內部方法都會新開啟自己的事務,且開啟的事務相互獨立,互不干擾。

外部有事務:

場景三:

    @Transactional
    public void doHandleTransNested3(){
        User user1 = createUser("三狗子");
        userService.addNested(user1);
        User user2 = createUser("雅少");
        userService.addNested(user2);
        throw new RuntimeException();
    }

場景四:

    @Transactional
    public void doHandleTransNested4(){
        User user1 = createUser("三狗子");
        userService.addNested(user1);
        User user2 = createUser("雅少");
        userService.addNestedException(user2);
    }

場景五:

    @Transactional
    public void doHandleTransNested5(){
        User user1 = createUser("三狗子");
        userService.addNested(user1);
        User user2 = createUser("雅少");
        try{
            userService.addNestedException(user2);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

結論:外圍主事務回滾,子事務一定回滾,而內部子事務可以單獨回滾而不影響外圍主事務和其他子事務。

 

區別:

1、NESTED和REQUIRED修飾的內部方法都屬於外圍方法事務,如果外圍方法拋出異常,這兩種方法的事務都會被回滾。但是REQUIRED是加入外圍方法事務,所以和外圍事務同屬於一個事務,一旦REQUIRED事務拋出異常被回滾,外圍方法事務也將被回滾。而NESTED是外圍方法的子事務,有單獨的保存點,所以NESTED方法拋出異常被回滾,不會影響到外圍方法的事務。

2、NESTED和REQUIRES_NEW都可以做到內部方法事務回滾而不影響外圍方法事務。但是因為NESTED是嵌套事務,所以外圍方法回滾之后,作為外圍方法事務的子事務也會被回滾。而REQUIRES_NEW是通過開啟新的事務實現的,內部事務和外圍事務是兩個事務,外圍事務回滾不會影響內部事務。

 

參考:https://segmentfault.com/a/1190000013341344

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM