Spring(二十二):Spring 事务

事务简介:

事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性。

事务就是一系列的动作,它们被当做一个单独的工作单元。这些动作要么全部完成,要么全部不起作用。

事务的是四个关键属性(ACID):

  1. 原子性(atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
  2. 一致性(consistency):一旦所有事务动作完成,事务就被提交。数据和资源就处于一种满足业务的一致性状态中。
  3. 隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
  4. 持久性(durablility):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响。通常情况下,事务的结果都被写到持久化存储中。

传统事务与Spring事务管理器:

传统事务:

package com.dx.jdbc;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class JdbcAPITx {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        DataSource dataSource = (DataSource) ctx.getBean("dataSource");
        Connection connection = null;
        try {
            connection = dataSource.getConnection();
            connection.setAutoCommit(false);

            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("select @@version;");
            while (resultSet.next()) {
                String version = resultSet.getString(1);
                System.out.println(version);
            }
            connection.commit();
        } catch (SQLException ex) {
            ex.printStackTrace();
            if (connection != null) {
                try {
                    connection.rollback();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
            throw new RuntimeException(ex);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e1) {
                    e1.printStackTrace();
                }
            }
        }
        System.out.println("Complete...");
    }
}
View Code

传统事务不利于一般情况需要把一个完整业务写在一个Transaction控制范围内,不利于代码按照功能划分数据操作类。还有必须要为不同的方法重写类似的样板代码。这段代码是特定于JDBC的,一旦选择其他数据库存取技术,代码需要作出相应的调整。

Spring事务管理器:

  • 作为企业级引用程序框架,Spring在不同的事务管理API之上定义了一个抽象类,而应用层开发人员不必了解底层的事务管理API,就可以使用Spring的事务管理机制。
  • Spring既支持编程式的事务管理,也支持声明式的事务管理:
  1. 编程式事务管理:将事务管理代码嵌入到业务方法中来控制事物的提交和回滚。在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码。
  2. 声明式事务管理:大多数情况下比编程式的事务管理更好用。它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理,事务管理作为一种横切关注点,可以通过AOP方法模块化,Spring通过Spring AOP框架支持声明式事务管理。
  • Spring从不同的事务管理API中抽象了一整套的事务机制。开发人员不必了解底层的事务API,就可以利用这些事务机制。有了这些事务机制,事务管理代码就能独立于特定的事务技术了。
  • Spring的核心事务管理抽象是org.springframework.transaction.PlatformTransactionManager.java接口类,它封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。org.springframework.transaction.PlatformTransactionManager.java接口类:
    1 package org.springframework.transaction;
    2 
    3 public interface PlatformTransactionManager {
    4     TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    5     void commit(TransactionStatus status) throws TransactionException;
    6     void rollback(TransactionStatus status) throws TransactionException;
    7 }

需求简介:

已知有图书网络销售店,包含有:账户、图书、图书库存。

账户:

/*Table structure for table `account` */
DROP TABLE IF EXISTS `account`;

CREATE TABLE `account` (
  `username` varchar(64) DEFAULT NULL,
  `balance` decimal(16,0) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `account` */
insert  into `account`(`username`,`balance`) values ('zhangsan','60');

图书:

/*Table structure for table `book` */
DROP TABLE IF EXISTS `book`;

CREATE TABLE `book` (
  `isbn` varchar(32) DEFAULT NULL,
  `book_name` varchar(256) DEFAULT NULL,
  `price` decimal(16,0) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `book` */
insert  into `book`(`isbn`,`book_name`,`price`) values ('10001','Java','100'),('10002','MySql','120');

库存:

/*Table structure for table `book_stock` */
DROP TABLE IF EXISTS `book_stock`;

CREATE TABLE `book_stock` (
  `isbn` varchar(32) DEFAULT NULL,
  `stock` int(8) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `book_stock` */
insert  into `book_stock`(`isbn`,`stock`) values ('10001',10),('10002',10);

需求实现:

图书销售功能

  • 第一步:获取书的单价;
  • 第二步:减少库存;
  • 第三步:更新用户余额

新建一个java project,在src下添加一个spring bean配置文件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"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    <!-- 导入资源文件 -->
    <context:property-placeholder location="classpath:db.properties" />
    <!-- 自动扫描的包 -->
    <context:component-scan base-package="com.dx.spring.tx"></context:component-scan>

    <!-- 配置C3P0数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
        <property name="driverClass" value="${jdbc.dirverClass}"></property>

        <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
        <property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
    </bean>

    <!-- 配置Spring 的 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

</beans>
View Code

db.properties数据库配置文件

jdbc.user=root
jdbc.password=123456
jdbc.jdbcUrl=jdbc:mysql://localhost/spring
jdbc.dirverClass=com.mysql.jdbc.Driver

jdbc.initialPoolSize=5
jdbc.maxPoolSize=50
jdbc.minPoolSize=3
View Code

数据层:

IBookShopDao.java

package com.dx.spring.tx;

public interface IBookShopDao {
    double findBookPriceByIsbn(String isbn);

    void updateBookStock(String isbn);

    void updateUserAccount(String username, double price);
}

BookShopDaoImpl.java

package com.dx.spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.dx.spring.exceptions.AccountBalanceInsufficientException;
import com.dx.spring.exceptions.BookStockException;

@Repository("bookShopDao")
public class BookShopDaoImpl implements IBookShopDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public double findBookPriceByIsbn(String isbn) {
        String sql = "select price from book where isbn=?";
        return jdbcTemplate.queryForObject(sql, Double.class, isbn);
    }

    @Override
    public void updateBookStock(String isbn) {
        String sql2 = "select stock from book_stock where isbn=?";
        int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
        if (stock <= 0) {
            throw new BookStockException("库存不足!");
        }

        String sql = "update book_stock set stock=stock-1 where isbn=?";
        jdbcTemplate.update(sql, isbn);
    }

    @Override
    public void updateUserAccount(String username, double price) {
        String sql2 = "select balance from account where username=?";
        double balance = jdbcTemplate.queryForObject(sql2, Double.class, username);
        if (balance < price) {
            throw new AccountBalanceInsufficientException("余额不足!");
        }
        String sql = "update account set balance=balance-? where username=?";
        jdbcTemplate.update(sql, price, username);

    }

}
View Code

服务层:

IBookShopService.java

package com.dx.spring.tx;

public interface IBookShopService {
    public void purchase(String username, String isbn);
}

BookShopServiceImpl.java

package com.dx.spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("bookShopService")
public class BookShopServiceImpl implements IBookShopService {
    @Autowired
    private IBookShopDao bookShopDao;

    @Override
    public void purchase(String username, String isbn) {
        // 第一步:获取书的单价;
        double price = bookShopDao.findBookPriceByIsbn(isbn);
        // 第二步:减少库存;
        bookShopDao.updateBookStock(isbn);
        // 第三步:更新用户余额
        bookShopDao.updateUserAccount(username, price);
    }

}

异常类:AccountBalanceInsufficientException.java

package com.dx.spring.exceptions;

public class AccountBalanceInsufficientException extends RuntimeException {
    private static final long serialVersionUID = -162287400165698929L;

    public AccountBalanceInsufficientException() {
        super();
    }

    public AccountBalanceInsufficientException(String message, Throwable cause, boolean enableSuppression,
            boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public AccountBalanceInsufficientException(String message, Throwable cause) {
        super(message, cause);
    }

    public AccountBalanceInsufficientException(String message) {
        super(message);
    }

    public AccountBalanceInsufficientException(Throwable cause) {
        super(cause);
    }

}
View Code

BookStockException.java

package com.dx.spring.exceptions;

public class BookStockException extends RuntimeException {
    private static final long serialVersionUID = 6192610386513480985L;

    public BookStockException() {
        super();
    }

    public BookStockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public BookStockException(String message, Throwable cause) {
        super(message, cause);
    }

    public BookStockException(String message) {
        super(message);
    }

    public BookStockException(Throwable cause) {
        super(cause);
    }

}
View Code

添加JUnit测试类BookShopTest.java测试功能

package com.dx.spring.tx;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.dx.spring.exceptions.AccountBalanceInsufficientException;
import com.dx.spring.exceptions.BookStockException;

public class BookShopTest {
    private ApplicationContext ctx = null;
    private IBookShopDao bookShopDao = null;
    private IBookShopService bookShopService = null;

    {
        ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        bookShopDao = (IBookShopDao) ctx.getBean("bookShopDao");
        bookShopService = (IBookShopService) ctx.getBean("bookShopService");
    }

    @Test
    public void testPurchase() {
        bookShopService.purchase("zhangsan", "10001");
    }

    @Test
    public void testFindBookPriceByIsbn() {
        System.out.println(bookShopDao.findBookPriceByIsbn("10001"));
    }

    @Test
    public void testUpdateBookStock() throws BookStockException {
        bookShopDao.updateBookStock("10001");
    }

    @Test
    public void testUpdateUserAccount() throws AccountBalanceInsufficientException {
        bookShopDao.updateUserAccount("zhangsan", 100d);
    }

}
View Code

执行测试方法:

    @Test
    public void testPurchase() {
        bookShopService.purchase("zhangsan", "10001");
    }

发现抛出了异常:

但是,查看数据库中记录发现库存已经被减1。这显然不满足事务的原子性。

那么如何使用Spring来管理图书网络销售事务?以下有两种方式来管理:基于Spring注解方式的事务管理、基于Spring XML配置文件的事务管理。

基于Spring注解方式的事务管理

第一步:修改spring bean配置文件,添加以下配置:

    <!-- 配置Spring事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <!-- 启用事务注解 如果上边配置的‘事务管理器’已经是‘transactonManager’时,这里的transaction-manager属性可以省略 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

第二步:在服务类purchase方法上添加@Transactional注解。

package com.dx.spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service("bookShopService")
public class BookShopServiceImpl implements IBookShopService {
    @Autowired
    private IBookShopDao bookShopDao;

    @Transactional
    @Override
    public void purchase(String username, String isbn) {
        // 第一步:获取书的单价;
        double price = bookShopDao.findBookPriceByIsbn(isbn);
        // 第二步:减少库存;
        bookShopDao.updateBookStock(isbn);
        // 第三步:更新用户余额
        bookShopDao.updateUserAccount(username, price);
    }
}

第三步:测试。

测试结果发现,当发现用户余额不足时,并没有使得库存减1。

Spring事务的传播行为

需求:

  • 新定义Cashier接口:表示客户的结账操作。
  • 修改数据表信息如下,目的是用户Tom在结账时,余额只能支付第一本数,不够支付第二本书。账户:,图书:,库存:
ICashier.java
package com.dx.spring.tx;

import java.util.List;

public interface ICashier {
    public void checkout(String username, List<String> isbns);
}

CashierImpl.java

package com.dx.spring.tx;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


@Service("cashier")
public class CashierImpl implements ICashier {
    @Autowired
    private IBookShopService bookShopService;

    @Transactional
    @Override
    public void checkout(String username, List<String> isbns) {
        for (String isbn : isbns) {
            bookShopService.purchase(username, isbn);
        }
    }
}

此时,添加Junit测试。

package com.dx.spring.tx;

import java.util.Arrays;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BookShopTest {
    private ApplicationContext ctx = null;
    private ICashier cashier = null;

    {
        ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        cashier = (ICashier) ctx.getBean("cashier");
    }

    @Test
    public void testCheckout() {
        cashier.checkout("Tom", Arrays.asList("0001", "0002"));
    }
}

测试抛出异常,但是此时数据库中的数据并未发生任何改变。

事务的传播行为 

PROPAGION_XXX :事务的传播行为 

* 保证同一个事务中 

PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认) 

PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务 

PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常 

* 保证没有在同一个事务中 

PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务 

PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务 

PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常 

PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行

REQUIRED传播行为:

  • 当bookService的purchase()方法被另外一个事务方法checkout()调用是,它默认会在现有的事务内运行,这个默认的传播行为就是REQUIRED,因此在checkout()方法的开始和终止边界内只有一个事务,这个事务只能自checkout()方法结束的时候被提交,结果用户一本书也买不了。
  • 事务传播属性可以在@Transactional注解的propagation属性中定义。

REQUIRES_NEW传播行为:

  • 另外一种常见的传播行为是REQUIRES_NEW,它表示该方法必须启动一个新事务,并能在自己的事务内运行。如果有事务在运行,就应该先挂起它。

修改BookShopServiceImpl.java中的@Transactional(propagation=REQUIRES_NEW)

package com.dx.spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service("bookShopService")
public class BookShopServiceImpl implements IBookShopService {
    @Autowired
    private IBookShopDao bookShopDao;

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    @Override
    public void purchase(String username, String isbn) {
        // 第一步:获取书的单价;
        double price = bookShopDao.findBookPriceByIsbn(isbn);
        // 第二步:减少库存;
        bookShopDao.updateBookStock(isbn);
        // 第三步:更新用户余额
        bookShopDao.updateUserAccount(username, price);
    }

}

此时,重新执行JUnit测试,发现数据发生变化:

账户余额发生变化:

库存减少:

Spring事务的隔离级别:

  • DEFAULT 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别. (Mysql 默认:可重复读,SqlServer,Oracle 默认:读已提交)
  • 未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生 
  • 已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生 
  • 可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生. 
  • 串行化的 (serializable) :避免以上所有读问题.

  • read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。
  • read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
  • repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。
  • serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读(避免三种)。

在Spring事务中用法是在@Transactional的属性中设置:

package com.dx.spring.tx;

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;

@Service("bookShopService")
public class BookShopServiceImpl implements IBookShopService {
    @Autowired
    private IBookShopDao bookShopDao;

    @Transactional(propagation=Propagation.REQUIRES_NEW,
            isolation=Isolation.READ_COMMITTED
            )
    @Override
    public void purchase(String username, String isbn) {
        // 第一步:获取书的单价;
        double price = bookShopDao.findBookPriceByIsbn(isbn);
        // 第二步:减少库存;
        bookShopDao.updateBookStock(isbn);
        // 第三步:更新用户余额
        bookShopDao.updateUserAccount(username, price);
    }
}

Spring事务的回滚:

 

正常情况下,按照正确的编码是不会出现事务回滚失败的。下面说几点保证事务能回滚的方法

  • (1)如果采用编程式事务,一定要确保切入点表达式书写正确
  • (2)如果Service层会抛出不属于运行时异常也要能回滚,那么可以将Spring默认的回滚时的异常修改为Exception,这样就可以保证碰到什么异常都可以回滚。具体的设置方式也说下:
    •  ① 声明式事务,在配置里面添加一个rollback-for,代码如下
      •  <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
    •  ② 注解事务,直接在注解上面指定,代码如下
      @Transactional(rollbackFor=Exception.class)
  • (3)只有非只读事务才能回滚的,只读事务是不会回滚的
  • 4)如果在Service层用了try catch,在catch里面再抛出一个 RuntimeException异常,这样出了异常才会回滚
  • (5)如果你不喜欢(4)的方式,你还可以直接在catch后面写一句回滚代码(TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();)来实现回滚,这样的话,就可以在抛异常后也能return 返回值;比较适合需要拿到Service层的返回值的场景。具体的用法可以参见考下面的伪代码
           /** TransactionAspectSupport手动回滚事务:*/
           @Transactional(rollbackFor = { Exception.class })  
           public boolean test() {  
                try {  
                   doDbSomeThing();    
                } catch (Exception e) {  
                     e.printStackTrace();     
                     //就是这一句了, 加上之后抛了异常就能回滚(有这句代码就不需要再手动抛出运行时异常了)
                     TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();  
                     return false;
                }  
               return true;
          }  

Spring事务的只读:

Spring的事务经常会有这样的配置:

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

或者这样的注记:

@Transactional(readOnly = true)

  “只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。
  但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。
  因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可。

Spring事务的超时:

@Transactional(timeout = 60)

如果用这个注解描述一个方法的话,线程已经跑到方法里面,如果已经过去60秒了还没跑完这个方法并且线程在这个方法中的后面还有涉及到对数据库的增删改查操作时会报事务超时错误(会回滚)。如果已经过去60秒了还没跑完但是后面已经没有涉及到对数据库的增删改查操作,那么这时不会报事务超时错误(不会回滚)。

基于Spring XML配置文件的事务管理

将项目转化为Spring配置(不包含事务):

IBookShopDao.java

package com.dx.spring.xml.tx;

public interface IBookShopDao {
    double findBookPriceByIsbn(String isbn);

    void updateBookStock(String isbn);

    void updateUserAccount(String username, double price);
}
View Code

IBookShopService.java

package com.dx.spring.xml.tx;

public interface IBookShopService {
    public void purchase(String username, String isbn);
}
View Code

ICashier.java

package com.dx.spring.xml.tx;

import java.util.List;

public interface ICashier {
    public void checkout(String username, List<String> isbns);
}
View Code

BookShopDaoImpl.java

package com.dx.spring.xml.tx;

import org.springframework.jdbc.core.JdbcTemplate;

import com.dx.spring.exceptions.AccountBalanceInsufficientException;
import com.dx.spring.exceptions.BookStockException;

public class BookShopDaoImpl implements IBookShopDao {
    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public double findBookPriceByIsbn(String isbn) {
        String sql = "select price from book where isbn=?";
        return jdbcTemplate.queryForObject(sql, Double.class, isbn);
    }

    @Override
    public void updateBookStock(String isbn) {
        String sql2 = "select stock from book_stock where isbn=?";
        int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
        if (stock <= 0) {
            throw new BookStockException("库存不足!");
        }

        String sql = "update book_stock set stock=stock-1 where isbn=?";
        jdbcTemplate.update(sql, isbn);
    }

    @Override
    public void updateUserAccount(String username, double price) {
        String sql2 = "select balance from account where username=?";
        double balance = jdbcTemplate.queryForObject(sql2, Double.class, username);
        if (balance < price) {
            throw new AccountBalanceInsufficientException("余额不足!");
        }
        String sql = "update account set balance=balance-? where username=?";
        jdbcTemplate.update(sql, price, username);

    }

}
View Code

BookShopServiceImpl.java

package com.dx.spring.xml.tx;

public class BookShopServiceImpl implements IBookShopService {
    private IBookShopDao bookShopDao;

    public void setBookShopDao(IBookShopDao bookShopDao) {
        this.bookShopDao = bookShopDao;
    }

    @Override
    public void purchase(String username, String isbn) {
        // 第一步:获取书的单价;
        double price = bookShopDao.findBookPriceByIsbn(isbn);
        // 第二步:减少库存;
        bookShopDao.updateBookStock(isbn);
        // 第三步:更新用户余额
        bookShopDao.updateUserAccount(username, price);
    }
}
View Code

CashierImpl.java

package com.dx.spring.xml.tx;

import java.util.List;

public class CashierImpl implements ICashier {
    private IBookShopService bookShopService;

    public void setBookShopService(IBookShopService bookShopService) {
        this.bookShopService = bookShopService;
    }

    @Override
    public void checkout(String username, List<String> isbns) {
        for (String isbn : isbns) {
            bookShopService.purchase(username, isbn);
        }
    }

}
View Code

新建Spring Bean Xml配置文件applicationContext-xml.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 http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
    <!-- 导入资源文件 -->
    <context:property-placeholder location="classpath:db.properties" />
    <!-- 自动扫描的包 -->
    <context:component-scan base-package="com.dx.spring.xml.tx"></context:component-scan>

    <!-- 配置C3P0数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
        <property name="driverClass" value="${jdbc.dirverClass}"></property>

        <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
        <property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
    </bean>

    <!-- 配置Spring 的 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    
    <!-- 配置 Bean -->
    <bean id="bookShopDao" class="com.dx.spring.xml.tx.BookShopDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>
    <bean id="bookShopService" class="com.dx.spring.xml.tx.BookShopServiceImpl">
        <property name="bookShopDao" ref="bookShopDao"></property>
    </bean>
    <bean id="cashier" class="com.dx.spring.xml.tx.CashierImpl">
        <property name="bookShopService" ref="bookShopService"></property>
    </bean>    
</beans>

添加Junit测试类BookShopTest.java

package com.dx.spring.xml.tx;

import java.util.Arrays;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class BookShopTest {
    private ApplicationContext ctx = null;
    private IBookShopService bookShopService = null;
    private ICashier cashier = null;

    {
        ctx = new ClassPathXmlApplicationContext("applicationContext-xml.xml");
        bookShopService = (IBookShopService) ctx.getBean("bookShopService");
        cashier = (ICashier) ctx.getBean("cashier");
    }

    @Test
    public void testCheckout() {
        cashier.checkout("Tom", Arrays.asList("0001", "0002"));
    }

    @Test
    public void testPurchase() {
        bookShopService.purchase("Tom", "0001");
    }
}

初始化数据库中:

库存:,书:,账户:

此时执行testPurchase()Junit测试方法:

执行抛出异常“余额不足”,但是库存减一 

库存减一:

将项目转化为Spring配置(包含事务):

1)初始化数据库中:

库存:,书:,账户:

2)导入aop依赖扩展包

3)修改Spring Bean XML配置文件applicationContext-xml.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" xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
    <!-- 导入资源文件 -->
    <context:property-placeholder location="classpath:db.properties" />

    <!-- 配置C3P0数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
        <property name="driverClass" value="${jdbc.dirverClass}"></property>

        <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
        <property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
    </bean>

    <!-- 配置Spring 的 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 配置 Bean -->
    <bean id="bookShopDao" class="com.dx.spring.xml.tx.BookShopDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"></property>
    </bean>
    <bean id="bookShopService" class="com.dx.spring.xml.tx.BookShopServiceImpl">
        <property name="bookShopDao" ref="bookShopDao"></property>
    </bean>
    <bean id="cashier" class="com.dx.spring.xml.tx.CashierImpl">
        <property name="bookShopService" ref="bookShopService"></property>
    </bean>

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

    <!-- 2.配置事务属性 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" />
        </tx:attributes>
    </tx:advice>

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

4)此时执行Junit测试方法:

    @Test
    public void testPurchase() {
        bookShopService.purchase("Tom", "0001");
    }

执行抛出异常,但是库存没有发生改变,说明事务已经开启。

调整项目结构使其设置事务的传播性:

1)修改账户余额:

库存不变:

图书价格也不变:

2)调整项目结构:

3)修改applicationContext-xml.xml配置文件

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
 4     xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
 5     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
 6         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
 7         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
 8         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">
 9     <!-- 导入资源文件 -->
10     <context:property-placeholder location="classpath:db.properties" />
11 
12     <!-- 配置C3P0数据源 -->
13     <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
14         <property name="user" value="${jdbc.user}"></property>
15         <property name="password" value="${jdbc.password}"></property>
16         <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
17         <property name="driverClass" value="${jdbc.dirverClass}"></property>
18 
19         <property name="initialPoolSize" value="${jdbc.initialPoolSize}"></property>
20         <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
21         <property name="minPoolSize" value="${jdbc.minPoolSize}"></property>
22     </bean>
23 
24     <!-- 配置Spring 的 JdbcTemplate -->
25     <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
26         <property name="dataSource" ref="dataSource"></property>
27     </bean>
28 
29     <!-- 配置 Bean -->
30     <bean id="bookShopDao" class="com.dx.spring.xml.tx.BookShopDaoImpl">
31         <property name="jdbcTemplate" ref="jdbcTemplate"></property>
32     </bean>
33     <bean id="bookShopService" class="com.dx.spring.xml.tx.service.impl.BookShopServiceImpl">
34         <property name="bookShopDao" ref="bookShopDao"></property>
35     </bean>
36     <bean id="cashier" class="com.dx.spring.xml.tx.service.impl.CashierImpl">
37         <property name="bookShopService" ref="bookShopService"></property>
38     </bean>
39 
40     <!-- 1.配置Spring事务管理器 -->
41     <bean id="transactionManager"
42         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
43         <property name="dataSource" ref="dataSource"></property>
44     </bean>
45 
46     <!-- 2.配置事务属性 -->
47     <tx:advice id="txAdvice" transaction-manager="transactionManager">
48         <tx:attributes>
49             <tx:method name="purchase" propagation="REQUIRES_NEW" />
50             <tx:method name="get*" read-only="true" />
51             <tx:method name="find*" read-only="true" />
52             <tx:method name="*" />
53         </tx:attributes>
54     </tx:advice>
55 
56     <!-- 3.配置事务切入点,以及把事务切入点和事务属性关联起来 -->
57     <aop:config>
58         <aop:pointcut expression="execution(* com.dx.spring.xml.tx.service.impl.*.*(..))"
59             id="txPointCut" />
60         <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
61     </aop:config>
62 </beans>

4)此时

    @Test
    public void testCheckout() {
        cashier.checkout("Tom", Arrays.asList("0001", "0002"));
    }

,执行测试发现库存少了一本。

参考:

①《Spring事务超时、回滚的相关说明

②《Spring 事务 readOnly 到底是怎么回事?

③《spring 中事务的Readonly的解释

原文地址:https://www.cnblogs.com/yy3b2007com/p/9156923.html