04【Spring声明式事、传播行为、AOP事务控制】

04【Spring声明式事、传播行为、AOP事务控制】

一、Spring声明式事务

1.1 回顾事务

1.1.1 什么是事务?

如果一个业务操作中多次访问了数据库,必须保证每条SQL语句都执行成功。如果其中有一条执行失败,所有已经执行过的代码必须回滚。回到没有执行前的状态。称为事务。要么所有的SQL语句全部执行成功,要么全部失败。

1.1.2 事务四大特性

事务特性含义
原子性(Atomicity)事务是工作的最小单元,整个工作单元要么全部执行成功,要么全部执行失败
一致性(Consistency)事务执行前与执行后,数据库中数据应该保持相同的状态。如:转账前总金额与转账后总金额相同。
隔离性(Isolation)事务与事务之间不能互相影响,必须保持隔离性。
持久性(Durability)如果事务执行成功,对数据库的操作是持久的。

1.1.3 事务并发访问问题

并发访问下事务产生的问题:

当同时有多个用户在访问同一张表中的记录,每个用户在访问的时候都是一个单独的事务。

事务在操作时的理想状态是:事务之间不应该相互影响,实际应用的时候会引发下面三种问题。应该尽量避免这些问题的发生。通过数据库本身的功能去避免,设置不同的隔离级别。

  • 脏读: 一个事务(用户)读取到了另一个事务没有提交的数据
  • 不可重复读:一个事务多次读取同一条记录,出现读取数据不一致的情况。一般因为另一个事务更新了这条记录而引发的。
  • 幻读:在一次事务中,多次读取到的条数不一致

1.1.4 事务隔离级别

级别名字隔离级别脏读不可重复读幻读数据库默认隔离级别
1读未提交read uncommitted
2读已提交read committedOracle和SQL Server
3可重复读repeatable readMySQL
4串行化serializable
  • 1)Read uncommitted (读未提交): 简称RU隔离级别,所有事务中的并发访问问题都会发生,读取的是其他事务没有提交的数据

  • 2)Read committed (读已提交):简称RC隔离级别,会引发不可重复读和幻读的问题,读取的永远是其他事务提交的数据

  • 3)Repeatable read (可重复读):简称RR隔离级别,有可能会引发幻读的问题,一次事务读取到的同一行数据,永远是一样

  • 4)Serializable (串行化): 可以避免所有事务产生的并发访问的问题 效率及其低下

1.1.5 MySQL事务相关命令

  • 测试表:
drop table if exists account;

-- 创建数据表  
CREATE TABLE account (  
    id INT PRIMARY KEY AUTO_INCREMENT,  
    name VARCHAR(10),  
    money DOUBLE  
);  

-- 添加数据  
INSERT INTO account (name, money) VALUES ('a', 1000), ('b', 1000);
  • 查询事务隔离级别:
select @@tx_isolation;
  • 设置事务隔离级别:
set global transaction isolation level 四种隔离;

set global transaction isolation level read uncommitted;
set global transaction isolation level read committed;
set global transaction isolation level repeatable read;
set global transaction isolation level serializable;

需要重启客户端会话

  • 开启事务:
start transaction;
或者
begin;
  • 提交事务:
commit;
  • 回滚事务:
rollback;

1.2 Spring事务管理介绍

1.2.1 编程式事务

  • 什么是编程式事务?

编程式事务简单的来说就是采用编程的方式来管理事务,编程式事务需要将事务管理的代码写入到业务方法中,相对于核心业务而言,事务管理的代码显然属于非核心业务对核心业务代码的侵入性太大;而且如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余

我们之前使用的JDBC API来操作事务就是编程式事务;

1.2.2 声明式事务

  • 什么是声明式事务?

声明式事务以声明的方式来实现事务管理,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务简单的来说就是通过一些表达式声明(拦截)一堆需要被事务管理的方法,然后被声明(拦截)的方法通过AOP的方式进行事务管理(执行目标方法之前开启事务,执行目标方法之后提交或者回滚事务)

显然声明式事务管理要优于编程式事务管理:它将事务管理代码从业务方法中分离出来,这正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受干扰;一个普通的对象,只要加上注解就可以获得完全的事务支持。

声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

1.2.3 Spring事务管理器

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

Spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口。

它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。

  • 事务管理器主要的实现:
    • DataSourceTransactionManager:使用Spring JDBC 或 Mybatis 等基于DataSource数据源的持久化技术时,使用 该事务管理器
    • JpaTransactionManager:使用JPA时采用的事务管理器
    • JtaTransactionManager:具有多个数据源的全局事务使用该事务管理器
    • JdoTransactionManager:使用JDO进行持久化时 ,使用该事务管理器
    • HibernateTransactionManager:使用Hibernate进行持久化时,使用该事务管理器

1.2.4 事务传播行为

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

例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为,在org.springframework.transaction.TransactionDefinition类中被定义。

静态常量说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择,也是Spring的默认值
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

TransactionDefinition类中不仅定义了事务的传播行为,也定义了很多事务的隔离级别:

静态常量说明
ISOLATION_DEFAULT使用数据库默认的隔离级别,Spring默认值
ISOLATION_READ_UNCOMMITTED读未提交
ISOLATION_READ_COMMITTED读已提交
ISOLATION_REPEATABLE_READ可重复读
ISOLATION_SERIALIZABLE串行化

1.3 Spring实现声明式事务

1.3.1 案例准备

1)SQL脚本:
drop database if exists spring;

create database spring;

use spring;

-- 创建数据表  
CREATE TABLE account (  
    id INT PRIMARY KEY AUTO_INCREMENT,  
    name VARCHAR(10),  
    money DOUBLE  
);  

-- 添加数据  
INSERT INTO account (name, money) VALUES ('a', 1000), ('b', 1000);
2)Maven依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dfbz</groupId>
    <artifactId>01_Spring</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!--spring基本环境依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!--spring对jdbc的支持(DataSourceTransaction)-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!--spring对事物的支持-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!--spring对切面AspectsJ的支持-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <!--spring对junit的整合-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.9.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.1</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
    </dependencies>

</project>
3)jdbc.properties:
jdbc.username=root
jdbc.password=admin
jdbc.url=jdbc:mysql:///spring
jdbc.driverClassName=com.mysql.jdbc.Driver
4)实体类:
package com.dfbz.entity;

import lombok.Data;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@Data
public class Account {
    private Integer id;
    private String name;
    private Double money;
}

1.3.2 事务通知XML配置

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:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

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

    <!--加载配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

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

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

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

    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>

            <!--
                isolation:事务隔离级别
                    DEFAULT:使用数据库默认的隔离级别(默认值)
                    READ_UNCOMMITTED:读未提交
                    READ_COMMITTED:读已提交
                    REPEATABLE_READ:可重复读
                    SERIALIZABLE:串行化
                propagation: 事务传播行为
                    REQUIRED:必须要有事务,如果已经存在事务,则加入到这个事务中,如果没有事务则创建一个新的事务
                    SUPPORTS:支持当前事务,如果调用这个方法之前没有事务则不会开启新的事务
                    MANDATORY:必须存在事务,如果调用此方法之前没有事务,就抛出异常
                    REQUIRES_NEW:新建事务,如果调用此方法之前就已经开启过事务,则将之前的事务挂起
                    NOT_SUPPORTED:非事务方式,如果调用此方法之前就存在事务,则挂起事务
                    NEVER:非事务方式运行,如果调用此方法之前存在事务,则抛出异常
                    NESTED:如果存在事务,则嵌套在事务内执行,如果没有事务则与REQUIRED保存一致
               read-only: 是否只读
                    true: 只读
                    false:非只读(默认值)
               timeout: 方法执行超时时间,默认值为-1,代表永不超时,单位为秒
               rollback-for: 指定特定的异常才回滚(默认情况下,任何的运行时异常事务都会回滚,但编译时异常都不会进行回滚),该配置针对于编译时异常;可以写多个,以逗号隔开

               no-rollback-for: 指定一个特定的异常事务不回滚,可以写多个,以逗号隔开

            -->
<!--            <tx:method name="query*" isolation="DEFAULT"
                       propagation="REQUIRED" read-only="true" timeout="3"
                       rollback-for="java.lang.NullPointerException,java.lang.ClassCastException"
                       no-rollback-for="java.lang.ArithmeticException"  />
                  -->
<!--            <tx:method name="query*" read-only="true"/>-->
<!--            <tx:method name="select*" read-only="true"/>-->
<!--            <tx:method name="query*" read-only="true"/>-->
<!--            <tx:method name="save*" read-only="true"/>-->
<!--            <tx:method name="insert*" />-->
<!--            <tx:method name="add*" />-->
<!--            <tx:method name="update*" />-->
<!--            <tx:method name="modify*" />-->
<!--            <tx:method name="delete*" />-->
<!--            <tx:method name="remove*" />-->
            <!--出现ArithmeticException异常不回滚-->
            <tx:method name="transfer" no-rollback-for="java.lang.ArithmeticException"/>
        </tx:attributes>

    </tx:advice>

    <!--配置切面-->
    <aop:config>
        <!--定义切入点-->
        <aop:pointcut id="myPoint" expression="execution(* com.dfbz.service.AccountService.*(..))"/>

        <!--将通知应用到切入点上(织入)-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="myPoint"></aop:advisor>
    </aop:config>
</beans>

**默认情况下,任何的运行时异常事务都会回滚,但编译时异常都不会进行回滚;**我们可以通过一些配置来调整

  • no-rollback-for:指定什么异常不回滚,可以写多个,以逗号隔开;

  • rollback-for:该配置针对于编译时异常;可以写多个,以逗号隔开;

上述两个参数在指定异常时,如果指定某个异常的父类,包括这个异常的所有子类异常都会回滚;

如:no-rollback-for指定的参数是Exception则代表Java中的任何异常都不回滚,

rollback-for指定的参数是Exception则Java中任何的异常都回滚;

2)业务类:
package com.dfbz.service;

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

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@Service
public class AccountService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * @param str     : 谁要转账
     * @param target: 转账给谁
     * @param flag:   模拟异常 true:出现异常 false:不出现异常
     */
    public void transfer(String str, String target, Boolean flag) {

        jdbcTemplate.update("update account set money=money-500 where name=?", str);

        if (flag) {
            // 模拟异常
            int i = 1 / 0;
        }

        jdbcTemplate.update("update account set money=money+500 where name=?", target);
    }
    
    /**
     *
     * @param id
     */
    public void findById(Integer id) {

        Account account = jdbcTemplate.queryForObject(
                "select * from account where id=?",
                new BeanPropertyRowMapper<>(Account.class),
                id
        );
    }
}
3)测试类
package com.dfbz.test;

import com.dfbz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class Demo01 {

    @Autowired
    private AccountService accountService;

    @Test
    public void test1(){
        accountService.transfer("a","b",false);
    }
}

1.3.3 注解配置

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:tx="http://www.springframework.org/schema/tx"
       xmlns:context="http://www.springframework.org/schema/context"
       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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

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

    <!--加载配置文件-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

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

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

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

    <!--
        开启事务注解驱动
        transaction-manager: 指定事务管理的名称,默认为transactionManager
    -->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

</beans>
2)业务类
package com.dfbz.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@Service
public class AccountService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * @param str     : 谁要转账
     * @param target: 转账给谁
     * @param flag:   模拟异常 true:出现异常 false:不出现异常
     */
/*    @Transactional(
            transactionManager = "transactionManager",              // 事务管理器的名称
            propagation = Propagation.REQUIRED,                     // 传播行为
            isolation = Isolation.DEFAULT,                          // 隔离级别
            timeout = 3000,                                         // 超时时间
            readOnly = false,                                       // 是否只读
            rollbackFor = {MyException.class},       				// 出现该异常回滚
            noRollbackFor = {ArithmeticException.class}             // 出现指定异常不回滚
    )    */
    @Transactional              // 使用注解方式管理事务
    public void transfer(String str, String target, Boolean flag) {

        jdbcTemplate.update("update account set money=money-500 where name=?", str);

        if (flag) {
            // 模拟异常
            int i = 1 / 0;
        }

        jdbcTemplate.update("update account set money=money+500 where name=?", target);
    }
}
3)测试类
package com.dfbz.test;

import com.dfbz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author lscl
 * @version 1.0
 * @intro:
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring2.xml")
public class Demo02 {

    @Autowired
    private AccountService accountService;

    @Test
    public void test1(){

        accountService.transfer("a","b",false);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

緑水長流*z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值