Springboot中的數據庫事務


Springboot中的數據庫事務

對於一些業務網站而言 , 產 品庫存的扣減、 交易記錄以及賬戶都必須是要么 同時成功, 要么 同時失敗 ,這便是一種事務機制,而在一些特殊的場景下 ,如一個批處理 ,它將處理多個交易 ,但是在一些交易中發生了異常 , 這個時候則不能將所有的交易都回滾。如果所有的交易都回瀆,那么那些本能夠正常處理的業務也無端地被回滾。 通過 Spring 的數據庫事務傳播行為,可以很方便地處理這樣的場景 。

首先配置數據庫信息

spring.datasource.url=jdbc:mysql://localhost:3306/demo
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5

一、JDBC數據庫事務

package  com.demo.servicee.impl

@Service
public class JdbcServiceImpl implements JdbcService{

	@Autowired
	private DataSource dataSource=null;

	@Override
	public int insertUser(String name,String note){
        Connection conn=null;
        int result=0;
        try{
            //獲取連接
            conn=dataSource.getConnection();
            //開啟事務
            conn.setAutoCommit(false);
            //設置隔離級別
            conn.setTransactionIsolation(TransactionIsolationLevel.RRAD_COMMITED.getLevel());
            //執行SQL
            PreparedStatement ps=conn.prepareStatement("insert into t_user(user_name,note)values(?,?)");
            ps.setString(1,userName);
            ps,setString(2,note);
            result=ps.executeUpdate();
            //提交事務
            conn.commit();
        }catch(Exception e){
            //回滾事務
            if(conn !=null){
                try{
                    conn.rollback();
                }catch(SqlException e1)
                    e1.printStackTrace();
                }
            }
            e.printStackTrace();
        }finally{
            try{
                if(conn !=null && !conn.isClosed()){
                    conn.close()
                }
            }catch(SQLException e){
                e.printStackTrace();
            }
        }
        return result;
	}
}

使用JDBC需要使用大量的try...catch...finally...語句,和關於連接的獲取關閉,事務的提交和回滾。使用Hibernate、myBatis可以減少try...catch...finally的使用,但是依舊不能減少開閉數據庫連接和事務控制的代碼。而AOP可以解決這樣的問題。

二、Spring 聲明式事務的使用

Spring AOP 會把我們的代碼織入到約定的流程中,同樣,同樣執行的SQL的代碼也可以織入的哦Spring 約定的數據庫事務的流程中。首先要掌握這個約定

1.Spring 聲明式數據庫事務約定

對於事務需要通過標注告訴Spring在什么地方啟用數據庫事務功能,對於聲明式數據庫,是使用@Transactional進行標注的。

@Transactional 這個注解可以標注類和方法上,當它標注在類上時,代表這個類所有公共(public)非靜態的方法東將啟用事務功能。在@Transactonal 中還可以進行事務的隔離級別和傳播行為,異常類型的配置。這些配置,是在Sprng IoC容器在加載時就會將這些配置信息解析出來,然后幫這些喜喜存儲到事務定義器(TransactonDefinition接口實現的類)里,並且記錄了那些類或者方法需要啟動事務的功能,采取什么策略去執行事務。在這個過程中我們所需要做的就是給需要事務的類和方法標注@Transactional並配置屬性

spring的事務處理機制

Spring 通過對注解@Transactional 屬性配置去設置數據庫事務 , 跟着 Spring 就會
開始調用開發者編寫 的業務代碼 。 執行開發者 的業務代碼,可能發生異常,也可能不發生異常 。 在Spring 數據庫事務 的流程 中,它會根據是否發生異常采取不同的策略 如果都沒有發生異常, Spring 數據庫攔截器就會幫助我們提交事務 , 這點也並不需要我們干預 。如果發生異常,就要判斷一次事務定義器內的配置,如果事務定義器己經約定了該類型的異常不回段事務就提交事務 , 如果沒有任何配置或者不是配置不回滾事務的異常,則會回滾事務,並且將異常拋出 , 這步也是由事務攔截器完成的。論發生異常與否, Spring 都會釋放事務資源,這樣就可以保證數據庫連接池正常可用了,這
也是由 Spring 事務攔截器完成的內容 。

public class UserServiceImpl implements UserService{
    
    @Autowired
    private UserDao userDao=null;
    
    @Override
    @Transactional
    public int insertUser(User user){
        return userDao.insertUser(user);
    }
}

2.@Transactional配置項

spring中關於數據庫屬性是由@Transactional 來配置的,源碼如下所示

package org.springframework.transaction.annotation;
/**** imports****/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetenttionPolicy.RUNTIME)
@InHerited
@Documented
public @interface Transactional{
    //通過bean name指定事務管理器
    @ALiasFor("transactionManager")
    String value() default"";
    
    //同value屬性
    @ALiasFor(value)
    String transactionManager() default"";
    
    //指定傳播行為
    Propagation propagation() default Propagetion.REQUIRED;
    
    //指定隔離級別
    Isolation isolation() default IsoLation.DEFAULT
    
    //指定超時時間(單位為秒)
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
        
    //時候只讀事務
    boolean readOnly() default false;
    
    //方法發生指定異常時回滾,默認是所有的異常都回滾
    Class<? extends Throwable>[] rollbackFor() default();
    
    //方法發生指定異常名稱時回滾,默認是所有異常都回滾
    String[] rollbackForClassName() default();
    
    //方法發生指定異常時不回滾,默認是所有的 異常都回滾
    Class<? extends Throwable>[] noRollBackFor() default();
    
    //方法在發生指定異常名稱時不回滾,默認所有異常都回滾
    String noRollbackForClassName() default();
}

  • value和transactionManager屬性是配置一個Spring的事務管理器
  • timeout是事務允許存在的時間戳
  • read Only 屬性 定義的是事務是否是只讀事務;
  • rollbackFor 、 rollbackForClassName 、 noRollbackFor 和 noRollbackForClassName 都是指定異常 ,我們從流程中可以看到在帶有事務的方法時,可能發生異常,通過這些屬性的設置可以指定在什么異常的情況下依舊提交事務,在什么異常的情況下回滾事務 , 這些可以根據自己的需要進行指定
  • propagation 傳播行為(重點)
  • isolation隔離級別(重點)

@Transactional可以放在接口上也可以放在實現類上,spring推薦放在實現類上。

3.Spring 事務管理器

在spring的事務流程中事務的打開回滾和提交是由事務管理器來完成的。事務管理器的頂層接口是PlatformTransactionManager.

當我們使用myBatis框架時最常用的的是DataSourceTransactionManager它實現了PlatformTransactionManager接口,關於PlatformTransactionManager的源碼如下

package org.springframework transaction;

public interface PlatformTransactionManager{
    //獲取事務,它還會設置數據屬性
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    
    //提交事務
    void commit(TransactionStatus status) throws TransactionException;          
    
    //回滾事務
    void rollback(TransactionStatus status) throws TransactionException;
}

Spring在事務管理時,就是將這些方法按照約定織入對應的流程,其中getTransaction方法的參數是一個事務定義器,它依賴於我們配置的@Transactional的配置項生成的。於是通過它就能夠設置事務的屬性了

在 Spring Boot 中,當你依賴於 mybatis-spring-boot-starter 之后 , 它會自動創建一個 DataSourceTransactionManager 對象 ,作為事務管理器 ,如果依賴於 spring-boot-starter-data-j pa ,則它會自動創建JpaTransactionManager 對象作為事務管理器 ,所以我們一般不需要自己創建事務管理器而直接使用它們即可。

4.事務的使用

創建一張表

create table t_user(
	id int(12) auto_increment,
	user_name varchar(60) not null,
	note varchar(512),
	primary key(id)
);

創建POJO

package com.demo.pojo
@Alias("user")
public class User{
    private Long id;
    private String userName;
    private String note;
    
    /**** setter and getter****/
}

myBatis接口

package com.demo.dao

@Repository
public interface UserDao{
    User getUser(Long id);
    int intsertUser(User user);
}

mapper映射文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper	
	PUBLIC "- //mybatis . org//DTD Mapper 3 . 0//EN"
	" http : //mybatis . org/dtd/mybat 工 s - 3 - mapper . dtd ">
<mapper namespace="com.demo.dao.UserDao">
	<select id="getUser" parameterType="long" resultType="user">
		select id,user_name	user,note from t_user where id= #{id}
	</select>
	
	<insert id="insertUser" useGeneratedKays="true" resulType="user">
		insert into t_user(user_name,note)value(#{userName},#{note})
	</insert>
</mapper>

服務接口

package com.demo.service

public interface UserService{
    //獲取用戶信息
    public User getUser(Long id);
    //新增用戶
    public int inserUser(User user);
}

服務接口實現類

package com.demo.service.demo

@Service
public class UserServiceImpl implements UserService{	
    @Autowired
    private UserDao userDao=null;
    
    @Override
    @Transactional(isolation=Isolation.READ_COMMITTED,timeout=1)
    public int insertUser(User user){
    	return userDao.insertUser(user);
    }
    
    @Override
    @Transactional(isolation=Isolation.READ_COMMITTED,timeout=1)
    public User insertUser(id){
        return userDao.getUser(id);
    }
}

代碼中的方法上標注了注解@Transactional , 意味着這兩個方法將啟用 Spring 數據庫事務機制。在事務配置中,采用了讀寫提交的隔離級別

編寫Controller

package com.springboot.chapter6.controller;

@Controller
@RequestMapping("/user")
public class UserController{
    @AutoWired
    private UserSevice userService=null;
    
    @RequestMapping("/getUser")
    @ResposeBody
    public  user getUser(Long id){
        return userService.getUser(id);
    }
    
    @RequestMapping("/insertUser")
    @ResponseBody
    public Mapping<String,Object> insertUser(String userName,String note){
        User user=new User();
        user.setUserName(userName);
        user.setNote(note);
		int update = userService.insertUser(user);
		Map<String,Object> result =new HaspMap();
		result.put("success",update==1);
		result.put("user",user);
		return result;
    }
}

在application.properties中添加配置

mybatis.mapper-locations=classpath:com/demo.mapper/*xml
mybatis.type-aliases-package=com.demo.pojo

依賴於 mybatis叩ring-boot”starter 之后, Spring Boot 會自動創建事務管理器、 MyBatis 的 SqlSessionFactory 和 SqlSessionTemplate 等 內容 。 下面我們需要配置 SpringBoot 的 運行文件 ,

package com.demo.main

@MapperScan(
	basePackages="com.demo",
	annotationClass=Repository.class
)
@SpringBootApplication(scanBasePackage="com.demo")
public class DemoApplication{
    public static void main(String[] args)throws Exception{
        SpringApplication.run(DemoApplication.class,args);
    }
}

先這里使用 了 @MapperScan 掃描對應的包 , 並限定了只有被注解@Repository 標注的接口 ,這樣就可以把 MyBati s 對應的接口文件掃描到 Spring IoC 容器 中 了

三、隔離級別

Spring事務機制中最重要的兩個配置項,隔離級別和傳播行為。

對於隔離級別,因為互聯網應用時刻面對着高並發的環境 ,時刻都是多個線程共
享的數據。對於數據庫而言 , 就會出現多個事務同時訪問同一記錄的情況 , 這樣引起數據 出 現不一致的情況,便是數據庫的丟失更新( Lost Update ) 問題。應該說 , 隔離級別是數據庫的概念 。

1.數據庫事務相關知識

數據庫的的4個基本特征,ACID

  • Atomic(原子性):事務中包含的操作被看作一個整體的業務單元,這個業務單元中的操作要么全部成功、要么全部失敗,不會出現部分失敗,部分成功的場景。
  • Consistency(一致性):在事務完成時,必須使所有的數據保持一致狀態
  • Isolation(隔離性):由於多個引用程序線程訪問同一數據,這樣數據庫的數據就會在各個不同的事務中被訪問。這樣會產生丟失更新。為了壓制丟失更新的產生,數據庫定義了隔離級別的概念,通過它的選擇,可以在不同程度上壓制丟失更新的發生。
  • Durability(持久性):事務結束后,所有的數據會固化到一個地方,如保存到磁盤中。

下面深入討論隔離性

在多個事務同時操作數據庫的情況下會引發丟失更新的場景,例如,電商有一種商品電商有一種商品,在瘋狂搶購中,會出現多個事務同時訪問商品庫存的場景 ,這樣就會產生丟失更新。一般而言 ,存在兩種類型的丟失更新 ,讓我們 了解下它們 。 下面假設一種商品的庫存數量還有 100 , 每次搶購都只能搶購 l 件商品,那么在搶購中就可能出現如下的場景。

時刻 事務1 事務2
T1 初始庫存100 初始庫存100
T2 庫存-1,余99 ......
T3 ...... 庫存-1余99
T4 提交事務,庫存變為99
T5 回滾事務,庫存100

對於這樣一個事務回滾一個事務提交引發的數據不一致的情況,我們稱為第一類丟失更新。現今大部分的數據庫都已經克服了第一類丟失更新的問題

時刻 事務1 事務2
T1 初始庫存100 初始庫存100
T2 庫存-1,余99 ......
T3 ...... 庫存-1余99
T4 ...... 提交事務,庫存變為99
T5 提交事務,庫存變為99

對於這樣多個事務都提交引發的丟失更新稱為第二類丟失更新

2.隔離級別詳解

我們 主要針對第二種丟失更新,為了壓制丟失更新,數據庫標准提出了4類不同的隔離級別,分別為未提交讀(read uncommitted)、讀寫提交(read commited)、可重復讀和串行化。提出4種不同的隔離級別是出於性能的考慮

未提交讀(read uncommitted)會產生臟讀

未提交讀是最低的隔離級別,其含義是允許一個事務讀取另外一個事務沒有提交的數據。未提交讀是一種危險的隔離級別,所以一般在我們實際的開發中應用不廣 , 但是它的優點在於並發能力高,適合那些對數據一致性沒有要求而追求高並發的場景 ,它的最大壞處是出現臟讀 。

讀寫提交(read committed)會產生不可重復讀

讀寫提交隔離級別,是指一個事務只能讀取另一個事務已經提交的數據,不能讀取未提交的數據。

可重復讀 會產生幻讀

可重復讀的目標是克服讀寫提交中出現的不可重復讀的現象,因為在讀寫提交的時候,可能出現一些值的變化, 影響當前事務的執行,如上述的庫存是個變化的值,這個時候數據庫提出 了可重復讀的隔離級別

串行化(Serializable)

串行化(Serializable)是數據庫最高的隔離級別,它會要求所有的 SQL 都會按照順序執行,這樣就可以克服上述隔離級別出現的各種問題,所以它能夠完全保證數據的一致性 。

使用合理的隔離級別

關於隔離級別和可能發生的現象

項目類型 臟讀 不可重復讀 幻讀
未提交讀 o o o
讀寫提交 x o o
可重復度 x x o
串行化 x x x

追求更高的隔離級別,它能更好地保證了數據的一致性,但是也要付出鎖的代價 。 有了鎖,就意味着性能的丟失,而且隔離級別越高,性能就越是直線地下降 所以在現實中一般而言,選擇隔離級別會以讀寫提交為主,它能夠防止臟讀,而不能避免不可重復讀和幻讀。為了克服數據不一致和性能問題,程序開發者還設計了樂觀鎖,甚至不再使用數據庫而使用其他的手段,例如,使用 Redis 作為數據載體

關於隔離級別的使用

@Transactional(isolation=Isolationn.SERIALIZABLE)

public int insertUser(User user){
    return userDao.insertUser(user);
}

也可以在application.properties中指定默認的隔離級別

#隔離級別數字配置的含義:
#-1		數據庫默認隔離級別
#1		未提交讀
#2		讀寫提交
#4		可重復讀
#8		串行化
#tomcat數據源默認隔離級別
#spring.datasource.tomcat.default-transaction-isolation=2
#dbcp2數據庫連接池默認隔離級別
#spring.datasource.dbcp2.default-transaction-isolation=2

四、傳播行為

傳播行為是方法之間調用事務采取的策略問題 。 在絕大部分的情況下,我們會認為數據庫事務要么全部成功 , 要么全部失敗。但現實中也許會有特殊的情況。例如,執行一個批量程序,它會處理很多 的交易,絕大部分交易是可以順利完成的,但是也有極少數的交易因為特殊原因不能完成而發生異常,這時我們不應該因為極少數的交易不能完成而回滾批量任務調用的其他交易,使得那些本能完成的交易也變為不能完成了 。 此時,我們真實的需求是,在一個批量任務執行的過程中,調用多個交易時,如果有一些交易發生異常 ,只是回滾那些出現異常的交易,而不是整個批量任務,這樣就能夠使得那些沒有 問題的交易可以順利完成,而有問題的交易則不做任何事情 。

在 Spring 中, 當一個方法調用另外一個方法時,可以讓事務采取不同的策略工作,如新建事務或者掛起當前事務等,這便是事務的傳播行為。
批量任務我們稱之為當前方法,那么批量事務就稱為當前事務,當它調用單個交易時,稱單個交易為子方法,當前方法調用子方法的時候,讓每一個子方法不在當前事務 中執行,而是創建一個新的事務去執行子方法,我們就說當前方法調用子方法的傳播行為為新建事務。此外 , 還可能讓子方法在無事務、獨立事務中執行,這些完全取決於你的業務需求。

1.傳播行為的定義

在Spring事務機制中對數據庫存在7種傳播行為。它是通過枚舉類Propagation定義的,其源碼為

pacakage org.springframework.transaction.annotation;
/****imports****/
public enum Propagation{
    //需要事務,它是默認傳播行為,如果當前存在事務,就沿用當前事務。否則新建一個事務運行子方法
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
    
    //支持事務,如果當前存在事務,就沿用當前事務,如果不存在,則無事務的方式運行子方法
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
    
    //須使用事務,如果當前沒有事務,則會拋出異常,如果存在當前事務 , 就沿用當前事務
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
    
    //無論當前事務是否存在,都會創建新事務運行方法,這樣新事務就可以擁有新的鎖和隔離級別等特性,與當前事務相互獨立
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    
    //不支持事務,當前存在事務時,將掛起事務,運行方法
    NOT_SUPPORTS(TransactionDefinition.PROPAGATION_NOT_SUPPORTS),
    
    //不支持事務,如果當前方法存在事務,則拋出異常,否則繼續使用無事務機制運行
    NEVER(TransactionDefinition.PROPAGATION_NEVER),
    
    //在當前方法調用子方法時,如果子方法發生異常,只回滾子方法執行過的SQL,而不回滾當前方法的事務
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
    
    private final int value;
    Propagetion(int value){this Value=value;}
    public int value(){
        return this.value;
    }
}


免責聲明!

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



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