Spring的声明式事务详解

1.事务的概述

在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。

事务的四大特性(ACID):

1)原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。

2)一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。

3)隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。

4)持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中

2.声明式事务管理

Spring既支持编程式事务管理,也支持声明式的事务管理。

大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。

事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。

Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制

3.Spring提供的事务管理器

Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。

Spring的核心事务管理抽象是PlatformTransactionManager接口(新的顶级接口为:TransactionManager),它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。

事务管理器可以以普通的bean的形式声明在Spring IOC容器中。

在这里插入图片描述
主要处理器:

1)DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。

2)HibernateTransactionManager:用Hibernate框架存取数据库

4.演示无事务出现的问题

service层

package com.zyd.service.impl;

import com.zyd.dao.BookMapper;
import com.zyd.service.BookShopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author : ZYD
 * @date : 2022/10/25 21:01
 * @description
 */
@Service
public class BookShopServiceImpl implements BookShopService {

    @Autowired
    private BookMapper bookMapper;


    /**
     * 购买图书
     *
     * @param userId 用户id
     * @param isbn   国际标准图书编号
     */
    @Override
    public void purchase(int userId, String isbn) {
        //根据图书编号获得图书价格
        Double bookPrice = bookMapper.getBookPrice(isbn);
        //更新库存,一次买一本
        bookMapper.updateBookStock(isbn);
        //根据用户id和图书价格来更新用户的余额
        bookMapper.updateUserBalance(userId, bookPrice);
    }
}

配置文件

<?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="classpath:mybatis/db.properties"/>

    <context:component-scan base-package="com.zyd.service"/>


    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${jdbc.username}"/>
        <property name="driverClassName" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="password" value="${password}"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="com.zyd.pojo"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- mapper注解扫描器配置,扫描@MapperScan注解,自动生成代码对象 -->
    <bean id="mapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.zyd.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
</beans>

数据库数据

在这里插入图片描述
测试此时去买书

public class TransactionTest {

    ApplicationContext ioc = new ClassPathXmlApplicationContext("Spring/applicationContext.xml");

    @Test
    public void test() {
        BookShopService bookShopService = ioc.getBean(BookShopService.class);
        bookShopService.purchase(1, "1001");
    }
}

报错

Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Out of range value for column 'balance' at row 1; 
Data truncation: Out of range value for column 'balance' at row 1; 
nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Out of range value for column 'balance' at row 1

	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.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:91)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
	at com.sun.proxy.$Proxy14.update(Unknown Source)

提示balance余额超出了范围

测试后的数据库信息
在这里插入图片描述

从表中看出,余额没变,但是图书库存减少了,按正常来说,余额不足支付失败后,余额、库存都应该不变
也就是说此时book表应该回滚数据,所以我们要开启事务

5.解决上述问题的方法

解决办法:开启事务,配置声明式事务(基于xml的和基于注解的)

6. 基于注解的声明式事务

步骤一:引入依赖

<!--spring-aspects-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>

步骤二:在配置文件中配置

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

    <!--开启事务注解支持
        当事务管理器的id是transactionManager时,可以省略指定transaction-manager属性
    -->
<!--    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>-->
    <tx:annotation-driven></tx:annotation-driven>

在这里插入图片描述
步骤三:在需要事务的地方加@Transactional注解

@Transactional
    @Override
    public void purchase(int userId, String isbn) {
        //根据图书编号获得图书价格
        Double bookPrice = bookMapper.getBookPrice(isbn);
        //更新库存,一次买一本
        bookMapper.updateBookStock(isbn);
        //根据用户id和图书价格来更新用户的余额
        bookMapper.updateUserBalance(userId, bookPrice);
    }

经过以上三步,再次测试购买时,余额不足也会报错,但是图书的库存不会在减少,问题解决

7.基于XML的声明式事务

先把基于注解的信息注释掉

步骤一:配置配置文件

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

    <!--配置声明式事务-->
    <tx:advice id="tx" 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" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
			<tx:method name="update*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
			<tx:method name="delete*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="purchase" propagation="REQUIRES_NEW" isolation="READ_COMMITTED"></tx:method>
        </tx:attributes>
    </tx:advice>

    <!--AOP配置-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="pointCut"
                      expression="execution(* com.zyd..purchase(..))"/>
        <!--将事务方法和切入点表达式关联起来-->
        <aop:advisor advice-ref="tx" pointcut-ref="pointCut"></aop:advisor>
    </aop:config>

经过以上步骤,再次测试购买时,余额不足也会报错,但是图书的库存不会在减少,问题解决

8.@Transactional注解

可设置的属性

  • propagation=“REQUIRES_NEW” (事务的传播行为)
  • isolation=“READ_COMMITTED”(事务的隔离级别)
  • rollbackFor: 设置异常的类型,当事务中出现对应异常时立刻回滚
  • noRollbackFor:设置异常的类型,当事务中出现对应异常时不用回滚

在这里插入图片描述

9.事务的传播行为

Spring的7种传播行为

  • REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行;否则就启动一个新的事务,并在自己的事务内运行
  • REQUIRES_NEW 当前的方法必须启动新事务,并在自己的事务内运行;如果有事务正在运行,应该将它挂起
  • SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行,否则可以不运行在事务中。
  • NOT_SUPPORTED 当前的方法不应该运行在事务中,如果有运行的事务将它挂起
  • MANDATORY 当前的方法必须运行在事务中,如果没有正在运行的事务就抛出异常。
  • NEVER 当前的方法不应该运行在事务中,如果有正在运行的事务就抛出异常。
  • NESTED 如果有事务正在运行,当前的方法就应该在这个事务的嵌套事务内运行,否则就启动一个新的事务,并在它自己的事务内运行

10.事务的隔离级别

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

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值