14.1、概述

14.1.1、编程式事务

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

Connection conn = ...;

try {

// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false); // 核心操作 // 提交事务
conn.commit(); }catch(Exception e){ // 回滚事务
conn.rollBack(); }finally{ // 释放数据库连接
conn.close(); }

编程式事务的缺陷:

  • 细节没有被屏蔽:所有细节都需要程序员自己来完成,比较繁琐。

  • 代码复用性不高:每次实现功能都需要自己编写代码,代码没有得到复用。

14.1.2、声明式事务

因为事务控制的代码有规律可循,代码的结构基本是确定的;所以框架就可以将固定模式的代码抽取出来,并进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

声明式事务的优点:

  • 提高开发效率

  • 消除了冗余的代码

  • 框架考虑和实现功能会更加全面

14.1.3、总结

  • 编程式:自己写代码实现功能

  • 声明式:通过配置让框架实现功能

14.2、环境搭建

创建名为spring_transaction的新module,过程参考13.1节

14.2.1、创建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"
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"> <!-- 导入外部属性文件 -->
<context:property-placeholder location="jdbc.properties"></context:property-placeholder> <!-- 配置数据源 -->
<bean id="datasource" class="com.alibaba.druid.pool.DruidDataSource">
<!--通过${key}的方式访问外部属性文件的value-->
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" 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> </beans>

14.2.2、创建表并填充数据

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;

注意:该表的库存(stock)字段已设置为不能是负数(unsigned)

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;

注意:该表的余额(balance)字段已设置为不能是负数(unsigned)

insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);

14.3、不考虑事务的实现

14.3.1、创建持久层接口BookDao及其实现类

package org.rain.spring.dao;

/**
* @author liaojy
* @date 2023/8/27 - 0:35
*/
public interface BookDao { /**
* 查询图书的价格
* @param bookId
* @return
*/
Integer getPriceByBookId(Integer bookId); /**
* 更新图书的库存
* @param bookId
*/
void updateStockOfBook(Integer bookId); /**
* 更新用户的余额
* @param userId
* @param price
*/
void updateBalanceOfUser(Integer userId,Integer price); }

package org.rain.spring.dao.impl;

import org.rain.spring.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository; /**
* @author liaojy
* @date 2023/8/27 - 0:45
*/
@Repository
public class BookDaoImpl implements BookDao { @Autowired
private JdbcTemplate jdbcTemplate; public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
Integer price = jdbcTemplate.queryForObject(sql, Integer.class,bookId);
return price;
} public void updateStockOfBook(Integer bookId) {
String sql = "update t_book set stock = stock -1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
} public void updateBalanceOfUser(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql,price,userId);
}
}

14.3.2、创建业务层接口BookService及其实现类

package org.rain.spring.service;

/**
* @author liaojy
* @date 2023/8/27 - 0:59
*/
public interface BookService { void buyBook(Integer bookId,Integer userId); }

package org.rain.spring.service.impl;

import org.rain.spring.dao.BookDao;
import org.rain.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; /**
* @author liaojy
* @date 2023/8/27 - 1:02
*/
@Service
public class BookServiceImpl implements BookService { @Autowired
private BookDao bookDao; public void buyBook(Integer bookId, Integer userId) { //查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存
bookDao.updateStockOfBook(bookId); //更新用户的余额
bookDao.updateBalanceOfUser(userId,price); } }

14.3.3、创建控制层BookController

注意:因为控制层没用到接口,所以方法的访问修饰符要手动设置

package org.rain.spring.controller;

import org.rain.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; /**
* @author liaojy
* @date 2023/8/27 - 1:07
*/
@Controller
public class BookController { @Autowired
private BookService bookService; public void buyBook(Integer bookId, Integer userId){
bookService.buyBook(bookId,userId);
} }

14.3.4、配置对注解组件的扫描

    <!--扫描注解组件-->
<context:component-scan base-package="org.rain.spring"></context:component-scan>

14.3.5、创建测试类

模拟场景:

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

  • 假设id为1的用户(余额为50),购买id为1的图书(价格为80)

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

    此时执行更新用户余额的sql语句会抛出异常

package org.rain.spring.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.rain.spring.controller.BookController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /**
* @author liaojy
* @date 2023/8/27 - 1:16
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-tx-annotation.xml")
public class TxByAnnotation { @Autowired
private BookController bookController; @Test
public void testBuyBook(){
bookController.buyBook(1,1);
} }

14.3.6、测试执行的效果

14.3.6.1、执行前的数据

此时id为1的图书库存为100

此时id为1的用户余额为50

14.3.6.2、执行时的异常

14.3.6.3、执行后的数据

此时id为1的图书库存为99,少了一本

此时id为1的用户余额为50,没有变化

14.3.6.4、执行结果的总结

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

  • 这样的结果是错误的,因为购买图书是一个完整的流程:更新(图书)库存和更新(用户)余额,要么都成功,要么都失败

14.4、考虑事务的实现

14.4.1、添加事务功能的相关配置

    <!--配置事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 装配要进行事务管理的数据源 -->
<property name="dataSource" ref="datasource"></property>
</bean> <!--
tx:annotation-driven标签:开启事务的注解驱动;
通过@Transactional注解所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
transaction-manager属性:设置使用的事务管理器;
属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>

注意:tx:annotation-driven标签导入的名称空间需要 tx 结尾的那个

14.4.2、使用@Transactional注解

因为service层表示业务逻辑层,一个方法表示一个完整的功能,所以处理事务一般在service层使用@Transactional注解

14.4.3、测试事务的效果

14.4.3.1、执行前的数据

此时id为1的图书库存(已修改)为100

此时id为1的用户余额为50

14.4.3.2、执行时的异常

14.4.3.3、执行后的数据

由于使用了Spring的声明式事务,更新(图书)库存和更新(用户)余额,要么都成功,要么都失败;

本例属于都失败,所以(图书)库存和(用户)余额都没有变化

14.4.4、@Transactional注解的位置

  • 标识在方法上:只对该方法进行事务管理

  • 标识在类上:相当于对该类的所有方法都标识了@Transactional

14.5、事务的属性

14.5.1、只读

14.5.1.1、使用目的

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

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

14.5.1.2、使用方式

    @Transactional(readOnly = true)

14.5.1.3、注意事项

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

14.5.2、超时

14.5.2.1、使用目的

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

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

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

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

14.5.2.2、使用方式

注意:timeout属性默认值为-1,表示事务执行的时间可以无限长

    // 设置事务执行超过3秒,则强制回滚、结束事务、释放资源
@Transactional(timeout = 3)

14.5.2.3、使用效果

事务执行超过设定时间,除了强制回滚、结束事务、释放资源之外,还会抛出异常:TransactionTimedOutException

14.5.3、回滚策略

14.5.3.1、使用目的

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

  • rollbackFor属性(不常用):设置会造成回滚的异常,属性值需要一个Class类型的对象

  • rollbackForClassName属性(不常用):设置会造成回滚的异常,属性值需要一个字符串类型的全类名字符串

  • noRollbackFor属性:设置不造成回滚的异常,属性值需要一个Class类型的对象

  • noRollbackForClassName属性:设置不造成回滚的异常,属性值需要一个字符串类型的全类名字符串

注意:因为声明式事务默认对所有运行时异常都进行回滚,所以rollbackFor和rollbackForClassName属性不常用

14.5.3.2、使用方式

注意:由于(本示例)没进行异常处理,所以发生数学运算异常(ArithmeticException)时程序会中止;

而且因为回滚策略设置了当出现数学运算异常(ArithmeticException)时不需要进行回滚,

为了不导致数据错误,所以发生异常的代码最好放在更新(图书库存和用户余额)操作之后。

    // 设置当出现数学运算异常(ArithmeticException)时,不需要进行回滚
@Transactional(noRollbackFor = {ArithmeticException.class})

14.5.3.3、使用效果

14.5.3.3.1、执行前的数据

此时id为2的图书(价格为50)库存为100

此时id为1的用户余额为50

14.5.3.3.2、执行时的异常

因为回滚策略设置了当出现数学运算异常(ArithmeticException)时不需要进行回滚,因此购买图书的操作会正常执行

14.5.3.3.3、执行后的数据

此时id为2的图书(价格为50)库存为99,少了一本

此时id为1的用户余额为0,少了50(一本图书价格为50)

14.5.4、隔离级别

14.5.4.1、使用目的

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

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

  • SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度

  • 隔离级别越高,数据一致性就越好,但并发性越弱

隔离级别一共有四种:

  • 读未提交(READ UNCOMMITTED):允许Transaction01读取Transaction02未提交的修改

  • 读已提交(READ COMMITTED):要求Transaction01只能读取Transaction02已提交的修改

  • 可重复读(REPEATABLE READ):确保Transaction01可以多次从一个字段中读取到相同的值;

    即Transaction01执行期间禁止其它事务对这个字段进行更新

  • 串行化(SERIALIZABLE):确保Transaction01可以多次从一个表中读取到相同的行;

    在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作;

    该隔离级别可以避免任何并发问题,但性能十分低下

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

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
  • 脏读(DirtyRead):一个事务读取了另一个并行未提交事务写入的数据。

  • 不可重复读(Non-RepeatableRead):一个事务重新读取之前读取过的数据,

    发现该数据已经被另一个事务(在初始读之后提交)修改。

  • 幻读(PhantomRead):一个事务重新执行一个返回符合一个搜索条件的行集合的查询,

    发现满足条件的行集合因为另一个最近提交的事务而发生了改变。

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

隔离级别 Oracle MySQL SQL Server 达梦 人大金仓
READ UNCOMMITTED × ×
READ COMMITTED √(默认) √(默认) √(默认) √(默认)
REPEATABLE READ × √(默认) ×
SERIALIZABLE

14.5.4.2、使用方式

@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别(默认且常用)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化

14.5.5、传播行为

14.5.5.1、使用目的

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

14.5.5.1.1、创建结账业务层接口CheckoutService及其实现类

package org.rain.spring.service;

/**
* @author liaojy
* @date 2023/8/29 - 8:07
*/
public interface CheckoutService { void checkout(Integer[] bookIds, Integer userId); }

注意:checkout方法进行了事务管理,它调用的buyBook方法也进行了事务管理

package org.rain.spring.service.impl;

import org.rain.spring.service.BookService;
import org.rain.spring.service.CheckoutService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; /**
* @author liaojy
* @date 2023/8/29 - 8:10
*/
@Service
public class CheckoutServiceImpl implements CheckoutService { @Autowired
private BookService bookService; //一次购买多本图书
@Transactional
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId,userId);
}
} }
14.5.5.1.2、在控制层BookController中添加结账方法

    @Autowired
private CheckoutService checkoutService; public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
14.5.5.1.3、添加结账的测试方法

    @Test
public void testCheckout(){
Integer[] bookIds = {1,2};
bookController.checkout(bookIds,1);
}
14.5.5.1.4、执行结账前的数据

此时id为1的图书(价格为80)库存为100,id为2的图书(价格为50)库存为100

此时id为1的用户余额为100

14.5.5.1.5、执行结账时的异常

14.5.5.1.6、执行结账后的数据

此时id为1的图书(价格为80)库存为100,id为2的图书(价格为50)库存为100;库存没有变化

此时id为1的用户余额为100;余额没有变化

14.5.5.1.7、测试的数据结果分析
  • @Transactional注解的propagation属性的默认值为:Propagation.REQUIRED;

    表示如果有已经开启的事务可用,那么就在这个事务中运行。

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

  • 所购买的两本图书的价格为80和50,而用户的余额为100;

    因此在购买第二本图书时余额不足失败,导致整个checkout()回滚。

  • 换句话说,只要有一本书买不了,就都买不了。

14.5.5.2、使用方式

14.5.5.2.1、修改被调用方法的事务传播属性

    // 表示不管是否有已经开启的事务,都要开启新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
14.5.5.2.2、执行结账前的数据

此时id为1的图书(价格为80)库存为100,id为2的图书(价格为50)库存为100

此时id为1的用户余额为100

14.5.5.2.3、执行结账时的异常

14.5.5.2.4、执行结账后的数据

此时id为1的图书(价格为80)库存为99,id为2的图书(价格为50)库存为100;id为1的图书库存少了一本

此时id为1的用户余额为100;余额少了80(id为1的图书价格)

14.5.5.2.5、测试的数据结果分析
  • 同样的场景,每次购买图书都是在buyBook()的事务中执行。

  • 因此第一本图书购买成功,事务结束。

  • 第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响。

  • 换句话说,能买几本就买几本。

14、Spring之基于注解的声明式事务的更多相关文章

  1. spring基于注解的声明式事务控制

    package com.hope.service.impl;import com.hope.dao.IAccountDao;import com.hope.domain.Account;import ...

  2. 阶段3 2.Spring_10.Spring中事务控制_7 spring基于注解的声明式事务控制

    创建新项目 复制上一个pom.xml的内容.依赖和打包的方式 再复制src的代码过来 bean.xml.多导入context的声明 Service的实现类增加注解 dao的set方法删掉 通过Auto ...

  3. 28Spring_的事务管理_银行转账业务加上事务控制_基于注解进行声明式事务管理

    将applicationContext.xml 和 AccountServiceImpl 给备份一个取名为applicationContext2.xml 和 AccountServiceImpl2.j ...

  4. spring下春注解的声明式事务控制

    package com.hope.test;import com.hope.domain.Account;import com.hope.service.IAccountService;import ...

  5. Spring中基于XML的声明式事务控制配置步骤

    1.配置事务管理器 2.配置事务的通知 此时,我们就需要导入事务的约束 tx名称空间和约束,同时也需要aop的 使用tx:advice标签配置事务通知 属性: id:给事务通知起一个唯一标识 tran ...

  6. spring基于XML的声明式事务控制

    <?xml version="1.0" encoding="utf-8" ?><beans xmlns="http://www.sp ...

  7. spring基于xml的声明式事务控制配置步骤

    <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.sp ...

  8. Spring:(三) --常见数据源及声明式事务配置

    Spring自带了一组数据访问框架,集成了多种数据访问技术.无论我们是直接通过 JDBC 还是像Hibernate或Mybatis那样的框架实现数据持久化,Spring都可以为我们消除持久化代码中那些 ...

  9. 使用spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。

    使用spring声明式事务,spring使用AOP来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务.

  10. 阶段3 2.Spring_10.Spring中事务控制_6 spring基于XML的声明式事务控制-配置步骤

    环境搭建 新建工程 把对应的依赖复制过来 src下内容复制 配置spring中的声明事物 找到bean.xml开始配置 配置事物管理器 里面需要注入DataSource 2-配置事物通知 需要先导入事 ...

随机推荐

  1. 获得centos7网络yum源

    获得centos73网络yum源 wget http://mirrors.163.com/.help/CentOS7-Base-163.repo 网易 wget -O /etc/yum.repos.d ...

  2. Mysql 创建索引语句

    mysql有哪些索引 index 普通索引 alter table table_name add index index_name(column) 最基本的索引,没有任何限制 primary key ...

  3. es6数组解构的原理初探

    原理 以前只用过数组解构为数组,或者将其他类数组解构为数组,但是还不知道对象为什么不能解构为数组 后面学习到了Symbol.iterator属性以后才知道,只要一个对象是可迭代的,那它就可以迭代为数组 ...

  4. 18.9k star!一个高性能的嵌入式分析型数据库,主要用于数据分析和数据处理任务。

    大家好,今天给大家分享的是一个开源的面向列的关系数据库管理系统(RDBMS). DuckDB是一个嵌入式的分析型数据库,它提供了高性能的数据分析和数据处理能力.DuckDB的设计目标是为数据科学家.分 ...

  5. 泛型模板化设计DEMO

    泛型模板化设计DEMO 1. 定义Result泛型类 package com.example.core.mydemo.java.fanxing; public class Result<T> ...

  6. @ConfigurationProperties(prefix = “xx.xx.xx“) 从配置文件中取值赋给类的属性

    @ConfigurationProperties(prefix = "xx.xx.xx") 从配置文件中取值赋给类的属性 @ConfigurationProperties(pref ...

  7. LocalDateTime日期相互转换字符串

    /** LocalDateTime日期相互转换字符串 * 默认的时间日期样式 */ public static final String YYYYMMDDHHMMSSS_PATTERN = " ...

  8. windows离线部署VSCode在Centos7上的远程开发环境

    前言 公司一直使用的是ssh+vim的远程开发方式,习惯了vim之后已经非常方便了.但是还是想尝试一下VSCode的开发方式.就我而言,原因如下 漂亮的语法高亮,并且有补全 基于语法解析的引用查找(尽 ...

  9. Linux OpenGrok搭建

    目录 一.目的 二.环境 三.相关概念 3.1 OpenGrok 3.2 CTags 3.3 Tomcat 四.OpenGrok搭建 4.1 安装jdk 4.2 安装ctags依赖 4.3 安装uni ...

  10. Ubuntu 使用 SVN 管理 项目

    背景 公司的项目需要在 Linux 环境进行开发,而都是使用 SVN 进行管理的.习惯了 SVN-GUI 的我,需要学习 SVN 的命令行. 准备 安装 SVN sudo apt-get apt-ge ...