0108 spring的申明式事務

背景

互聯網的金融和電商行業,最關注數據庫事務。
業務核心 說明
金融行業-金融產品金額 不容許發生錯誤
電商行業-商品交易金額,商品庫存 不容許發生錯誤

面臨的難點:java

高併發下保證: 數據一致性,高性能;

spring對事物的處理:mysql

採用AOP技術提供事務支持,申明式事務,去除了代碼中重複的try-catch-finally代碼;

兩個場景的解決方案:git

場景 解決辦法
庫存扣減,交易記錄,帳戶金額的數據一致性 數據庫事務保證一致性
批量處理部分任務失敗不影響批量任務的回滾 數據庫事務傳播行爲

jdbc處理事務

代碼github

package com.springbootpractice.demo.demo_jdbc_tx.biz;
import lombok.SneakyThrows;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Objects;
import java.util.Optional;
/**
 * 說明:代碼方式事務編程 VS 申明式事物編程
 * @author carter
 * 建立時間: 2020年01月08日 11:02 上午
 **/
@Service
public class TxJdbcBiz {
    private final JdbcTemplate jdbcTemplate;

    public TxJdbcBiz(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @SneakyThrows
    public int insertUserLogin(String username, String note) {
        Connection connection = null;
        int result = 0;
        try {
            connection = Objects.requireNonNull(jdbcTemplate.getDataSource()).getConnection();

            connection.setAutoCommit(false);

            connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);

            final PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO user_login(user_name,password,sex,note)  VALUES(?,?,?,?)");

            preparedStatement.setString(1, username);
            preparedStatement.setString(2, "abc123");
            preparedStatement.setInt(3, 1);
            preparedStatement.setString(4, note);

            result = preparedStatement.executeUpdate();

            connection.commit();
        } catch (Exception e) {
            Optional.ofNullable(connection)
                    .ifPresent(item -> {
                        try {
                            item.rollback();
                        } catch (SQLException ex) {
                            ex.printStackTrace();
                        }
                    });
            e.printStackTrace();
        } finally {
            Optional.ofNullable(connection)
                    .filter(this::closeConnection)
                    .ifPresent(item -> {
                        try {
                            item.close();
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    });
        }
        return result;
    }
    private boolean closeConnection(Connection item) {
        try {
            return !item.isClosed();
        } catch (SQLException e) {
            e.printStackTrace();
            return false;
        }
    }
    @Transactional
    public int insertUserLoginTransaction(String username, String note) {
        String sql = "INSERT INTO user_login(user_name,password,sex,note)  VALUES(?,?,?,?)";
        Object[] params = {username, "abc123", 1, note};
        return jdbcTemplate.update(sql, params);
    }
}

測試代碼redis

package com.springbootpractice.demo.demo_jdbc_tx;
import com.springbootpractice.demo.demo_jdbc_tx.biz.TxJdbcBiz;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.TransactionManager;
import org.springframework.util.Assert;
@SpringBootTest
class DemoJdbcTxApplicationTests {
    @Autowired
    private TxJdbcBiz txJdbcBiz;
    @Autowired
    private TransactionManager transactionManager;

    @Test
    void testInsertUserTest() {
        final int result = txJdbcBiz.insertUserLogin("monika.smith", "xxxx");
        Assert.isTrue(result > 0, "插入失敗");
    }

    @Test
    void insertUserLoginTransactionTest() {
        final int result = txJdbcBiz.insertUserLoginTransaction("stefan.li", "hello transaction");
        Assert.isTrue(result > 0, "插入失敗");
    }

    @Test
    void transactionManagerTest() {
        System.out.println(transactionManager.getClass().getName());
    }
}
代碼中有一個很討厭的地方,就是 try-catch-finally;

流程圖spring

graph TD
A[開始] --> B(開啓事務)
B --> C{執行SQL}
C -->|發生異常| D[事務回滾]
C -->|正常| E[事物提交]
D --> F[釋放事務資源]
E --> F[釋放事務資源]
F --> G[結束]
總體流程跟AOP的流程很是的類似,使用AOP,能夠把執行sql的步驟抽取出來單獨實現,其它的固定流程放到通知裏去作。

jdbc使用事物編程代碼點我!sql

申明式事務

經過註解@Transaction來標註申明式事務,能夠標準在類或者方法上;
@Tranaction使用位置 說明
類上或者接口上 類中全部的 公共非靜態方法 都將啓用事務,spring推薦放在實現類上,不然aop必須基於接口的代理生效的時候才能生效
方法上 本方法

@Transaction的源碼和配置項數據庫

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

說明:編程

屬性 說明
isolation 事務的隔離級別
propagation 傳播行爲
rollbackFor,rollbakcForClassName 哪一種異常會觸發事務回滾
value 事務管理器
timeout 事務超時時間
readOnly 是不是隻讀事務
noRollbackFor,noRollbackForClassName 哪些異常不會觸發事務回滾

事務的安裝過程:api

springIOC容器啓動的時候,會把@Transactional註解的配置信息解析出來,而後存到事務定義器(TransactionDefinition),並記錄哪些類的方法須要啓動事務,採起什麼策略去執行事務。

咱們要作的只是標註@Transactional和配置屬性便可;

流程如圖:

graph TD
A[開始] --> B(開啓和設置事務)
B --> C{執行方法邏輯}
C -->|發生異常| D[事務回滾]
C -->|正常| E[事物提交]
D --> F[釋放事務資源]
E --> F[釋放事務資源]
F --> G[結束]
使用方式大大簡化;

代碼

@Transactional
    public int insertUserLoginTransaction(String username, String note) {
        String sql = "INSERT INTO user_login(user_name,password,sex,note)  VALUES(?,?,?,?)";
        Object[] params = {username, "abc123", 1, note};
        return jdbcTemplate.update(sql, params);
    }

事務管理器

事務的打開,提交,回滾都是放在事務管理器上的。TransactionManager;

TransactionManager代碼

package org.springframework.transaction;

public interface TransactionManager {
}
這是一個空接口,實際起做用的是PlatfromTransactionManager;

PlatfromTransactionManager代碼:

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;

    void commit(TransactionStatus var1) throws TransactionException;

    void rollback(TransactionStatus var1) throws TransactionException;
}

3個架子的事務管理器對比:

架子 事務管理 說明
spring-jdbc DatasourceTransactionManager
jpa JpaTransactionManager
mybatis DatasourceTransactionManager

mybatis的代碼實例點我!

事務隔離級別

場景:電商行業的庫存扣減,時刻都是多線程的環境中扣減庫存,對於數據庫而言,就會出現多個事務同事訪問同一記錄,這樣引發的數據不一致的狀況,就是數據庫丟失更新。

數據庫事務4個特性

即ACID
事務的特性 英文全稱 說明
原子性 Atomic 一個事務中包含多個步驟操做A,B,C,原子性是標識這些操做要目所有成功,要麼所有失敗,不會出現第三種狀況
一致性 Consistency 在事務完成後,全部的數據都保持一致狀態
隔離性 Isolation 多個線程同時訪問同一數據,每一個線程處在不一樣的事務中,爲了壓制丟失更新的產生,定了隔離級別,經過隔離性的設置,能夠壓制丟失更新的發生,這裏存在一個選擇的過程
持久性 Durability 事務結束後,數據都會持久化,斷電重啓後也是能夠提供給程序繼續使用

隔離級別:

隔離級別 說明 問題 併發性能
讀未提交【read uncommitted】 容許事務讀取另一個事務沒有提交的數據,事務要求比較高的狀況下不適用,適用於對事務要求不高的場景 髒讀(單條) 併發性能最高
讀已提交【read committed】 一個事務只能讀取另一個事務已經提交的數據 不可重複讀(單條) 併發性能通常
可重複讀【read repeated】 事務提交的時候也會判斷最新的值是否變化 幻想讀(多條數據而言) 併發性能比較差
串行化【serializable】 全部的sql都按照順序執行 數據徹底一致 併發性能最差

選擇依據

隔離級別 髒讀 不可重複讀 幻象讀
讀未提交
讀已提交
可重複讀
串行化

按照實際場景的容許狀況來設置事務的隔離級別;

隔離級別會帶來鎖的代價;優化方法:

  1. 樂觀鎖,
  2. redis分佈式鎖,
  3. zk分佈式鎖;
數據庫 事務隔離級別 默認事務隔離級別
mysql 4種 可重複讀
oracle 讀已提交,串行化 讀已提交
springboot配置應用默認的事務隔離級別:spring.datasource.xxx.default-transaction-isolation=2
數字 對應隔離級別
-1
1 讀未提交
2 讀已提交
4 可重複讀
8 串行化

事務傳播行爲

傳播行爲是方法之間調用事務採起的策略問題。
場景:一個批量任務處在一個事務A中,每一個單獨是事務都有一個獨立的事務Bn; 子任務的回滾不影響事務A的回滾;

傳播行爲源碼

package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
public enum Propagation {
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
    private final int value;
    Propagation(int value) {
        this.value = value;
    }
    public int value() {
        return this.value;
    }
}

列舉了7種傳播配置屬性,下面分別說明:

傳播行爲 父方法中存在事務子方法行爲 父方法中不存在事務子方法行爲
REQUIRED 默認傳播行爲,沿用, 建立新的事務
SUPPORTS 沿用; 無事務,子方法中也無事務
MANDATORY 沿用 拋出異常
REQUIRES_NEW 建立新事務 建立新事務
NOT_SUPPORTED 掛起事務,運行子方法 無事務,運行子方法
NEVER 拋異常 無事務執行子方法
NESTED 子方法發生異常,只回滾子方法的sql,而不回滾父方法中的事務 發生異常,只回滾子方法的sql,跟父方法無關

經常使用的三種傳播行爲:

  • REQUIRED
  • REQUIRES_NEW
  • NESTED

代碼測試這三種傳播行爲:

代碼點我!

spring使用了save point的技術來讓子事務回滾,而父事務不會滾;若是不支持save point,則新建一個事務來運行子事務;
區別點 RequestNew Nested
傳遞 擁有本身的鎖和隔離級別 沿用父事務的隔離級別和鎖

@Transaction自調用失效問題

事務的實現原理是基於AOP,同一個類中方法的互相調用,是本身調用本身,而沒有代理對象的產生,就不會用到aop,因此,事務會失效;
解決辦法:經過spring的ioc容器獲得當前類的代理對象,調用本類的方法解決; 原創不易,轉載請註明出處,歡迎溝通交流。
相關文章
相關標籤/搜索