Spring框架——事務處理(編程式和聲明式)

 1、 事務概述

●在JavaEE企業級開發的應用領域,爲了保證數據的完整性一致性,必須引入數據庫事務的概念,因此事務管理是企業級應用程序開發中必不可少的技術。java

●事務就是一組因爲邏輯上緊密關聯而合併成一個總體(工做單元)的多個數據庫操做,這些操做要麼都執行要麼都不執行mysql

●事務的四個關鍵屬性(ACID)web

原子性(atomicity):「原子」的本意是「不可再分」,事務的原子性表現爲一個事務中涉及到的多個操做在邏輯上缺一不可。事務的原子性要求事務中的全部操做要麼都執行,要麼都不執行。spring

一致性(consistency):「一致」指的是數據的一致,具體是指:全部數據都處於知足業務規則的一致性狀態。一致性原則要求:一個事務中無論涉及到多少個操做,都必須保證事務執行以前數據是正確的,事務執行以後數據仍然是正確的。若是一個事務在執行的過程當中,其中某一個或某幾個操做失敗了,則必須將其餘全部操做撤銷,將數據恢復到事務執行以前的狀態,這就是回滾sql

隔離性(isolation):在應用程序實際運行過程當中,事務每每是併發執行的,因此頗有可能有許多事務同時處理相同的數據,所以每一個事務都應該與其餘事務隔離開來,防止數據損壞。隔離性原則要求多個事務在併發執行過程當中不會互相干擾數據庫

持久性(durability):持久性原則要求事務執行完成後,對數據的修改永久的保存下來,不會因各類系統錯誤或其餘意外狀況而受到影響。一般狀況下,事務對數據的修改應該被寫入到持久化存儲器中。express

 

2、 Spring事務管理

2.1 編程式事務管理

2.1.1使用原生的JDBC API進行事務管理

[1]獲取數據庫鏈接Connection對象編程

[2]取消事務的自動提交服務器

[3]執行操做併發

[4]正常完成操做時手動提交事務

[5]執行失敗時回滾事務

[6]關閉相關資源

例:

//1.獲取Connection對象

Connection conn = JDBCUtils.getConnection();

try {

    //2.開啓事務:取消自動提交

    conn.setAutoCommit(false);

    //3.執行操做

    //4.正常完成操做,手動提交事務

    conn.commit();

}catch(Exception e) {

    //5.回滾事務

    conn.rollBack();

}finally{

    //6.釋放資源

}

2.1.2評價

使用原生的JDBC API實現事務管理是全部事務管理方式的基石,同時也是最典型的編程式事務管理。編程式事務管理須要將事務管理代碼嵌入到業務方法中來控制事務的提交和回滾。在使用編程的方式管理事務時,必須在每一個事務操做中包含額外的事務管理代碼。相對於核心業務而言,事務管理的代碼顯然屬於非核心業務,若是多個模塊都使用一樣模式的代碼進行事務管理,顯然會形成較大程度的代碼冗餘

 

2.2 聲明式事務管理

大多數狀況下聲明式事務比編程式事務管理更好:它將事務管理代碼從業務方法中分離出來,以聲明的方式來實現事務管理。

事務管理代碼的固定模式做爲一種橫切關注點,能夠經過AOP方法模塊化,進而藉助Spring AOP框架實現聲明式事務管理

Spring在不一樣的事務管理API之上定義了一個抽象層,經過配置的方式使其生效,從而讓應用程序開發人員沒必要了解事務管理API的底層實現細節,就可使用Spring的事務管理機制。

Spring既支持編程式事務管理,也支持聲明式的事務管理。

 

2.3 Spring提供的事務管理器

Spring從不一樣的事務管理API中抽象出了一整套事務管理機制,讓事務管理代碼從特定的事務技術中獨立出來。開發人員經過配置的方式進行事務管理,而沒必要了解其底層是如何實現的。

Spring的核心事務管理抽象是PlatformTransactionManager。它爲事務管理封裝了一組獨立於技術的方法。不管使用Spring的哪一種事務管理策略(編程式或聲明式),事務管理器都是必須的。

事務管理器能夠以普通的bean的形式聲明在Spring IOC容器中。

 

2.4 事務管理器的主要實現

①DataSourceTransactionManager:在應用程序中只須要處理一個數據源,並且經過JDBC存取。

②JtaTransactionManager:在JavaEE應用服務器上用JTA(Java Transaction API)進行事務管理

③HibernateTransactionManager:用Hibernate框架存取數據庫

 

 

3 測試數據準備

3.1 需求

 

 

3.2 數據庫表

CREATE TABLE book (

  isbn VARCHAR (50) PRIMARY KEY,

  book_name VARCHAR (100),

  price INT

) ;

 

CREATE TABLE book_stock (

  isbn VARCHAR (50) PRIMARY KEY,

  stock INT,

  CHECK (stock > 0)

) ;

 

CREATE TABLE account (

  username VARCHAR (50) PRIMARY KEY,

  balance INT,

  CHECK (balance > 0)

) ;

 

INSERT INTO account (`username`,`balance`) VALUES ('Tom',100000);

INSERT INTO account (`username`,`balance`) VALUES ('Jerry',150000);

 

INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-001','book01',100);

INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-002','book02',200);

INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-003','book03',300);

INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-004','book04',400);

INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-005','book05',500);

 

INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-001',1000);

INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-002',2000);

INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-003',3000);

INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-004',4000);

INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-005',5000);

 

4 初步實現

4.1 配置文件

<!-- 配置事務管理器 -->

<bean id="transactionManager" 

  class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

<property name="dataSource" ref="dataSource"/>   

</bean>

 

<!-- 啓用事務註解 -->

<tx:annotation-driven transaction-manager="transactionManager"/>

 

4.2 在須要進行事務控制的方法上加註解

@Transactional

 

5 事務的傳播行爲

5.1 簡介

當事務方法被另外一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啓一個新事務,並在本身的事務中運行。

事務的傳播行爲能夠由傳播屬性指定。Spring定義了7種類傳播行爲。系統默認屬性爲:REQUIRED。

 

事務傳播屬性能夠在@Transactional註解的propagation屬性中定義。

 

5.2 測試

 

 

5.3 說明

REQUIRED傳播行爲

bookServicepurchase()方法被另外一個事務方法checkout()調用時,它默認會在現有的事務內運行。這個默認的傳播行爲就是REQUIRED。所以在checkout()方法的開始和終止邊界內只有一個事務。這個事務只在checkout()方法結束的時候被提交,結果用戶一本書都買不了。

 

 

REQUIRES_NEW傳播行爲

表示該方法必須啓動一個新事務,並在本身的事務內運行。若是有事務在運行,就應該先掛起它。

 

 

5.4 補充

Spring 2.x事務通知中,能夠像下面這樣在<tx:method>元素中設定傳播事務屬性。

 

 

 

6 事務的隔離級別

6.1 數據庫事務併發問題

假設如今有兩個事務:Transaction01Transaction02併發執行。

髒讀

[1]Transaction01將某條記錄的AGE值從20修改成30

[2]Transaction02讀取了Transaction01更新後的值:30

[3]Transaction01回滾,AGE值恢復到了20

[4]Transaction02讀取到的30就是一個無效的值。

不可重複讀

[1]Transaction01讀取了AGE值爲20

[2]Transaction02AGE值修改成30

[3]Transaction01再次讀取AGE值爲30,和第一次讀取不一致。

幻讀

[1]Transaction01讀取了STUDENT表中的一部分數據。

[2]Transaction02STUDENT表中插入了新的行。

[3]Transaction01讀取了STUDENT表時,多出了一些行。

6.2 隔離級別

數據庫系統必須具備隔離併發運行各個事務的能力,使它們不會相互影響,避免各類併發問題。一個事務與其餘事務隔離的程度稱爲隔離級別SQL標準中規定了多種事務隔離級別,不一樣隔離級別對應不一樣的干擾程度,隔離級別越高,數據一致性就越好,但併發性越弱。

讀未提交:READ UNCOMMITTED

容許Transaction01讀取Transaction02未提交的修改。

讀已提交:READ COMMITTED

要求Transaction01只能讀取Transaction02已提交的修改。

可重複讀:REPEATABLE READ

確保Transaction01能夠屢次從一個字段中讀取到相同的值,即Transaction01執行期間禁止其它事務對這個字段進行更新。

串行化:SERIALIZABLE

確保Transaction01能夠屢次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其它事務對這個表進行添加、更新、刪除操做。能夠避免任何併發問題,但性能十分低下。

⑤各個隔離級別解決併發問題的能力見下表

 

髒讀

不可重複讀

幻讀

READ UNCOMMITTED

READ COMMITTED

REPEATABLE READ

SERIALIZABLE

⑥各類數據庫產品對事務隔離級別的支持程度

 

Oracle

MySQL

READ UNCOMMITTED

×

READ COMMITTED

REPEATABLE READ

×

(默認)

SERIALIZABLE

 

 

6.3 Spring中指定事務隔離級別

①註解

@Transactional註解聲明式地管理事務時能夠在@Transactionalisolation屬性中設置隔離級別

XML

Spring 2.x事務通知中,能夠在<tx:method>元素中指定隔離級別

 

 

 

7 觸發事務回滾的異常

7.1 默認狀況

捕獲到RuntimeExceptionError時回滾,而捕獲到編譯時異常不回滾。

7.2 設置途經

①註解

@Transactional 註解

[1]rollbackFor屬性:指定遇到時必須進行回滾的異常類型,能夠爲多個

[2]noRollbackFor屬性:指定遇到時不回滾的異常類型,能夠爲多個

 

 

XML

Spring 2.x事務通知中,能夠在<tx:method>元素中指定回滾規則。若是有不止一種異常則用逗號分隔。

 

 

8 事務的超時和只讀屬性

8.1 簡介

因爲事務能夠在行和表上得到鎖,所以長事務會佔用資源,並對總體性能產生影響。

若是一個事物只讀取數據但不作修改,數據庫引擎能夠對這個事務進行優化。

超時事務屬性:事務在強制回滾以前能夠保持多久。這樣能夠防止長期運行的事務佔用資源。

只讀事務屬性: 表示這個事務只讀取數據但不更新數據, 這樣能夠幫助數據庫引擎優化事務。

8.2 設置

①註解

@Transaction註解

 

 

 

XML

Spring 2.x事務通知中,超時和只讀屬性能夠在<tx:method>元素中進行指定

 

 

基於XML文檔的聲明式事務配置

<!-- 配置事務切面 -->

<aop:config>

<aop:pointcut

expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))"

id="txPointCut"/>

<!-- 將切入點表達式和事務屬性配置關聯到一塊兒 -->

<aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/>

</aop:config>

 

<!-- 配置基於XML的聲明式事務  -->

<tx:advice id="myTx" transaction-manager="transactionManager">

<tx:attributes>

<!-- 設置具體方法的事務屬性 -->

<tx:method name="find*" read-only="true"/>

<tx:method name="get*" read-only="true"/>

<tx:method name="purchase"

isolation="READ_COMMITTED"

No-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException"

propagation="REQUIRES_NEW"

read-only="false"

timeout="10"/>

</tx:attributes>

</tx:advice>

 

 

10.聲明式事務例:

①導入SQL文件:declaration_transaction.sql

該文件內容以下,可直接複製到記事本,文件名後綴爲.sql,經過數據庫(如:Navicat Premium等)運行該sql文件就好。

 

/*
SQLyog Ultimate v9.20 
MySQL - 5.1.37-community : Database - tx
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`tx` /*!40100 DEFAULT CHARACTER SET gb2312 */;

USE `tx`;

/*Table structure for table `account` */

DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `username` varchar(50) NOT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `account` */

insert  into `account`(`username`,`balance`) values ('Jerry',800),('Tom',100000);

/*Table structure for table `book` */

DROP TABLE IF EXISTS `book`;

CREATE TABLE `book` (
  `isbn` varchar(50) NOT NULL,
  `book_name` varchar(100) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `book` */

insert  into `book`(`isbn`,`book_name`,`price`) values ('ISBN-001','book01',100),('ISBN-002','book02',200),('ISBN-003','book03',300),('ISBN-004','book04',400),('ISBN-005','book05',500);

/*Table structure for table `book_stock` */

DROP TABLE IF EXISTS `book_stock`;

CREATE TABLE `book_stock` (
  `isbn` varchar(50) NOT NULL,
  `stock` int(11) DEFAULT NULL,
  PRIMARY KEY (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312;

/*Data for the table `book_stock` */

insert  into `book_stock`(`isbn`,`stock`) values ('ISBN-001',1000),('ISBN-002',2000),('ISBN-003',3000),('ISBN-004',4000),('ISBN-005',5000);

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
declaration_transaction.sql

 

②建立一個動態web工程

1.加入jar

com.springsource.net.sf.cglib-2.2.0.jar

com.springsource.org.aopalliance-1.0.0.jar

com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar

commons-logging-1.1.3.jar

 

spring-aop-4.0.0.RELEASE.jar

spring-aspects-4.0.0.RELEASE.jar

spring-beans-4.0.0.RELEASE.jar

spring-context-4.0.0.RELEASE.jar

spring-core-4.0.0.RELEASE.jar

spring-expression-4.0.0.RELEASE.jar

 

spring-jdbc-4.0.0.RELEASE.jar

spring-orm-4.0.0.RELEASE.jar

spring-tx-4.0.0.RELEASE.jar

 

而後是mysql驅動包即C3P0jar包:

c3p0-0.9.1.2.jar

mysql-connector-java-5.1.37-bin.jar

 2.建立一份jdbc.properties文件

 

jdbc.user=root

jdbc.passowrd=123456

jdbc.url=jdbc:mysql://localhost:3306/tx

jdbc.driver=com.mysql.jdbc.Driver

 

  3.spring配置文件中配置數據源

<!-- 引入外部屬性文件 -->

<context:property-placeholder location="classpath:jdbc.properties"/>

<!-- 配置數據源 -->

<bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">

<property name="user" value="${jdbc.user}"></property>

<property name="password" value="${jdbc.passowrd}"></property>

<property name="jdbcUrl" value="${jdbc.url}"></property>

<property name="driverClass" value="${jdbc.driver}"></property>

</bean>

 

  4.測試數據源:

public class TestDataSource {

  private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");

  @Test

  public void test() throws SQLException {
    //獲取數據源類
    DataSource bean = ioc.getBean(DataSource.class);
    //檢驗是否可以鏈接
    System.out.println(bean.getConnection());   } }

  

  5.配置jdbcTemplate:

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  <property name="dataSource" ref="comboPooledDataSource"></property>
</bean>

 

  6.建立Dao

<!-- 在xml文件中設置掃描的包 -->

<context:component-scan base-package="com.neuedu"></context:component-scan>

  

@Repository
public class BookDao {

@Autowired
private JdbcTemplate  jdbcTemplate;

//[1]根據isbn的值查詢書的價格
public Integer findPriceByIsbn(String isbn){
  String sql = "SELECT price FROM book WHERE isbn = ?";
  Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
  return price;
}

//[2]根據isbn的值減小書的庫存,假設每次都只買1本書
public void updateStockByIsbn(String isbn){
  String sql = "UPDATE book_stock SET stock=stock-1 WHERE isbn=? ";
  jdbcTemplate.update(sql, isbn);
}

//[3]根據用戶名減小用戶帳戶中的餘額,減小的額度就是書的價格
public void updateBalance(String username,int price){
  String sql = "UPDATE account SET balance=balance-? WHERE username=? ";
  jdbcTemplate.update(sql,price, username);
}

public void updatePriceByIsbn(String isbn,int price){
  String sql="UPDATE book SET price=? WHERE isbn=? ";
  jdbcTemplate.update(sql, price,isbn);
}

 
public int findPriceByIsbn(String isbn){
  String sql = "SELECT price FROM book WHERE isbn = ?";
  Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
  return price;
}
}

  

 

  測試上面的該方法:

 public class TestDataSource {

  private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
  private BookDao bean = ioc.getBean(BookDao.class);
  @Test
  public void test01() throws SQLException {
    int findPriceByIsbn = bean.findPriceByIsbn("ISBN-005");
    System.out.println(findPriceByIsbn);
  }
}

 

  而後在dao類中繼續編寫以下方法:

 

//[2]根據isbn的值減小書的庫存,假設每次都只買1本書
public void updateStockByIsbn(String isbn){
  String sql = "UPDATE book_stock SET stock = stock -1 WHERE isbn = ?";
  jdbcTemplate.update(sql, isbn);
}

 

  

 

  繼續測試:

 public class TestDataSource {

private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
private BookDao bean = ioc.getBean(BookDao.class);

@Test
public void test02() throws SQLException {
bean.updateStockByIsbn("ISBN-004");
}
}

  

  繼續編寫Dao類中的方法:

//[3]根據用戶名減小用戶帳戶中的餘額,減小的額度就是書的價格
public void updateBalance(String userName,int price){
  String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
  jdbcTemplate.update(sql, price,userName);
}

  

  繼續測試:

@Test
public void test03() throws SQLException {
  bean.updateBalance("Tom", 1000);
}

  

 

7.建立Service層:

@Service
public class BookService {
  @Autowired
  private BookDao bookDao;
 
  public void doCash(String isbn,String username){
    int price = bookDao.findPriceByIsbn(isbn);
    bookDao.updateStockByIsbn(isbn);
    bookDao.updateBalance(username, price);
  }
}

  

   8.注意哦:上面doCash方法中調用的三個方法應該在同一個事務中,要麼同時成功,要麼同時失敗!

   先統一設置一下:

UPDATE account SET balance = 10000;
UPDATE book_stock SET stock = 1000;

而後先正常測試一下service中的doCash方法:

public class TestDataSource {
  private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");
  private BookDao bean = ioc.getBean(BookDao.class);
  private BookService bookService  = ioc.getBean(BookService.class);
   
  @Test
  public void test04() throws SQLException {
    bookService.doCash("ISBN-001","Tom");
  }
}

  

        因爲此時該方法沒在事務中,若是doCash方法調用dao層的方法的時候,在中間位置出現了錯誤,此時就會形成一部分數據改掉了,而

 另外一部分數據沒有改掉,這就麻煩了,因此此時應該加入事務機制!

  9.若是想開啓事務,就須要在spring的配置文件中配置事務管理器

 <!-- 配置事務管理器,併爲事務管理器配置數據源!-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="comboPooledDataSource"></property>
</bean>
<!-- 開啓基於註解的聲明式事務功能,須要設置transaction-manager屬性-->
<!-- 若是 事務管理器的id正好是transaction-manager的默認值transactionManager,則能夠省略-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

而後在service層的doCash方法上加上:@Transactional註解這樣就開啓了事務!須要注意的是事務通常是加在service層的!

③數據庫操做

[1]根據isbn的值查詢書的價格

[2]根據isbn的值減小書的庫存,假設每次都只買1本書

[3]根據用戶名減小用戶帳戶中的餘額,減小的額度就是書的價格

相關文章
相關標籤/搜索