1 Spring中的事务管理

  • 事务就是一系列的动作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用。
  • Spring在不同的事物管理API之上定义了一个抽象层。应用程序开发人员不必了解底层的事物管理API,就可以使用Spring的事物管理机制。
  • Spring既支持编程式事务管理,也支持声明式事务管理。
  • 编程式事务管理:将事务管理代码嵌入到业务中来控制事务的提交和回滚。
  • 声明式事务管理:将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
  • 事务管理作为一种横切关注点,可以通过AOP方法模块化。
  • Spring通过Spring AOP框架支持声明式事务管理。
  • 事务管理器以普通的Bean形式声明在Spring IOC容器中。

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 `spring_test`.`book_stock` (`isbn`, `stock`) VALUES ('1001', '10');
    INSERT INTO `spring_test`.`book_stock` (`isbn`, `stock`) VALUES ('1002', '10');
    INSERT INTO `spring_test`.`book` (`isbn`, `book_name`, `price`) VALUES ('1001', '《Java从入门到放弃》', '100');
    INSERT INTO `spring_test`.`book` (`isbn`, `book_name`, `price`) VALUES ('1002', '《活着》', '70');
    INSERT INTO `spring_test`.`account` (`username`, `balance`) VALUES ('Tom', '300');
  • 项目目录:

图片说明

  • db.properties:

    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/spring_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    jdbc.username=root
    jdbc.password=root
  • 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/context
                               https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <!-- 注解扫描 -->
        <context:component-scan base-package="com.xianhuii"></context:component-scan>
    
        <!-- 引入外部化的配置文件 -->
        <context:property-placeholder location="classpath:db.properties"/>
        <!-- 配置数据源 -->
        <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${jdbc.driver}"></property>
            <property name="jdbcUrl" value="${jdbc.url}"></property>
            <property name="user" value="${jdbc.username}"></property>
            <property name="password" value="${jdbc.password}"></property>
        </bean>
    
        <!-- 配置jdbcTemplate -->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!-- 注入数据源 -->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!-- 配置事务管理器 -->
        <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!-- 基于注解使用事务,需要开启事务注解 -->
        <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
    
    </beans>
  • BookShopDao:

    package com.xianhuii.dao;
    
    public interface BookShopDao {
        // 1、根据书号获取对应的价格
        public int findBookPriceByIsbn(String isbn);
    
        // 2、根据书号更新库存
        public void updateBookStock(String isbn);
    
        // 3、根据书的价格更新用户的余额
        public void updateUserAccount(String username, int price);
    
    }
  • BookShopDaoImpl:

    package com.xianhuii.dao;
    
    import com.xianhuii.exception.BookStockException;
    import com.xianhuii.exception.UserAccountException;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class BookShopDaoImpl implements BookShopDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public int findBookPriceByIsbn(String isbn) {
            String sql = "select price from book where isbn = ?";
            return jdbcTemplate.queryForObject(sql, Integer.class, isbn);
        }
    
        @Override
        public void updateBookStock(String isbn) {
            // 判断库存是否足够
            String sql = "select stock from book_stock where isbn = ?";
            int stock = jdbcTemplate.queryForObject(sql, Integer.class, isbn);
            if (stock <= 0) {
                // 抛出库存不足的异常
                throw new BookStockException("库存不足!");
            }
            sql = "update book_stock set stock = stock -1 where isbn = ?";
            jdbcTemplate.update(sql, isbn);
        }
    
        @Override
        public void updateUserAccount(String username, int price) {
            // 判断余额是否足够
            String sql = "select balance from account where username = ?";
            int balance = jdbcTemplate.queryForObject(sql, Integer.class, username);
            if (balance < price) {
                // 抛出余额不足的异常
                throw new UserAccountException("余额不足!");
            }
            sql = "update account set balance = balance - ? where username = ?";
            jdbcTemplate.update(sql, price, username);
        }
    
    }
  • BookStockException:

    package com.xianhuii.exception;
    
    public class BookStockException extends RuntimeException {
        public BookStockException() {
            super();
        }
    
        public BookStockException(String message) {
            super(message);
        }
    
        public BookStockException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public BookStockException(Throwable cause) {
            super(cause);
        }
    
        protected BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    
    }
  • UserAccountException:

    package com.xianhuii.exception;
    
    public class UserAccountException extends RuntimeException {
        public UserAccountException() {
            super();
        }
    
        public UserAccountException(String message) {
            super(message);
        }
    
        public UserAccountException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public UserAccountException(Throwable cause) {
            super(cause);
        }
    
        protected UserAccountException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    
    }
  • BookShopService:

    package com.xianhuii.service;
    
    public interface BookShopService {
        public void buyBook(String username, String isbn);
    }
  • BookShopServiceImpl:

    package com.xianhuii.service;
    
    import com.xianhuii.dao.BookShopDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    //@Transactional  // 对该类中所有的方法都起作用
    public class BookShopServiceImpl implements BookShopService {
        @Autowired
        private BookShopDao bookShopDao;
    
        @Transactional  // 对指定方法起作用
        @Override
        public void buyBook(String username, String isbn) {
            // 1、查询书的价格
            int price = bookShopDao.findBookPriceByIsbn(isbn);
            // 2、更新数的库存
            bookShopDao.updateBookStock(isbn);
            // 3、更新用户的余额
            bookShopDao.updateUserAccount(username, price);
        }
    
    }
  • TransactionTest:

    package com.xianhuii.test;
    
    import com.xianhuii.service.BookShopService;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    public class TransactionTest {
        private ApplicationContext context;
        private BookShopService bookShopService;
    
        {
            context = new ClassPathXmlApplicationContext("applicationContext.xml");
            bookShopService = (BookShopService) context.getBean("bookShopServiceImpl");
        }
    
        @Test
        public void buyBookTest() {
            bookShopService.buyBook("Tom", "1001");
        }
    
    }

3 总结

  • ①在applicationContext.xml配置:

  • tx:annotation-driven中,transaction-manager的默认值是transactionManager。

  • 如果当前事务管理器的id值就是transactionManager,那么transaction-manager的配置可以省略。

  • 如果当前事务管理器的id值不是transactionManager,那么就必须在transaction-manager中指定当前的事务管理器的id值。

  <?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/context
                             https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

      <!-- 注解扫描 -->
      <context:component-scan base-package="com.xianhuii"></context:component-scan>

      <!-- 引入外部化的配置文件 -->
      <context:property-placeholder location="classpath:db.properties"/>
      <!-- 配置数据源 -->
      <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
          <property name="driverClass" value="${jdbc.driver}"></property>
          <property name="jdbcUrl" value="${jdbc.url}"></property>
          <property name="user" value="${jdbc.username}"></property>
          <property name="password" value="${jdbc.password}"></property>
      </bean>

      <!-- 配置jdbcTemplate -->
      <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
          <!-- 注入数据源 -->
          <property name="dataSource" ref="dataSource"></property>
      </bean>

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

      <!-- 基于注解使用事务,需要开启事务注解 -->
      <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>

  </beans>
  • ②使用注解方式开启事务:

    package com.xianhuii.service;
    
    import com.xianhuii.dao.BookShopDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    //@Transactional  // 对该类中所有的方法都起作用
    public class BookShopServiceImpl implements BookShopService {
        @Autowired
        private BookShopDao bookShopDao;
    
        @Transactional  // 对指定方法起作用
        @Override
        public void buyBook(String username, String isbn) {
            // 1、查询书的价格
            int price = bookShopDao.findBookPriceByIsbn(isbn);
            // 2、更新数的库存
            bookShopDao.updateBookStock(isbn);
            // 3、更新用户的余额
            bookShopDao.updateUserAccount(username, price);
        }
    
    }

4 事务的属性

1、propagation

  • propagation:事务的传播行为。
    • Propagation.REQUIRED:使用调用者的事务,默认值。
    • Propagation.REQUIRES_NEW:将调用者的事务挂起,使用自己的新事务。

2、isolation

  • 事务的隔离级别,最常用的是:READ_COMMITTED。

3、readOnly

  • 指定事务是否为只读。如果是只读事务,代表这个事务只读取数据库,而不进行修改操作。
  • 若一个事务真的是只读取数据,就有必要设置readOnly=true,可以帮助数据库引擎进行优化。

4、(不)回滚条件

  • rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName。

5、timeout

  • 指定强制回滚前事务可以占用的时间。为了避免一个事务占用过长时间。

6、案例

  • Cashier:

    package com.xianhuii.service;
    
    import java.util.List;
    
    public interface Cashier {
        /**
         * 模拟用户结账行为,同时买多本书
         */
        public void checkOut(String username, List<String> isbns);
    }
  • CashierImpl:

    package com.xianhuii.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Isolation;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.List;
    
    @Service
    public class CashierImpl implements Cashier {
        @Autowired
        private BookShopService bookShopService;
    
        /**
         * 事务的属性:
         * propagation:事务的传播行为。
         *      Propagation.REQUIRED:使用调用者的事务,默认值。
         *      Propagation.REQUIRES_NEW:将调用者的事务挂起,使用自己的新事务。
         * isolation:事务的隔离级别,最常用的是:READ_COMMITTED。
         * readOnly:指定事务是否为只读。如果是只读事务,代表这个事务只读取数据库,而不进行修改操作。
         *           若一个事务真的是只读取数据,就有必要设置readOnly=true,可以帮助数据库引擎进行优化。
         * rollbackFor
         * rollbackForClassName
         * noRollbackFor
         * noRollbackForClassName
         * timeout:指定强制回滚前事务可以占用的时间。为了避免一个事务占用过长时间。
         */
        @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
        public void checkOut(String username, List<String> isbns) {
            for (String isbn : isbns) {
                bookShopService.buyBook(username, isbn);
            }
        }
    
    }
  • 测试:

    @Test
        public void checkOutTest () {
            List<String> isbns = new ArrayList<>();
            isbns.add("1001");
            isbns.add("1002");
            cashier.checkOut("Tom", isbns);
        }

5 基于XML配置事务

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

        <!-- 配置事务属性 -->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <tx:attributes>
                <tx:method name="byBook" propagation="REQUIRES_NEW" isolation="READ_COMMITTED" read-only="false"/>
                <tx:method name="checkOut"/>
                <tx:method name="update*" propagation="REQUIRES_NEW"/>
                <tx:method name="insert*" propagation="REQUIRED"/>
                <tx:method name="delete*" propagation="REQUIRED"/>
                <tx:method name="get*" read-only="true"/>
                <!-- *代表除了上述指定的方法之外的方法 -->
                <tx:method name="*"/>
            </tx:attributes>
        </tx:advice>

        <!-- 配置事务切入点,以及事务切入点和事务属性关联起来 -->
        <aop:config>
            <aop:pointcut id="txPointCut" expression="execution(* com.xianhuii.service.*.*(..))"></aop:pointcut>
            <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
        </aop:config>