什么是基于注解的声明式事务

基于注解的声明式事务是Spring框架提供的一种事务管理机制,它允许开发者以声明的方式指定哪些方法应该在事务边界内执行,而不是通过编程方式显式地管理事务开始和结束。这种机制极大地简化了事务管理代码,使得业务逻辑更加清晰,同时也提高了代码的可维护性和可读性。

在Spring中,声明式事务主要通过@Transactional注解来实现。这个注解可以被应用在以下几个地方:

  1. 方法级:直接将@Transactional注解添加到方法签名上,这样每次调用该方法时,Spring都会确保在一个事务上下文中执行该方法。
  2. 类级:如果一个类的所有公共方法都需要相同的事务属性,可以在类级别上应用@Transactional。这样,类中的所有方法都会继承这些事务属性,除非某个特定方法上另有声明。
  3. 接口级:类似于类级别的应用,如果接口上的所有方法都应具有相同的事务行为,可以在接口上应用@Transactional

@Transactional注解支持多种属性,包括但不限于:

  • propagation:指定事务的传播行为,比如是否支持现有事务或创建新的事务。
  • isolation:指定事务的隔离级别,如读未提交、读已提交、可重复读或序列化。
  • readOnly:指定事务是否只读,这可以影响数据库的优化策略。
  • timeout:指定事务的超时时间。
  • rollbackFor:指定哪些类型的异常会导致事务回滚。
  • noRollbackFor:指定哪些类型的异常不会导致事务回滚。

当使用基于注解的声明式事务时,还需要确保Spring配置中启用了事务管理器,并且将@EnableTransactionManagement注解添加到配置类中,或者在XML配置中使用<tx:annotation-driven>元素。这样,Spring才能识别和处理@Transactional注解。

准备工作

导入相关依赖

<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 持久化层支持jar包 -->
    <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
    <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- Spring 测试相关 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- junit测试 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
</dependencies>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.

创建jdbc.properties

jdbc.user=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC
jdbc.driver=com.mysql.cj.jdbc.Driver
  • 1.
  • 2.
  • 3.
  • 4.

配置Spring的配置文件,tx-annotation.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:jdbc="http://www.springframework.org/schema/tool" 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/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--    扫描组件 -->
    <context:component-scan base-package="com.miaow"></context:component-scan>

<!--    导入外部属性文件-->
    <context:property-placeholder location="classpath:db.properties"></context:property-placeholder>

<!--    配置数据源-->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
    </bean>

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

</beans>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

创建数据库

CREATE TABLE `t_book` (
    `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
    `price` int(11) DEFAULT NULL COMMENT '价格',
    `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
    PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);

CREATE TABLE `t_user` (
    `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `username` varchar(20) DEFAULT NULL COMMENT '用户名',
    `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
    PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

创建Service接口

public interface BookService {
	void buyBook(Integer bookId, Integer userId);
}
  • 1.
  • 2.
  • 3.

创建Service实现层

@Service
public class BookServiceImpl implements BookService {
    @Autowired
    private BookDao bookDao;
    @Override
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

创建dao接口

public interface BookDao {
    Integer getPriceByBookId(Integer bookId);
    void updateStock(Integer bookId);
    void updateBalance(Integer userId, Integer price);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

创建dao的是实现层

@Repository
public class BookDaoImpl implements BookDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Override
    public Integer getPriceByBookId(Integer bookId) {
        String sql = "select price from t_book where book_id = ?";
        return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
    }
    @Override
    public void updateStock(Integer bookId) {
        String sql = "update t_book set stock = stock - 1 where book_id = ?";
        jdbcTemplate.update(sql, bookId);
    }
    @Override
    public void updateBalance(Integer userId, Integer price) {
        String sql = "update t_user set balance = balance - ? where user_id =?";
            jdbcTemplate.update(sql, price, userId);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

控制类

@Controller
public class BookController {
    @Autowired
    private BookService bookService;
    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

创建一个测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
    @Autowired
    private BookController bookController;
    @Test
    public void testBuyBook(){
        bookController.buyBook(1, 1);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

不加事务的时候进行
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额

假设用户id为1的用户,购买id为1的图书用户余额为50,而图书价格为80,注意这里,用户的额度只有50,但是一本书需要80, 50 - 80 = -30 ,由于数据库余额字段设置为无符号,会报错。

Spring系统学习- 事务之基于注解的声明式事务_xml

原因,购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段此时执行sql语句会抛出SQLException。

但是我们去看数据库,这个时候数据库会有一定的问题。

Spring系统学习- 事务之基于注解的声明式事务_学习_02


Spring系统学习- 事务之基于注解的声明式事务_数据库_03

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新,显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败。

完善上述操作:

在tx-annotation.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:jdbc="http://www.springframework.org/schema/tool" 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/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--    扫描组件 -->
    <context:component-scan base-package="com.miaow"></context:component-scan>

    <!--    导入外部属性文件-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>

    <!--    配置数据源-->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${jdbc.user}"></property>
        <property name="password" value="${jdbc.password}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="driverClassName" value="${jdbc.driver}"></property>
    </bean>

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

    <!--    从现在添加事务 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="druidDataSource"></property>
    </bean>

    <!--    开启事务注解驱动
        通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
        transaction-manager属性的默认值是transactionManager,如果事务管理器bean的ID就是这个默认的值,那么就可以忽略这个属性
    -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

启用了事务,我们就需要service层佳航我们的事务注解。
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理在BookServiceImpl的buybook()添加注解@Transactional

  • @Transactional标识在方法上,咋只会影响该方法
  • @Transactional标识的类上,咋会影响类中所有的方法
@Transactional
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    //System.out.println(1/0);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

看这是我们之前没加事务之前的结果,数据库的stock减少了1,如果我们没有加事务之前,那么这个结果stock会变成98,但是我们加上事务之后,正常情况想,只有在上述代码所有成功才能完成修改,否则不予修改

Spring系统学习- 事务之基于注解的声明式事务_学习_04


Spring系统学习- 事务之基于注解的声明式事务_数据库_05

我们在执行一次测试类查看,结果如下:

Spring系统学习- 事务之基于注解的声明式事务_学习_06


数据无变动,说明事务生效了。由于使用了Spring的声明式事务,更新库存和更新余额都没有执行

事务的属性

本篇开始之前我们讲了事务的属性有如下:
@Transactional注解支持多种属性,包括但不限于:

  • propagation:指定事务的传播行为,比如是否支持现有事务或创建新的事务。
  • isolation:指定事务的隔离级别,如读未提交、读已提交、可重复读或序列化。
  • readOnly:指定事务是否只读,这可以影响数据库的优化策略。
  • timeout:指定事务的超时时间。
  • rollbackFor:指定哪些类型的异常会导致事务回滚。
  • noRollbackFor:指定哪些类型的异常不会导致事务回滚。
ReadOnly属性
@Override
    @Transactional(readOnly = true) // 只读事务,无法进行增删改操作
    public void buyBook(Integer bookId, Integer userId) {
        //查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        //更新图书的库存
        bookDao.updateStock(bookId);
        //更新用户的余额
        bookDao.updateBalance(userId, price);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1066)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1042)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1345)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1027)
	at com.alibaba.druid.pool.DruidPooledPreparedStatement.executeUpdate(DruidPooledPreparedStatement.java:253)
	at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651)
	... 51 more
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
超时timeout
@Transactional(timeout = 3) //设置3秒等待时间,3秒内没执行完就回滚
public void buyBook(Integer bookId, Integer userId) {
    try {
        TimeUnit.SECONDS.sleep(5); //休眠5秒,假装该方法需要5秒才能执行完毕
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    //System.out.println(1/0);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Tue Jul 16 13:52:44 CST 2024

	at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
	at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
	at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
	at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:339)
	at org.springframework.jdbc.core.JdbcTemplate.applyStatementSettings(JdbcTemplate.java:1470)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:650)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
事务的回滚策略

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象
  • rollbackForClassName属性:需要设置一个字符串类型的全类名
  • noRollbackFor属性:需要设置一个Class类型的对象
  • rollbackFor属性:需要设置一个字符串类型的全类名

使用方式

@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
    //查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    //更新图书的库存
    bookDao.updateStock(bookId);
    //更新用户的余额
    bookDao.updateBalance(userId, price);
    System.out.println(1/0);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行.

事务的隔离级别(isolation)
  1. DEFAULT(默认):使用数据库默认的事务隔离级别,通常为数据库的默认级别(如MySQL的REPEATABLE READ)。
  2. READ_UNCOMMITTED(读未提交):允许事务读取未提交的数据更改,可能导致脏读、不可重复读和幻读问题。
  3. READ_COMMITTED(读已提交):确保一个事务只能读取到已提交的数据,可以避免脏读,但仍可能出现不可重复读和幻读问题。
  4. REPEATABLE_READ(可重复读):确保事务可以多次读取相同的数据,并且在事务执行期间其他事务对数据的修改不会影响到该事务,可以避免脏读和不可重复读,但仍可能出现幻读问题。
  5. SERIALIZABLE(串行化):最高的隔离级别,确保事务可以完全隔离,避免脏读、不可重复读和幻读,但可能导致性能下降。

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

隔离级别

脏读

不可重复读

幻读

READ

UNCOMMITTED



READ COMMITTED




REPEATABLE READ




SERIALIZABLE




各种数据库产品对事务隔离级别的支持程度:

隔离级别

Oracle

MySQL

READ UNCOMMITTED

×


READ COMMITTED

√(默认)


REPEATABLE READ

×

√(默认)

SERIALIZABLE



@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
事务的传播行为(propagation)
  1. REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式执行。
  3. MANDATORY:强制要求当前存在事务,如果没有事务,则抛出异常。
  4. REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
  5. NOT_SUPPORTED:以非事务的方式执行操作,如果当前存在事务,则将当前事务挂起。
  6. NEVER:以非事务的方式执行操作,如果当前存在事务,则抛出异常。
  7. NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建一个新的事务。

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

创建接口CheckoutService:

public interface CheckoutService {
	void checkout(Integer[] bookIds, Integer userId);
}
  • 1.
  • 2.
  • 3.

创建实现类CheckoutServiceImpl:

@Service
public class CheckoutServiceImpl implements CheckoutService {
    @Autowired
    private BookService bookService;
    @Override
    @Transactional
    //一次购买多本图书
    public void checkout(Integer[] bookIds, Integer userId) {
        for (Integer bookId : bookIds) {
            bookService.buyBook(bookId, userId);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

在BookController中添加方法:

@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
    checkoutService.checkout(bookIds, userId);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

在数据库中将用户的余额修改为100元

可以通过@Transactional中的propagation属性设置事务传播行为修改BookServiceImplbuyBook()上,注解@Transactionalpropagation属性。

此时运行代码如下:

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance = balance - ? where user_id =?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`test`.`t_user`.`balance` - 50)'; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`test`.`t_user`.`balance` - 50)'

	at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:104)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:79)
	at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1541)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:667)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:960)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1015)
	at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:1025)
	at com.miaow.dao.impl.BookDaoImpl.updateBalance(BookDaoImpl.java:30)
	at com.miaow.service.impl.BookServiceImpl.buyBook(BookServiceImpl.java:44)
	at com.miaow.service.impl.CheckoutServiceImpl.checkout(CheckoutServiceImpl.java:23)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:134)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy20.checkout(Unknown Source)
	at com.miaow.controller.BookController.checkout(BookController.java:26)
	at com.miaow.AppTest.checkout(AppTest.java:40)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
	at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
	at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
	at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
	at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
	at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
	at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`test`.`t_user`.`balance` - 50)'
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:955)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1094)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1042)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1345)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1027)
	at com.alibaba.druid.pool.DruidPooledPreparedStatement.executeUpdate(DruidPooledPreparedStatement.java:253)
	at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965)
	at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651)
	... 52 more

Disconnected from the target VM, address: '127.0.0.1:50043', transport: 'socket'

Process finished with exit code -1
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • @Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
    经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了。
  • @Transactional(propagation = Propagation.REQUIRES_NEW)表示不管当前线程上是否有已经开启的事务,都要开启新事务
    同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。
//默认值
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRED) 

//如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.SUPPORTS) 

//如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY) 

//创建一个新事务,如果当前存在事务,则把当前事务挂起。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW) 

//以非事务方式执行操作,如果当前存在事务,则把当前事务挂起。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED) 

//以非事务方式执行,如果当前存在事务,则抛出异常。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NEVER) 

//如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.NESTED)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.