SSM框架(3):配置Spring的事務管理器,實現事務控制


 一、相關概念

1、不可重復讀 和 幻讀 的區別

  很多人容易搞混不可重復讀和幻讀,確實這兩者有些相似。但不可重復讀重點在於update和delete,而幻讀的重點在於insert

  如果使用鎖機制來實現這兩種隔離級別,在可重復讀中,該sql第一次讀取到數據后,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重復 讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會 發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這么做可以有效的避免幻讀、不可重復讀、臟讀等問題,但會極大的降低數據庫的並發能力。

  所以說不可重復讀和幻讀最大的區別,就在於如何通過鎖機制來解決他們產生的問題

  上文說的,是使用悲觀鎖機制來處理這兩種問題,但是MySQL、ORACLE、PostgreSQL等成熟的數據庫,出於性能考慮,都是使用了以樂觀鎖為理論基礎的MVCC(多版本並發控制)來避免這兩種問題。

  當然, 從總的結果來看, 似乎兩者都表現為兩次讀取的結果不一致。但如果你從控制的角度來看, 兩者的區別就比較大。

  對於前者, 只需要鎖住滿足條件的記錄

  對於后者, 要鎖住滿足條件及其相近的記錄

 

2、什么是:悲觀鎖和樂觀鎖?

  • 悲觀鎖:正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處 於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機 制,也無法保證外部系統不會修改數據)。在悲觀鎖的情況下,為了保證事務的隔離性,就需要一致性鎖定讀。讀取數據時給加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。
  • 樂觀鎖:相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如 果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
  • 要說明的是,MVCC的實現沒有固定的規范,每個數據庫都會有不同的實現方式,這里討論的是InnoDB的MVCC。

     以上內容轉載自不可重復讀和幻讀的區別

 

3、什么是事務?事務有什么要求或特征?

  大家所了解的事務Transaction,它是一些列嚴密操作動作,要么都操作完成,要么都回滾撤銷。Spring事務管理基於底層數據庫本身的事務處理機制。數據庫事務的基礎,是掌握Spring事務管理的基礎。這篇總結下Spring事務。
事務具備ACID四種特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔離性)和Durability(持久性)的英文縮寫。
(1)原子性(Atomicity)
  事務最基本的操作單元,要么全部成功,要么全部失敗,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
(2)一致性(Consistency)
  事務的一致性指的是在一個事務執行之前和執行之后數據庫都必須處於一致性狀態。如果事務成功地完成,那么系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那么系統中的所有變化將自動地回滾,系統返回到原始狀態。
(3)隔離性(Isolation)
  指的是在並發環境中,當不同的事務同時操縱相同的數據時,每個事務都有各自的完整數據空間。由並發事務所做的修改必須與任何其他並發事務所做的修改隔離。事務查看數據更新時,數據所處的狀態要么是另一事務修改它之前的狀態,要么是另一事務修改它之后的狀態,事務不會查看到中間狀態的數據。
(4)持久性(Durability)
  指的是只要事務成功結束,它對數據庫所做的更新就必須永久保存下來。即使發生系統崩潰,重新啟動數據庫系統后,數據庫還能恢復到事務成功結束時的狀態。


4、事務的傳播特性

  事務傳播行為就是多個事務方法調用時,如何定義方法間事務的傳播。Spring定義了7中傳播行為:
(1)propagation_requierd:如果當前沒有事務,就新建一個事務,如果已存在一個事務中,加入到這個事務中,這是Spring默認的選擇。
(2)propagation_supports:支持當前事務,如果沒有當前事務,就以非事務方法執行。
(3)propagation_mandatory:使用當前事務,如果沒有當前事務,就拋出異常。
(4)propagation_required_new:新建事務,如果當前存在事務,把當前事務掛起。
(5)propagation_not_supported:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。
(6)propagation_never:以非事務方式執行操作,如果當前事務存在則拋出異常。
(7)propagation_nested:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與propagation_required類似的操作。

 

5、事務的隔離級別

(1)read uncommited:是最低的事務隔離級別,它允許另外一個事務可以看到這個事務未提交的數據。
(2)read commited:保證一個事物提交后才能被另外一個事務讀取。另外一個事務不能讀取該事物未提交的數據。
(3)repeatable read:這種事務隔離級別可以防止臟讀,不可重復讀。但是可能會出現幻象讀。它除了保證一個事務不能被另外一個事務讀取未提交的數據之外還避免了以下情況產生(不可重復讀)。
(4)serializable:這是花費最高代價但最可靠的事務隔離級別。事務被處理為順序執行。除了防止臟讀,不可重復讀之外,還避免了幻象讀
(5)臟讀、不可重復讀、幻象讀概念說明:
a.臟讀:指當一個事務正字訪問數據,並且對數據進行了修改,而這種數據還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據。因為這個數據還沒有提交那么另外一個事務讀取到的這個數據我們稱之為臟數據。依據臟數據所做的操作肯能是不正確的。
b.不可重復讀:指在一個事務內,多次讀同一數據。在這個事務還沒有執行結束,另外一個事務也訪問該同一數據,那么在第一個事務中的兩次讀取數據之間,由於第二個事務的修改第一個事務兩次讀到的數據可能是不一樣的,這樣就發生了在一個事物內兩次連續讀到的數據是不一樣的,這種情況被稱為是不可重復讀。
c.幻象讀:一個事務先后讀取一個范圍的記錄,但兩次讀取的紀錄數不同,我們稱之為幻象讀(兩次執行同一條 select 語句會出現不同的結果,第二次讀會增加一數據行,並沒有說這兩次執行是在同一個事務中)


六、事務幾種實現方式

(1)編程式事務管理對基於 POJO 的應用來說是唯一選擇。我們需要在代碼中調用beginTransaction()、commit()、rollback()等事務管理相關的方法,這就是編程式事務管理。目前,編程式事務用的相對比較少;
(2)基於 TransactionProxyFactoryBean的聲明式事務管理
(3)基於 @Transactional 的聲明式事務管理
(4)基於Aspectj AOP配置事務

     以上內容轉載自:Spring事務管理之幾種方式實現事務

 

七、為什么事務操作注解,要添加在Service實現類上,而不是在service接口 或 Controller中呢?

  使用注解的方式進行事務控制時,將@Transactional注解寫在實現類的方法或類上!不建議寫在接口類中! 
  Spring團隊的建議是你在具體的類(或類的方法)上使用 @Transactional 注解,而不要使用在類所要實現的任何接口上。你當然可以在接口上使用 @Transactional 注解,但是這將只能當你設置了基於接口的代理時它才生效。因為注解是不能繼承的,這就意味着如果你正在使用基於類的代理時,那么事務的設置將不能被基於類的代理所識別,而且對象也將不會被事務代理所包裝(將被確認為嚴重的)。因此,請接受Spring團隊的建議並且在具體的類上使用 @Transactional 注解。  

  在以上解決方法中,若將@Transactional 注解寫在接口上,則無法實現事務。所以請將 @Transactional 注解寫在實現類中!

 

二、實踐中遇到的問題 及 總結

 1、@Transactional注解事務無效的幾種可能性。

  (1)Springs事務控制器,默認情況下監聽的是運行時異常及其子類異常。如果異常不是運行時異常,可以在配置事務時,顯示地設置事務監聽的異常類型(rollbackOn = Exception.class)

  (2)是不是配置文件的沒配置好,比如Bean,比如mapper.xml:

<!-- 配置事物管理類 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource" />
</bean>
<!-- 開啟注解式事物掃描 -->
<tx:annotation-driven transaction-manager="transactionManager" />

  (3)還有一種情況,因為我以前寫的都是對一個數據庫的操作,現在是一個數據源多個數據庫操作,當時有點懷疑我是不是少配置了什么,導致多數據庫時無法啟動事務,於是便還原到一個數據庫,結果事務還是無效,排除此問題

  經過一番折騰,在網上找到一篇文章,說原因是applicationContext.xml的父容器先於Servlet的子容器生效,將Service提前加載了。

  於是驗證了一下,首先去掉Service實現類的@Service注解,在spring.xml(也就是applicationContext.xml,我起名是spring.xml),配置該類的Bean:

  <bean id="sysUserServiceImp" class="cn.kx59.user.service.imp.SysUserServiceImp"></bean>
  結果運行之后:事務起作用了。

  原因如下: Spring容器優先加載由ServletContextListener(對應applicationContext.xml,我這里是spring.xml)產生的父容器,而SpringMVC(對應spring-mvc.xml)產生的是子容器。 子容器Controller進行掃描裝配時裝配的@Service注解的實例是沒有經過事務加強處理,即沒有事務處理能力的Service,而父容器進行初始化的Service是保證事務的增強處理能力的。如果不在子容器中將Service exclude掉,此時得到的將是原樣的無事務處理能力的Service。 所以我們要在掃描的時候在子容器中將Service exclude掉就好了。

也就是在spring-mvc.xml中進行如下修改:

<!--掃描Controller-->
<context:component-scan base-package="cn.kx59">
<context:include-filter type="annotation"
expression="org.springframework.stereotype.Controller" />
<!--下面這個是防止事務沒起作用,spring.xml的父容器先於Servlet的子容器生效,將Service提前加載了。這里不用再進行加載裝配-->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
</context:component-scan>

     以上內容轉載自:@Transaction注解后,事務未能成功

 

三、項目實踐

  業務描述:

  用戶Newbie有一張銀行卡 和 一份股票,數據分別存儲在銀行賬戶表 和 股票記錄表中。當用戶消費minusMoney元去購買股票時,銀行賬戶表中的余額減去minusMoney,而股票記錄表中的股票數增加minusMoney,將兩張表的修改動作綁定為事務進行控制。

  測試時,首先讓銀行賬戶表減去相應金額,並執行成功;然后,方法主動拋出異常,使得后續的修改股票記錄表的操作失敗,進行事務操作測試。

 

步驟一:(1)在數據庫中增加銀行賬戶表 和 股票記錄表;(2)在項目中增加對應的文件:pojo、beanMapper、beanMapper.xml。

          在項目中增加MyBatis-Generator的maven依賴,可以自動生成projo、mapper、mapper.xml三種文件。方式請參考以下鏈接:

     利用MyBatis-Generator自動生成實體類、Dao層和Mapper.xml的兩種方法總結

 

# 用戶銀行賬戶表
drop table if exists user_account;
create table user_account(
  account_id varchar(25) not null comment '賬戶ID',
  user_id varchar(25) not null comment '用戶ID',
  balance int  default 0 comment '賬戶余額',
  remark varchar(50) default '主庫master' comment '信息備注',
  primary key (account_id)
);
insert into user_account (account_id,user_id,balance) values ('6222-0001','newbie',100);

# 用戶股票記錄表
drop table if exists user_stock;
create table user_stock(
  stock_id varchar (25) not null comment '股票ID',
  user_id varchar (25) not null comment '用戶ID',
  stock_name varchar (32) comment '股票名稱',
  count_num int default 0 comment '擁有股的數量',
  remark varchar(50) default '主庫master' comment '信息備注',
  primary key (stock_id)
);
insert into user_stock (stock_id,user_id,stock_name,count_num) values ('AB-01','newbie','AB股','0');

 

步驟二:配置Spring提供的事務管理器DataSourceTransactionManager,並將數據源作為參數傳入事務管理器。實現事務控制的方式有兩種,實際項目開發中任選其一即可。

  方式一:開啟事務注解掃描功能,進行事務控制;

  方式二:使用AOP切面方式,植入事務控制;

    <!-- 配置事務管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dynamicDataSource"/>
    </bean>
<!--方式一:開啟事務注解掃描功能,進行事務管理 --> <tx:annotation-driven transaction-manager="transactionManager"/>

<!-- 方式二:使用AOP切面方式,植入事務控制 --> <!-- 使用aop方式,將事務植入到service方法上,實現事務操作 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 為連接點指定事務屬性 : 切入的方法、事務傳播行為、監聽的異常類型 --> <tx:method name="aopAdvice*" propagation="REQUIRED" rollback-for="Exception"/> </tx:attributes> </tx:advice> <!-- aop切入點設置 --> <aop:config> <aop:pointcut id="pointcutService" expression="execution(* *..service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcutService"/> </aop:config>

 

步驟三:編寫Service層業務邏輯,並在Service層中,實現事務的控制(增加事務注解、或植入aop切面)

package com.newbie.service.impl;

import com.newbie.dao.UserAccountMapper;
import com.newbie.dao.UserStockMapper;
import com.newbie.domain.UserAccount;
import com.newbie.domain.UserStock;
import com.newbie.service.ITransactionService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;

import javax.annotation.Resource;
import javax.transaction.Transactional;

/**
 * 單數據庫中的事務操作測試
 */
@Service
public class TransactionService implements ITransactionService {
    @Resource
    private UserAccountMapper userAccountMapper;
    @Resource
    private UserStockMapper userStockMapper;

    /**
     * 沒有增加事務控制
     */
    public void notTransactionCommit() throws Exception{
        updateDBData();
    }

    /**
     * 使用注解的方式,增加事務控制
     */
    @Transactional(rollbackOn = Exception.class)
    public void annotationCommit() throws Exception{
        updateDBData();
    }

    /**
     * 基於AOP切面的方式,增加事務控制
     */
    public void aopAdviceCommit() throws Exception{
        updateDBData();

    }

    /**
     * 查詢用戶的賬戶余額 和 股票票數
     * @return
     */
    public String queryData(){
        String accountId = "6222-0001"; //賬戶Id
        String stockId = "AB-01";       //股票ID
        //查詢用戶的賬戶余額 和 股票票數
        UserAccount account = userAccountMapper.selectByPrimaryKey(accountId);
        UserStock stock = userStockMapper.selectByPrimaryKey(stockId);
        String message = "<p>查詢成功<p>";
        message += "<p>賬戶Id : "+account.getAccountId()+" , 余額 : "+account.getBalance()+"</p>";
        message += "<p>股票Id:"+stock.getStockId()+" , 股票數 :"+stock.getCountNum()+"</p>";
        System.out.println("================== service : message = "+message);
        return message;
    }

    /**
     * 重置數據:將賬戶余額 和 股票票數 重置到原始狀態
     */
    public void updateResetData(){
        UserAccount account = new UserAccount();
        account.setAccountId("6222-0001");
        account.setBalance(100);
        UserStock stock = new UserStock();
        stock.setStockId("AB-01");
        stock.setCountNum(0);
        userAccountMapper.updateByPrimaryKeySelective(account);
        userStockMapper.updateByPrimaryKeySelective(stock);
    }

    /**
     * 修改數據
     * 第一步:查詢消費前,用戶的賬戶余額 和 股票票數
     * 第二步:修改賬戶余額,余額減去 20元
     * 第三步:修改股票票數,票數增加 20股
     */
    public void updateDBData() throws Exception{
        String accountId = "6222-0001"; //賬戶Id
        String stockId = "AB-01";       //股票ID
        int minusMoney = 20;            //消費金額
        //查詢消費前,用戶的賬戶余額 和 股票票數
        UserAccount account = userAccountMapper.selectByPrimaryKey(accountId);
        UserStock stock = userStockMapper.selectByPrimaryKey(stockId);
        //修改賬戶余額 和 股票票數
        account.setBalance(account.getBalance() - minusMoney);
        stock.setCountNum(stock.getCountNum() + minusMoney);
        //執行數據庫操作,完成修改
        userAccountMapper.updateByPrimaryKey(account);
        if(true){
            throw new Exception("出現了異常,程序中斷了,應該將之前的數據回退");
            //  Spring事務控制,默認情況下監聽的是運行時異常及其子類異常
            // 不過可以在配置事務時,修改事務監聽的異常類(rollbackOn = Exception.class)
            //int rs = 1/0;
        }
        userStockMapper.updateByPrimaryKey(stock);
    }
}

 

步驟四:編寫Controller

package com.newbie.controller;

import com.newbie.domain.User;
import com.newbie.service.ITransactionService;
import com.newbie.service.impl.TransactionService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.Resource;
import javax.transaction.Transactional;
import java.util.List;

/**
 * 測試:單數據庫中的事務操作
 */
@Controller
public class TransactionController {
    @Resource
    ITransactionService transactionService;

    /**
     * 修改數據:沒有增加事務控制
     */
    @RequestMapping("/notTransactionCommit")
    public String notTransactionCommit(Model model) {
        //執行數據庫操作
        try {
            transactionService.notTransactionCommit();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //設置向客戶端返回的model數據 和 邏輯視圖名稱
        return this.setModel(model);
    }

    /**
     * 修改數據:使用注解的方式,增加事務控制
     */
    @RequestMapping("/annotationCommit")
    public String annotationCommit(Model model){
        try {
            transactionService.annotationCommit();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //設置向客戶端返回的model數據 和 邏輯視圖名稱
        return this.setModel(model);
    }

    /**
     * 修改數據:基於AOP切面的方式,增加事務控制
     */
    @RequestMapping("/aopAdviceCommit")
    public String aopAdviceCommit(Model model) {
        try {
            transactionService.aopAdviceCommit();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //設置向客戶端返回的model數據 和 邏輯視圖名稱
        return this.setModel(model);
    }

    /**
     * 重置數據:將賬戶余額 和 股票票數 重置到原始狀態
     * @param model
     * @return
     */
    @RequestMapping("/resetData")
    public String resetData(Model model){
        transactionService.updateResetData();
        model.addAttribute("message","數據重置完成");
        return "showInfo";
    }
    /**
     * 設置向客戶端返回的model數據 和 邏輯視圖名稱
     */
    public String setModel(Model model) {
        String message = "操作失敗";
        message = transactionService.queryData();
        model.addAttribute("message", message);
        return "showInfo";
    }

}

 

五、編寫前端頁面,請求操作數據庫

//index.jsp 請求頁面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html> <head> <title>測試多數據源模式</title> </head> <body> <h2>練習二:單數據庫中事務操作</h2> <h3>方式一:使用注解方式:@</h3> <a href="notTransactionCommit">沒有增加事務控制</a><br/><br/> <a href="annotationCommit">事務控制:使用注解的方式</a><br/><br/> <a href="aopAdviceCommit"> 事務控制:基於AOP切面的方式</a><br/><br/> <a href="resetData"> 重置數據:賬戶余額=100 和 股票票數=0</a><br/><br/> </body> </html>
//showInfo.jsp 結果顯示頁面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>顯示結果信息</title>
</head>
<body>
處理信息:${message}<br/>
處理結果:<br/>
&nbsp;&nbsp;&nbsp;&nbsp;用戶ID:${user.id} <br/>
&nbsp;&nbsp;&nbsp;&nbsp;用戶名:${user.username}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;職 級:${user.title}<br/>
&nbsp;&nbsp;&nbsp;&nbsp;數據源:${user.remark}<br/>
</body>
</html>

 

六、查看結果

  1、index.jsp 請求頁面效果

 

  2、事務控制時,事務執行過程中發生異常,數據自動回退,所以結果沒有變化

  3、沒有進行事務控制時,數據直接提交了,沒有回退,結果賬戶余額減少了,而股票數卻沒有增加。

 


免責聲明!

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



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