第八章、声明式事务管理

一、事务概述

  1. 事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行,为了保证数据的完整性和一致性。

  2. 事务的四个关键属性(ACID)

    1. 原子性(atomicity):“原子”的本意是“操作不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。

    2. 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。

    3. 隔离性(isolation):在应用程序实际运行过程中,事务之间是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。

    4. 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

二、Spring事务管理

2.1、编程式事务管理

  使用原生的JDBC API进行事务管理

    1. 获取数据库连接Connection对象
    2. 取消事务的自动提交
    3. 执行操作
    4. 正常完成操作时手动提交事务
    5. 执行失败时回滚事务
    6. 关闭相关资源

  使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余

2.2、声明式事务管理

  通过配置的形式,基于AOP的方式,动态的把事务管理的代码作用的目标方法上面大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。 Spring既支持编程式事务管理,也支持声明式的事务管理。

2.3、Spring提供的事务管理器

  Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。 事务管理器可以以普通的bean的形式声明在Spring IOC容器中。

2.4、事务管理器的主要实现

  1. DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。

JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理。

  1. HibernateTransactionManager:用Hibernate框架存取数据库。

三、测试数据准备

3.1、需求

 

   可以通过余额不足但是库存减少了,来演示事务。

  • Dao层
public interface BookShopDao {

    /**
     * 根据书号查询书的价格
     */

    Book findBookPriceByIsbn(String isbn);

    /**
     * 根据书号查询书的价格
     * @param isbn
     * @param stock
     * @return
     */
    int updateBookStock(String isbn);

    /**
     * 根据书号查询书的价格
     * @param username
     * @param balance
     * @return
     */
    int updateAccount(String username, Integer price);
}

 

@Repository
public class BookShopDaoImpl implements BookShopDao {

    @Autowired
    private JdbcTemplate template;

    @Override
    public Book findBookPriceByIsbn(String isbn) {
        String sql = "SELECT * FROM book WHERE isbn=?";
        RowMapper<Book> rowMapper = new BeanPropertyRowMapper<>(Book.class);
        return template.queryForObject(sql, rowMapper, isbn);
    }

    @Override
    public int updateBookStock(String isbn) {
        //判断库存是否足够
        String sql = "SELECT stock from book_stock WHERE isbn = ?";
        Integer bookCount = template.queryForObject(sql, Integer.class, isbn);
        if (bookCount <= 0) {
            throw new RuntimeException("库存不足,没有书了");
        }
        sql = "UPDATE book_stock SET stock = stock-1 WHERE isbn = ?";
        return template.update(sql, isbn);
    }

    @Override
    public int updateAccount(String username, Integer price) {
        //判断余额是否足够
        String sql = "SELECT balance from account WHERE username = ?";
        Integer balance = template.queryForObject(sql, Integer.class, username);
        if (balance <= price) {
            throw new RuntimeException("余额不足");
        }
        sql = "UPDATE account  SET balance  = balance - ? WHERE username = ?";
        return template.update(sql, price, username);
    }
}
  • Service层
public interface BookShopService {

    Book buyBook(String isbn, String username);
}

 

@Service("bookShopServiceImpl")
public class BookShopServiceImpl implements BookShopService {


    @Autowired
    private BookShopDao bookShopDao;

    /**
     * 买书 :账户余额减少,库存减少
     * @param isbn
     * @param username
     * @param stock
     * @param balance
     * @return
     */
    @Transactional
    @Override
    public Book buyBook(String isbn, String username) {
        //根据书号查询书的价格
        Book book  = bookShopDao.findBookPriceByIsbn(isbn);
        String  price = book.getPrice();
        //判断库存是否足够
        int updateBookStockResult = bookShopDao.updateBookStock(isbn);
        //判断余额是否足够
        int updateAccountResult =  bookShopDao.updateAccount(username,Integer.valueOf(price));
        return book;
    }
}

  @Transactional标准的位置

    • 标注在类上,对当前类的所有方法都起作用。
    • 标注在方法上,只对当前方法起作用。
  •  配置声明式事务application_tx.xml
   <!--包扫描-->
    <context:component-scan base-package="com.jdy.spring2020.scan"/>
    <!--引入外部配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 一、数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
        <property name="driverClass" value="${jdbc.driverClass}"/>
        <property name="user" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--二、JDBC模板-->
    <bean id="template" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 三、配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 四、启用事务注解 transaction-manager:用来指定事务管理器,如果事务管理器的id值时transaction-manager,可以省略不进行指定 -->
    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
  • 测试
public class TX_Test {
    private BookShopDao bookShopDao;
    private BookShopService bookShopService;
    {
        ApplicationContext context = new ClassPathXmlApplicationContext("application_tx.xml");
        bookShopService= context.getBean("bookShopServiceImpl", BookShopServiceImpl.class);
        bookShopDao= context.getBean("bookShopDaoImpl", BookShopDaoImpl.class);
    }
    /**
     * 买书
     */
    @Test
    public void test_method01() {
        bookShopService.buyBook("ISBN-001","Jerry");
    }
}

假设BookShopDaoImpl.buyBook上没有@Transactional注解,通过改变库的数据,是Jerry的账户金额不足买一本书,运行测试方法后,可以看到书本的数量减少,但是账户金额没有变,

加上@Transactional注解后,进行实物控制后,整个buyBook方法成功才会出现,余额减少,数量减少。

四、事务的传播行为

  • 当事务方法(A)被另一个事务方法(B)调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
  • 事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
  • 事务传播属性可以在@Transactional注解的propagation属性中定义。

            如图:方法C是外围方法,A、B是子方法。  

传播属性 描述
Required

使用调用者的事务】A、B两个事务方法,A调用了B,整体使用A的事务,B中异常,则A方法整体回滚。

  1. 外围方法开启事务,子方法直接加入到外围事务中,形成一个整体事务。
  2. 外围方法没开启事务,则子方法创建独立的事务,不同子方法创建的事务互不干扰,可以独立回滚或提交。
Required_new

将调用者的事务挂起,重启开启事务来使用

  1. 不管外围方法有没有开启事务,子方法都会创建一个属于自己的事务,子方法创建的事务互不干扰,与外围方法也互不干扰。
Support

如果有事务,就在事务内运行,如果没有,可以不再事务内运行

  1. 外围方法开启事务,子方法一起加入到外围事务中,形成一个整体事务。
  2. 外围方法没开启事务,则子方法直接不使用事务。
Not_ support

当前方法不应该运行在事务中,如果有事务,将事务挂起

  1. 不管外围方法有没有开启事务,子方法都不想去使用事务,外围方法回滚时,不会回滚子方法。
Mandatory

当前方法必须在事务内部运行,如果没有实物,就抛出异常

  1. 外围方法开启事务,子方法一起加入到外围事务中,形成一个整体事务。
  2. 外围方法没开启事务,则子方法直接抛出异常,强制需要外围方法有事务
Never

当前事务不应该在事务中运行,如果有就抛出异常

  1. 外围方法开启事务,则子方法直接抛出异常。
  2. 外围方法没开启事务,子方法也不会使用事务。
Nested

如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则,就启动一个新的事务,并在它自己的事务内运行。

  1. 外围方法开启事务,外围事务回滚时,子事务会全部回滚。但某一个子事务由于异常回滚时,不会影响外围事务与其他子事务。
  2. 外围方法没开启事务,则子方法创建独立的事务,不同子方法创建的事务互不干扰,可以独立回滚或提交。

五、事务的隔离级别

5.1、数据库事务并发问题

  假设现在有两个事务:Transaction01和Transaction02并发执行。

  1. 脏读:指当一个事务(T1)正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务(T2)也访问这个数据,然后使用了这个数据。

  2. 不可重复读:指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。在第一个事务中(T1)的两次读数据之间,由于第二个事务(T2)的修改,导致了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。不可重复读的重点是修改:同样的条件,你读取过的数据,再次读取出来发现值不一样了

  3. 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”幻读的重点在于新增或者删除:同样的条件,第 1 次和第 2 次读出来的记录数不一样

5.2、隔离级别

  数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

  1. 读未提交:READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。(问题:脏读

  2. 读已提交:READ COMMITTED

    要求Transaction01只能读取Transaction02已提交的修改。(问题:不可重复读(修改数据问题))

  3. 可重复读(默认):REPEATABLE READ

    确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。(问题:幻读(增加数据问题))

  4. 串行(xing)化:SERIALIZABLE

    确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下

  5. 各个隔离级别解决并发问题的能力见下表

  脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

5.3、在Spring中指定事务隔离级别

  • 注解

    用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别

public class Cashierimpl implements Cashier  {

    @Autowired
    private BookShopService bookShopService;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED)
    public void checkOut(List<String> isbns, String username) {
        for (String isbn : isbns) {
            Book book  = bookShopService.buyBook(isbn,username);
        }
    }
}

六、触发事务回滚的异常

  默认情况,捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。通过注解@Transactional设置回滚的异常

    • rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个.

    • noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个

public class Cashierimpl implements Cashier  {

    @Autowired
    private BookShopService bookShopService;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED,
            rollbackFor = {IOException.class, SQLException.class},
            noRollbackFor= {NullPointerException.class}
            )
    public void checkOut(List<String> isbns, String username) {
        for (String isbn : isbns) {
            Book book  = bookShopService.buyBook(isbn,username);
        }
    }
}

七、事务的超时和只读属性

  由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。 如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。

  超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。

  只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。

7.1、设置

  1、注解@Transactional

public class Cashierimpl implements Cashier  {

    @Autowired
    private BookShopService bookShopService;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW ,isolation= Isolation.READ_COMMITTED,
            rollbackFor = {IOException.class, SQLException.class},
            noRollbackFor= {NullPointerException.class},
            readOnly = true,
            timeout = 30
            )
    public void checkOut(List<String> isbns, String username) {
        for (String isbn : isbns) {
            Book book  = bookShopService.buyBook(isbn,username);
        }
    }
}

八、 基于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>
posted @ 2020-09-16 15:48  jingdy  阅读(285)  评论(0编辑  收藏  举报