9、声明式事务


【尚硅谷】SSM框架全套教程-讲师:杨博超

但行好事,莫问前程

9、声明式事务

9.1、声明式事务概念

1 编程式事务

事务功能的相关操作全部通过自己编写代码来实现。

Connection conn = ...;
try {
	// 开启事务:关闭事务的自动提交
	conn.setAutoCommit(false);
	// 核心操作
	// 提交事务
	conn.commit();
}catch(Exception e){
	// 回滚事务
	conn.rollBack();
}finally{
	// 释放数据库连接
	conn.close();
}
  • 编程式的实现方式存在缺陷:
    • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
    • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

2 声明式事务

  • 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
  • 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
    • 好处1:提高开发效率
    • 好处2:消除了冗余的代码
    • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
  • 所以,我们可以总结下面两个概念:
    • 编程式:自己写代码实现功能
    • 声明式:通过配置让框架实现功能

9.2、基于注解的声明式事务

1 准备工作

依赖坐标

<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.22</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.22</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.22</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.29</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.11</version>
</dependency>

创建jdbc.properties

druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.url=jdbc:mysql://localhost:3306/mybatis?userUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
druid.username=root
druid.password=root

Spring配置文件

<!--扫描组件-->
<context:component-scan base-package="pers.tianyu"></context:component-scan>
<!--导入外部属性文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>

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

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

创建表

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`)
);

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`)
);

INSERT INTO `t_user`(`user_id`,`username`,`balance`) VALUES
(1,'admin',50);

创建组件

(controller层)

BookController

@Controller
public class BookController {

    private BookService bookService;

    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
    
    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}

(service层)

接口BookService

public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}

实现类BookServiceImpl

@Service
public class BookServiceImpl implements BookService {

    private BookDao bookDao;

    @Autowired
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public void buyBook(Integer bookId, Integer userId) {
        // 查询图书的价格
        Integer price = bookDao.getPriceByBookId(bookId);
        // 更新图书的库存
        bookDao.updateStock(bookId);
        // 更新用户的余额
        bookDao.updateBalance(userId,price);
    }
}

(dao层)

接口BookDao:

public interface BookDao {

    Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}

实现类BookDaoImpl

@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);
    }
}

2 测试无事务情况

测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestBookController {

    private BookController bookController;

    @Autowired
    public void setBookController(BookController bookController) {
        this.bookController = bookController;
    }

    @Test
    public void testBuyBook(){
        bookController.buyBook(1,1);
    }
}

模拟场景

用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额。

假设用户id为1的用户,购买id为1的图书。

用户余额为50,而图书价格为80。

购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段。

此时执行sql语句会抛出SQLException。

观察结果

运行报异常。

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新。

显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败。

3 加入事务

添加事务配置

在Spring的配置文件中添加配置:

<?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:aop="http://www.springframework.org/schema/aop"
       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.xsd
	   http://www.springframework.org/schema/aop
	   http://www.springframework.org/schema/aop/spring-aop.xsd
	   http://www.springframework.org/schema/tx
	   http://www.springframework.org/schema/tx/spring-tx.xsd">
	<!--事务,spring封装好的事务切面类,不需要用户在写切面类,在使用AOP配置织入-->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	    <property name="dataSource" ref="dataSource"></property>
	</bean>
	<!--
	    开启事务的注解驱动(作用:把事务切面类中通知与目标类连接)
	    将使用@Transactional注解所标识的方法或类中所有的方法使用事务进行管理
	-->
	<!-- transaction-manager属性设置为事务管理器的id
	若事务管理器的bean默认值是transactionManager,则可以省略这个属性
	(spring配置文件内值变灰色原因:如果引用的属性是默认值,就会变灰色) -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

注意:导入的名称空间需要tx 结尾的那个。

添加事务注解

因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理。

在BookServiceImpl的buybook()添加注解@Transactional

@Transactional
@Override
public void buyBook(Integer bookId, Integer userId) {
    // 查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    // 更新图书的库存
    bookDao.updateStock(bookId);
    // 更新用户的余额
    bookDao.updateBalance(userId,price);
}

观察结果

由于使用了Spring的声明式事务,更新库存和更新余额都没有执行。

4 总结

声明式事务的配置步骤

1 在spring的配置文件中配置事务管理器

2 开启事务的注解驱动

3 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理

@Transactional注解标识的位置

@Transactional标识在方法上,则该方法会被事务管理。

@Transactional标识的类上,则类中所有的方法都会被事务管理。

9.3、事务属性

1 只读

介绍

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。

这样数据库就能够针对查询操作来进行优化。

使用方式

@Transactional(readOnly = true)
@Override
public void buyBook(Integer bookId, Integer userId) {
    // 查询图书的价格
    Integer price = bookDao.getPriceByBookId(bookId);
    // 更新图书的库存
    bookDao.updateStock(bookId);
    // 更新用户的余额
    bookDao.updateBalance(userId,price);
}

注意

对增删改操作设置只读会抛出下面异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

2 超时

介绍

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。

而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

使用方式

@Override
@Transactional(timeout = 3) // 超时时间为3秒钟
public void buyBook(Integer bookId, Integer userId) {
    try {
        TimeUnit.SECONDS.sleep(5); // 休眠5秒
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

     // 查询图书的价格
     Integer price = bookDao.getPriceByBookId(bookId);
     // 更新图书的库存
     bookDao.updateStock(bookId);
     // 更新用户的余额
     bookDao.updateBalance(userId,price);
}

观察结果

执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Sun Sep 11 22:19:33 CST 2022

3 回滚策略

介绍

  • 声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
  • 可以通过@Transactional中相关属性设置回滚策略
    • rollbackFor 属性:需要设置一个Class类型的对象(因为什么而回滚)
    • rollbackForClassName 属性:需要设置一个字符串类型的全类名(因为什么而回滚)
    • noRollbackFor 属性:需要设置一个Class类型的对象(不因为什么而回滚)
    • noRollbackForClassName 属性:需要设置一个字符串类型的全类名(不因为什么而回滚)

使用方式

@Override
@Transactional(noRollbackFor = ArithmeticException.class) // noRollbackFor:对数组类型属性赋值需要加大括号{},如果只有一个数据,那大括号可以省略
//@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);
}

观察结果

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

4 事务隔离级别

①介绍

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。

一个事务与其他事务隔离的程度称为隔离级别。

SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

MySQL默认隔离级别是可重复读

  • 隔离级别一共有四种:
    • 读未提交:READ UNCOMMITTED
      允许Transaction01读取Transaction02未提交的修改。
    • 读已提交:READ COMMITTED、
      要求Transaction01只能读取Transaction02已提交的修改。
    • 可重复读:REPEATABLE READ
      确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
    • 串行化:SERIALIZABLE
      确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

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

隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

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

隔离级别OracleMySQL
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)// 串行化

5 事务传播行为

介绍

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

测试

接口CheckoutService

public interface CheckoutService {
    //一次购买多本图书
    void checkout(Integer[] bookIds, Integer userId);
}

实现类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);
        }

    }
}

在BookController中添加ckeckout()方法

@Autowired
private CheckoutService checkoutService;

public void ckeckout(Integer[] bookIds, Integer userId) {
    checkoutService.checkout(bookIds, userId);
} 

观察结果

可以通过@Transactional中的propagation属性设置事务传播行为。

修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性。

@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。(使用调用者的事务)

经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。

所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了。

@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。(使用被调用者方法本身的事务)

同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。

9.4、基于XML的声明式事务

1 场景模拟

参考基于注解的声明式事务

2 修改Spring配置文件

将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:

<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
	<tx:attributes>
		<!-- tx:method标签:配置具体的事务方法 -->
		<!-- name属性:指定方法名,可以使用星号代表多个字符 -->
		<tx:method name="get*" read-only="true"/>
		<tx:method name="query*" read-only="true"/>
		<tx:method name="find*" read-only="true"/>
		<!-- read-only属性:设置只读属性 -->
		<!-- rollback-for属性:设置回滚的异常 -->
		<!-- no-rollback-for属性:设置不回滚的异常 -->
		<!-- isolation属性:设置事务的隔离级别 -->
		<!-- timeout属性:设置事务的超时属性 -->
		<!-- propagation属性:设置事务的传播行为 -->
		<tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
		<tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
		<tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
	</tx:attributes>
</tx:advice>

<aop:config>
	<!-- 配置事务通知和切入点表达式 -->
	<aop:advisor advice-ref="txAdvice" pointcut="execution(*
pers.tianyu.service.impl.*.*(..))"></aop:advisor>
</aop:config>

注意

基于xml实现的声明式事务,必须引入aspectJ的依赖

<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.22</version>
</dependency>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值