IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring事务管理学习笔记 -> 正文阅读

[Java知识库]Spring事务管理学习笔记

五、Spring事务管理

5.1 梗概

事务,其实这个概念是数据库的知识点,但这里还是讲一下,他是数据库操作的最基本单元,逻辑上一组操作,要么都成功,要么有一个失败,所有的操作就都失败。这个时候我们就需要进行回滚(刚才的加成功的减掉,减成功地加上。。。。,回到刚才没有执行这个事务之前的状态)。

典型的案例:银行转账
现在,Mike 要给 Amy 转520,那么
Mike 的账户要 -520
Amy 的账户要 +520
这两个操作要都成功,这个事务才算完成,如果Mike的钱-520之后,银行的服务器恰好断电了,导致Amy的账户没能加上这520,那么这个事务就会失败,然后回滚,把520 再给Mike加上。

事务有四个特性(ACID):

  • 原子性:他是最小的执行单元
  • 一致性:简单说就是现在Mike少了520,那么Amy就是多520,我称之为 守恒
  • 隔离性:多事务操作的时候,事务之间互不影响
  • 持久性:操作成功后,数据会修改并保存在数据库,不像变量用完就被回收了

5.2 环境搭建

在Spring框架中开发的时候,我们通常把系统分成三个层,一个是用户操作的Web层,然后是Web层直接调用的Service层(主要实现业务操作),Service层再去调用我们的Dao层(主要执行数据库操作,不写业务)。

下面,我们以事务的典型案例转账为例,基本思想图解如下:
(1)在Dao层写 加钱 和 减钱 两个方法
(2) 然后在Service层调用这两个方法
(3)最后我们用测试类模拟web点击操作的过程
在这里插入图片描述
废话话不多说,咱们实际操作案例来一波:
先在数据库中建表 (表名:account):
在这里插入图片描述
在这里插入图片描述
之后配置我们的jar包,添加(pom.xml):

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.9</version>
        </dependency>

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>aopalliance</groupId>
            <artifactId>aopalliance</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.6</version>
        </dependency>

然后写我们的jdbc配置文件(jdbc.properties):

prop.driverClass=com.mysql.cj.jdbc.Driver
prop.url=jdbc:mysql://localhost:3306/user_db?&useSSL=false&serverTimezone=UTC
prop.username=root
prop.password=root

再写我们的配置文件:

<?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
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--开启注解扫描-->
    <context:component-scan base-package="com.example"></context:component-scan>

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

    <!--配置数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>

    <!--JdbcTemplate对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
</beans>

写完这些,就可以开始写我们的主逻辑代码了:
先写我们的dao层:
AccountDao.java

package com.example.dao;

public interface AccountDao {
    public void addMoney(String id, Integer money);     //加钱
    public void reduceMoney(String id, Integer money);     //减钱
}

AccountDaoImpl.java

package com.example.dao.impl;

import com.example.dao.AccountDao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;

@Repository
public class AccountDaoImpl implements AccountDao {

    /*注入jdbc实现数据库操作*/
    @Resource
    private JdbcTemplate jdbcTemplate;

    /*加钱*/
    @Override
    public void addMoney(String id, Integer money) {
        String sql = "update account set money = money + ? where id = ?";
        /*参数记得按照问号的顺序来*/
        Object[] args = {money, id};
        jdbcTemplate.update(sql,args);
        System.out.println("加钱操作完成");
    }

    /*减钱*/
    @Override
    public void reduceMoney(String id, Integer money) {
        String sql = "update account set money = money - ? where id = ?";
        /*参数记得按照问号的顺序来*/
        Object[] args = {money, id};
        jdbcTemplate.update(sql, args);
        System.out.println("减钱操作完成");
    }
}

然后写我们的Service层:
AccountService:

package com.example.service;

import com.example.dao.AccountDao;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class AccountService {
    /*注入accountDao,调用接口*/
    @Resource
    private AccountDao accountDao;

    /*id_1用户把money转给id_2用户*/
    public void transferMoney(String id_1, String id_2, Integer money){
        /*减钱*/
        accountDao.reduceMoney(id_1, money);
        /*加钱*/
        accountDao.addMoney(id_2,money);
        System.out.println("转账事务执行成功");
    }
}

最后是我们的测试类:

package com.example.test;

import com.example.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTest {
    @Test
    public void test_1(){
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = context.getBean("accountService", AccountService.class);
        /*id为1的用户给id为2的用户转账520*/
        accountService.transferMoney("1","2",520);

    }
}

运行结果如下:(可见Mike的爱心520,Amy已收到)
在这里插入图片描述

5.3 事务场景引入

上面这么做,乍一看好像没毛病。但是,前面说过了,如果突然出现服务器断电等等情况,Mike的520可能就不翼而飞了。我们用异常来模拟一下断电情况:
在这里插入图片描述
修改完这个业务逻辑代码后,我们再次运行测试方法会发现:
Mike减钱的操作执行了,但是Amy由于异常情况收不到Mike的钱,这么搞,问题就大条了!!!
在这里插入图片描述在这里插入图片描述
那么,我们该怎么补救呢?这就需要用到我们的事务管理了。

5.4 Spring事务管理

对事务的操作分为以下四个步骤:
1、开启事务
2、编写业务逻辑代码
3-1、没有发生异常,提交事务
3-2、发生异常,回滚操作

另外,Spring事务管理API 针对不同的框架提供不同的实现类。
使用jdbc或者mybatis的时候,我们使用的是DataSourceTransactionManager实现类
使用hibernate的话,我们使用的是HibernateTransactionManager实现类。
在这里插入图片描述
在Spring中进行事务管理操作有两种方式,一种是编程式事务管理(繁琐,一般不会这么用,这里不做过多介绍),一种是声明式事务管理(常用),下面将就声明式事务管理 使用jdbc的形式给出详细的代码和解析。

5.5 声明式事务管理(基于注解方式)

声明式事务管理,底层使用AOP原理,在方法前后分别切入 开启事务管理 和 提交事务。

Spring框架使用声明式事务管理分成以下几个步骤:
1、在配置文件中创建事务管理器
2、在配置文件中开启事务注解(需要引入tx名称空间)
3、在Service类或类方法上添加@Transactional 事务注解

回归正题,我们首先要在配置文件中创建事务管理器。
在applicationContext.xml中添加代码如下:

    <!--创建事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源,说明-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

然后还需要开启事务注解(记得在最前面添加tx名称空间):

<?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: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/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--开启注解扫描-->
    <context:component-scan base-package="com.example"></context:component-scan>

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

    <!--配置数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>

    <!--JdbcTemplate对象-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!--注入dataSource-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--创建事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源,说明-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--开启事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>

然后我们就可以在Service类或者Service类中的方法上边添加事务注解@Transactional
添加在类上边表示,为这个类的所有方法添加事务
添加在方法上边,表示只为这个方法添加事务

回归我们的源码,在我们的Service中添加@Transactional注解如下:

package com.example.service;

import com.example.dao.AccountDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
@Transactional
public class AccountService {
    /*注入accountDao,调用接口*/
    @Resource
    private AccountDao accountDao;

    /*id_1用户把money转给id_2用户*/
    public void transferMoney(String id_1, String id_2, Integer money){
        /*减钱*/
        accountDao.reduceMoney(id_1, money);

        /*出现异常情况*/
        int i = 10 / 0;

        /*加钱*/
        accountDao.addMoney(id_2,money);
        System.out.println("转账事务执行成功");
    }
}

然后我们再次执行测试方法,观看此时的数据库数据变化(可见,一模一样,Mike的520并没有不翼而飞):
在这里插入图片描述
在这里插入图片描述

5.6 @Transactional注解的参数详解

参数描述
propagation事务传播行为
ioslation事务隔离级别
timeout设置超时时间,事务必须在此时间内提交,否则事务回滚(默认值是-1,自动设置以秒为单位)
readOnly是否只读,默认值是false,表示可以增删改查,设置为true,只能做查询操作
rollbackFor设置出现哪些异常进行事务回滚
noRollbackFor设置出现哪些异常不进行事务回滚

5.6.1 事务传播行为

在这里插入图片描述
Spring框架事务传播行为有7种:(常用的是REQUIRED和REQUIRED_NEW
以下描述都是针对给update加事务并附上以下各种属性而论
在这里插入图片描述
SUPPORTS:如果add有事务,那么update在add中运行,否则update在自己的事务中运行。
NOT_SUPPORT:如果add有事务,那么将add挂起,update运行在自己的事务中。
MANDATORY:update只能被有事务的方法调用,如果add方法不是事务,那么调用update的时候,抛出异常。
NEVER:update不能被有事务的方法调用,如果add方法是事务,那么抛出异常。
NESTED:如果add方法是事务,那么update在add事务的嵌套事务内运行,否则,update自己启动一个新的事务,并在自己的事务内运行。

默认的传播行为是REQUIRED,调用方法如下:

@Transactional(propagation = Propagation.REQUIRED)

在这里插入图片描述

5.6.2 事务隔离级别

ioslation:事务隔离级别
事务有特性成为隔离性,多事务操作之间不会产生影响。不考虑隔离性会产生很多问题。
典型的有三种读的问题:
1、脏读:事务A正在修改数据a 5000,并 - 4000 改为 1000,且尚未提交,此时事务B也读取了数据a 5000,并 + 1000 改为 6000,这个时候,A提交数据a,a就变成1000,b又提交数据a,a又变成了6000,显然,这个数据就乱了。(简单说就是,事务A正在改数据a的时候,事务B读取了原先的a进行修改,此时事务B读取的数据其实就脏了,正常情况,他得等事务A改完数据a再读取这个数据a)
2、不可重复读:事务A读取数据a 5000,此时事务B也读取数据a 5000并修改为500,且提交事务,事务A又再读取一次数据a,此时读取的就是500,两次读取的数据不同,就是不可重复读。
3、虚读:一个未提交事务读取到另一个提交事务添加数据。
在这里插入图片描述
通过设置事务隔离性,可以有效避免以上三种读得问题,他的属性值可以取以下几个值:
在这里插入图片描述
格式:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)

5.7 声明式事务管理(基于XML)

在配置文件中配置事务管理器

    <!--配置数据库连接池-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="url" value="${prop.url}"/>
        <property name="username" value="${prop.username}"/>
        <property name="password" value="${prop.password}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>
    
    <!--创建事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据源,说明-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

配置通知

    <tx:advice id="myAdvice">
        <!--配置事务参数-->
        <tx:attributes>
            <!--指定在哪种规则的方法上添加事务,下面的例子是直接写具体的方法名-->
            <tx:method name="transferMoney"/>
            <!--
                如果是要指定存在什么字段的方法的话,比如以trans开头的配置如下:
                <tx:method name="trans*"/>
            -->
        </tx:attributes>
    </tx:advice>

也可以在tx:method中加自己需要的各种属性如下:
在这里插入图片描述
配置切入点和切面

    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="accountPC" expression="execution(* com.example.service.AccountService.*(..))"/>
        <!--配置切面-->
        <aop:advisor advice-ref="myAdvice" pointcut-ref="accountPC"/>
    </aop:config>

该写的都写了,现在我们把下面这一段注释掉,然后运行我们的测试方法
在这里插入图片描述
再去刷新数据库的数据信息,可见事务正常回滚。
在这里插入图片描述

5.8 声明式事务管理(完全注解方式)

配置类代码如下:

package com.example.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration      //配置类
@ComponentScan(basePackages = "com.example")    //组件扫描
@EnableTransactionManagement        //开启事务
public class TxConfig {
    //创建数据库连接池
    @Bean
    public DruidDataSource getDruidDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/user_db?&useSSL=false&serverTimezone=UTC");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    //创建jdbcTemplate对象
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        /*注入dataSource*/
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    //创建事务管理器
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager manager = new DataSourceTransactionManager();
        manager.setDataSource(dataSource);
        return manager;
    }
}

然后我们把刚才注释掉的注解取消掉:
在这里插入图片描述
编写测试方法:

    @Test
    public void test(){
        ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
        AccountService accountService = context.getBean("accountService", AccountService.class);
        /*id为1的用户给id为2的用户转账520*/
        accountService.transferMoney("1","2",520);
    }

运行后跟之前有xml的时候一模一样,大功告成。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-17 15:15:56  更:2021-08-17 15:17:48 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/21 5:17:54-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码