Spring 事務管理(13)

  • 2020 年 3 月 18 日
  • 筆記

事務管理用來確保數據的完整性和一致性。事務就是一系列的工作,它們被當做一個單獨的工作單元,這些動作要麼全部完成,要麼全部不起作用。

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

  • 原子性:事務是一個原子操作,由一系列動作組成,事務的原子性確保動作要麼全部完成要麼完全不起作用
  • 一致性:事務的執行的結果必須是使數據庫從一個一致性狀態變到另一個一致性狀態。因此當數據庫只包含成功事務提交的結果時,就說數據庫處於一致性狀態。如果數據庫系統運行時發生故障,有些事務尚未完成就被迫中斷,這些未完成事務對數據庫所做的修改有一部分已寫入物理數據庫,這時數據庫就處於一種不正確的狀態,或者說是不一致的狀態
  • 隔離性:一個事務的執行不能有其他事務干擾。即一個事務內部的操作及使用的數據對其它並發事務是隔離的,並發之執行的各個事務之間不能互相干擾。事務的隔離界別有4級。
  • 持續性:也稱永久性,指一個事務一旦提交,它對數據庫中的數據的改變就應該是永久的,不能回滾。接下來的其它操作或故障不應該對其執行結果有任何影響

Spring中的事務管理

Spring在不同的事務管理API之上定義了一個抽象層,Spring既支持編程式事務管理,也支持聲明式的事務管理。

  • 編程式事務管理:將事務管理代碼潛入到業務方法中來控制事務的提交和回滾
  • 聲明式事務管理:將事務管理代碼從業務方法中分離出來,以聲明的方式來實現事務管理,Spring通過Spring AOP框架支持聲明式事務管理

Spring的核心事務管理抽象是org.springframework.transaction.PlatformTransactionManager ,這是一個接口,封裝了一組獨立於技術的方法,無論使用Spring的哪種事務管理策略,事務管理器都是必須的。

事務管理器的不同實現:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager : 在應用程序中只需要處理一個數據源,而且通過JDBC存取
  • org.springframework.transaction.jta.JtaTransactionManager 在JavaEE應用服務器上用JTA(Java Transaction API)進行事務管理

事務管理器最終以普通的Bean形式聲明在Spring IOC容器中

事務的傳播行為

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

Spring支持的事務傳播行為

傳播屬性

描述

REQURED

如果有事務在運行,當前的方法就在這個事務內運行,否則,就啟動一個新的事務,並在自己的事務內運行

REQUIRED_NEW

當前的定義方法必須啟動新事務,並在它自己的事務內運行,如果有事務正在運行,應該將它掛起

並發事務所導致的問題

並發事務(當同一個應用程序或不同應用程序中的多個事務在同一個數據集上並行執行時)可能導致的問題:

  • 臟讀:一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然後使用了這個數據
  • 不可重複讀:一個事務內,多次讀同一個數據。在這個事務還沒有結束時,另外一個事務也訪問該統一數據,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的數據可能是不一樣。
  • 幻讀:第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行,同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。第一個事務同樣的操作讀取兩次,得到的記錄數並不相同

隔離級別

描述

READ_UNCOMMITED

允許事務讀取未其他事務提交的變更,臟讀,不可重複讀和幻讀的問題都會出現

READ_COMMITED

一個事務只能看見已經提交事務所做的改變,可以避免臟讀,但不可重複讀和幻讀的問題仍舊會出現,這是默認的隔離級別

REPEATABLE——READ

確保事務可以多次從一個字段讀取相同的值,在這個事務持續期間,禁止其他事務對這個字段進行更新,可以避免臟讀和不可重複讀,但幻讀的問題仍然存在

SERIALZABLE

確保事務可以從一個表中讀取相同的行,在這個事務持續期間,禁止其他事務執行插入,更新和刪除操作,所有並發都可以避免,但性能十分低下

注意:事務的隔離級別受到數據庫的限制,不同的數據庫支持的的隔離級別不一定相同

代碼

// BookShopDao.java  public interface BookShopDao {        // 根據書號獲取書的單價      public  int findBookPriceByIsbn(String isbn);        // 更新書的庫存,使書號對應的庫存 -1      public void updateBookStock(String isbn);        // 更新用戶的賬戶餘額:使username的balacne - price      public void updateUserAccount(String username,int price);  }
// BookShopDaoImpl.java  @Repository("bookShopDao") // 可以不命名,默認使用類名首字母小寫  public class BookShopDaoImpl implements BookShopDao {        @Autowired      private JdbcTemplate jdbcTemplate;        @Override      public int findBookPriceByIsbn(String isbn) {          String sql = "SELECT price FROM book WHERE id = ?";          return jdbcTemplate.queryForObject(sql,Integer.class,isbn);      }        @Override      public void updateBookStock(String isbn) {          // 檢查書的庫存是否足夠,若不夠,則拋出異常          String sql2 = "SELECT stock FROM book_stock WHERE book_id = ?";          int stock = jdbcTemplate.queryForObject(sql2,Integer.class,isbn);          if(stock == 0){              throw new BookStockException("庫存不足!");          }          String sql = "UPDATE book_stock SET stock = stock - 1 WHERE book_id = ?";          jdbcTemplate.update(sql,isbn);      }        @Override      public void updateUserAccount(String username, int price) {          String sql2 = "SELECT balance FROM accout WHERE user = ?";          int balance = jdbcTemplate.queryForObject(sql2,Integer.class,username);          if(balance < price){              throw new UserAccountException("餘額不足!");          }          String sql = "UPDATE accout SET balance = balance - ? WHERE user = ?";          jdbcTemplate.update(sql,price,username);      }  }
// BookShopService.java  public interface BookShopService {      public void purchase(String username,String isbn);  }
// BookShopServiceImpl.java  @Service("bookShopService")  public class BookShopServiceImpl implements BookShopService {        @Autowired      private BookShopDao bookShopDao;        // -->添加事務註解      // -->使用propagation 指定事務的傳播行為,即當前的事務方法被另一個事務方法調用時      // 如何使用事務,默認取值為 REQUIRED,即使用調用方法的事務      // REQUIRED_NEW:事務自己的事務,調用的事務方法的事務被掛起      // -->使用isolation 指定事務的隔離級別,最常用的取值為READ_COMMITTED      // 默認情況下Spring的聲明式事務對所有的運行時異常進行回滾,也可以通過對應的屬性進行設置,通常情況下默認值即可      // -->使用timeout指定強制回滾之前事務可以佔用的時間      // -->使用readOnly指定事務是否為只讀。表示這個事務只讀取事務但不更新數據      @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED,                      noRollbackFor = {UserAccountException.class},                      timeout = 3,readOnly = false)      @Override      public void purchase(String username, String isbn) {          // 1. 獲取書的單價          int price = bookShopDao.findBookPriceByIsbn(isbn);            // 2. 更新書的庫存          bookShopDao.updateBookStock(isbn);            // 3. 更新用戶餘額          bookShopDao.updateUserAccount(username,price);      }  }
// BookStockException.java  public class BookStockException extends RuntimeException {        private static final long sericlVersionUID = 1L;        public BookStockException(){          super();      }        public BookStockException(String message,Throwable cause,                                     boolean enableSuppression,boolean writeableStackTrace){          super(message,cause,enableSuppression,writeableStackTrace);      }        public BookStockException(String message,Throwable cause){          super(message,cause);      }        public BookStockException(String message){          super(message);      }        public BookStockException(Throwable cause){          super(cause);      }    }
// SpringTransactionTest.java  public class SpringTransactionTest {      private ApplicationContext ctx = null;      private BookShopDao bookShopDao = null;      private BookShopService bookShopService = null;      {          ctx = new ClassPathXmlApplicationContext("applicationContext.xml");          bookShopDao = ctx.getBean(BookShopDao.class);          bookShopService = ctx.getBean(BookShopService.class);      }        @Test      public void testBookShopDaoFindPriceByIsbn(){          System.out.println(bookShopDao.findBookPriceByIsbn("2"));      }        @Test      public void testBookShopDaoUpdateBookStock(){          bookShopDao.updateBookStock("1");      }        @Test      public void testUpdateUserAccount(){          bookShopDao.updateUserAccount("ada",1);      }        @Test      public void testBookShopService(){          bookShopService.purchase("ada","1");      }  }
// UserAccountException.java  public class UserAccountException extends RuntimeException {        private static final long serialVersionUID = 1L;        public UserAccountException(){          super();      }        public UserAccountException(String message,Throwable cause,                                boolean enableSuppression,boolean writeableStackTrace){          super(message,cause,enableSuppression,writeableStackTrace);      }        public UserAccountException(String message,Throwable cause){          super(message,cause);      }        public UserAccountException(String message){          super(message);      }        public UserAccountException(Throwable cause){          super(cause);      }  }
// applicationContext.xml  <?xml version="1.0" encoding="UTF-8"?>  <beans xmlns="http://www.springframework.org/schema/beans"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xmlns:context="http://www.springframework.org/schema/context"         xmlns:tx="http://www.springframework.org/schema/tx"         xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd          http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">        <context:component-scan base-package="com.sangyu.test12"/>      <!--導入資源文件-->      <context:property-placeholder location="classpath:db.properties"/>        <!--配置c3p0數據源-->      <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">          <property name="User" value="${jdbc.user}"/>          <property name="Password" value="${jdbc.password}"/>          <property name="DriverClass" value="${jdbc.driverClass}"/>          <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>            <property name="InitialPoolSize" value="${jdbc.initPoolSize}"/>          <property name="MaxPoolSize" value="${jdbc.maxPoolSize}"/>      </bean>        <!-- 配置 Spirng 的 JdbcTemplate -->      <bean id="jdbcTemplate"            class="org.springframework.jdbc.core.JdbcTemplate">          <property name="dataSource" ref="dataSource"></property>      </bean>        <!--配置NamedParameterJdbcTemplate,該對象可以使用具名參數,其沒有無參數的構造器,所以必須為其構造器指定參數-->      <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">          <constructor-arg ref="dataSource"></constructor-arg>      </bean>        <!--配置bean-->      <bean class="com.sangyu.test12.BookShopDao" abstract="true"></bean>      <bean class="com.sangyu.test12.BookShopService" abstract="true"></bean>          <!-- 配置事務管理器 -->      <bean id="transactionManager"            class="org.springframework.jdbc.datasource.DataSourceTransactionManager">          <property name="dataSource" ref="dataSource"></property>      </bean>        <!-- 啟用事務註解 -->      <tx:annotation-driven transaction-manager="transactionManager"/>  </beans>