Spring事务详解

标签: Spring  java  spring  mysql

一、什么是事务

事务,是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行,要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。

我们知道,在JavaEE的开发过程中,service方法用于处理主要的业务逻辑,而业务逻辑的处理往往伴随着对数据库的多个操作。以我们生活中常见的转账为例,service方法要实现将A账户转账到B账户,则该方法内必定要有两个操作:先将A账户的金额减去要转账的数目,然后将B账户加上相应的金额数目。这两个操作必定要全部成功,方才表示本次转账成功;若有任何一方失败,则另一方必须回滚(即全部失败)。事务指的就是这样一组操作:这组操作是不可分割的,要么全部成功,要么全部失败。

二、事务的ACID特性

  • 原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
    主要涉及InnoDB事务。相关特性:事务的提交、回滚、信息表。

  • 一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态。在事务开始前后,数据库的完整性约束没有被破坏。
    若违反了唯一性,必须撤销事务,返回初始状态。主要涉及内部InnoDB处理,以保护数据不受崩溃,相关特性:双写缓冲、崩溃恢复。

  • 隔离性:每个读写事务的对象对其他事务的操作对象能相互分离,即:事务提交前对其他事务是不可见的,通常内部加锁实现。
    主要涉及事务,尤其是事务隔离级别,相关特性:隔离级别、innoDB锁的底层实现细节。

  • 持久性:一旦事务提交,则其所做的修改会永久保存到数据库。涉及到mysql软件特性与特定硬件配置的相互影响。
    相关特性:4个配置项:双写缓冲开关、事务提交刷新log的级别、binlog同步频率、表文件;写缓存、操作系统对fsync()的支持、备份策略等。

数据库在事务的提交、回滚或运行中,总是处于一个一致的状态。若关联数据跨多张表更新,查询时只能看见全部老数据/全部新数据,而非新老数据混合。

三、事务的传播机制

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就需要事务传播机制的配置来确定如何执行。

划重点:因为Spring是使用AOP来代理事务控制,是针对于接口或类的,所以在同一个Service类中两个方法的调用,传播机制是不生效的。

Spring定义了七种传播行为如下:

1.PROPAGATION_REQUIRED(默认)(常用)

支持当前事务,如果当前没有事务,则新建事务。
如果当前存在事务,则加入当前事务,合并成一个事务。

2.PROPAGATION_REQUIRES_NEW(次常用:一般用在子方法需要单独事务的情况)

新建事务,如果当前存在事务,则把当前事务挂起。
这个方法会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交。

3.PROPAGATION_NESTED

如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父级事务结束才提交。
如果当前没有事务,则新建事务。
如果它异常,父级可以捕获它的异常而不进行回滚,正常提交。
但如果父级异常,它必然回滚,这就是和REQUIRES_NEW的区别。

4.PROPAGATION_SUPPORTS

如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行,完全依赖外层的事务。

5.PROPAGATION_NOT_SUPPORT

该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码。

6.PROPAGATION_NEVER

该传播机制不支持外层事务,即如果外层有事务就抛出异常。

7.PROPAGATION_MANDATORY

与NEVER相反,如果外层没有事务则抛出异常。

四、事务的隔离级别

一个数据库可能有多个客户端访问,这些客户端访问数据库时,如果不考虑并发事务隔离性,就会出现以下问题。这些问题分为类,包括三类数据读问题:脏读、不可重复读、幻读。两类数据更新问题:第一类丢失更新、第二类丢失更新。

并发事务下出现的问题

1.脏读

A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这时候如果事务B回滚,那么A事务读到的数据是不被承认的。例如常见的取款事务和转账事务:
在这里插入图片描述

2.不可重复读

不可重复读是指A事务读取了B事务已经提交的更改数据。假如A在取款事务的过程中,B往该账户转账100,A两次读取的余额发生不一致。

3.幻读

A事务读取B事务提交的新增数据,会引发幻读问题。幻读一般发生在计算统计数据的事务中,例如银行系统在同一个事务中两次统计存款账户的总金额,在两次统计中,刚好新增了一个存款账户,存入了100,这时候两次统计的总金额不一致。
在这里插入图片描述
划重点:不可重复读和幻读的区别是:前者是读到了已经提交的事务的更改数据(修改或删除);后者是读到了其他已提交事务的新增数据。对于这两种问题采用不同的方法解决:防止读到更改数据,只需要对操作的数据添加行级锁,防止操作中的数据发生变化;防止读到新增数据,往往需要添加表级锁,将整张表锁定。

4.第一类丢失更新

A事务撤销时,把已经提交的B事务的更新数据覆盖了。
在这里插入图片描述
这时候取款事务A撤销事务,余额恢复为1000,这就丢失了更新。

5.第二类丢失更新

A事务覆盖B事务已提交的数据,造成B事务所做的操作丢失。
在这里插入图片描述
为了解决上述问题,数据库通过锁机制解决并发访问的问题。根据锁定对象不同:分为行级锁和表级锁;根据并发事务锁定的关系上看:分为共享锁定和独占锁定,共享锁定会防止独占锁定但允许其他的共享锁定。而独占锁定既防止共享锁定也防止其他独占锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,insert、update、delete和selsct for update语句都会隐式采用必要的行锁定。

但是直接使用锁机制管理是很复杂的,基于锁机制,数据库给用户提供了不同的事务隔离级别,只要设置了事务隔离级别,数据库就会分析事务中的sql语句然后自动选择合适的锁。

隔离级别

1.READ UNCOMMITED 读未提交

如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务A读取到了事务B未提交的数据。

解决了更新丢失,但还是可能会出现脏读。

2.READ COMMITED 不可重复读

如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

解决了更新丢失和脏读问题。

3.REPEATABLE READ 可重复读

可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。

解决了更新丢失、脏读、不可重复读、但是还会出现幻读。

补充:可重复读在读事务内是禁止针对数据内容的写操作的,但是不能禁止其他操作,比如增加删除记录,所以会造成幻读。

4.SERIALIZABLE 串行化

提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读

解决了更新丢失、脏读、不可重复读、幻读(虚读)。

5.四种隔离机制对比

在这里插入图片描述
在MYSQL数据库中,支持上面四种隔离级别,默认的为Repeatable read(可重复读);而在Oracle数据库中,只支持Serializeble(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed级别。

划重点:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。

五、Spring事务管理

Spring 事务管理为我们提供了三个高层抽象的接口,分别是TransactionProxyFactoryBean,TransactionDefinition,TransactionStatus。

1.PlatformTransactionManager事务管理器

Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,Spring框架并不直接管理事务,而是通过这个接口为不同的持久层框架提供了不同的PlatformTransactionManager接口实现类,也就是将事务管理的职责委托给Hibernate或者iBatis等持久化框架的事务来实现。

//使用JDBC或者iBatis进行持久化数据时使用
org.springframework.jdbc.datasource.DataSourceTransactionManager
//使用hibernate5版本进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager
//使用JPA进行持久化数据时使用
org.springframework.orm.jpa.JpaTransactionManager
//当持久化机制是jdo时使用
org.springframework.jdo.JdoTransactionManager
//使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用
org.springframework.transaction.jta.JtaTransactionManager

PlatformTransactionManager接口源码:

public interface PlatformTransactionManager {
    //事务管理器通过TransactionDefinition,获得“事务状态”,从而管理事务
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //根据状态提交
    void commit(TransactionStatus var1) throws TransactionException;
   //根据状态回滚
    void rollback(TransactionStatus var1) throws TransactionException;
}

2.TransactionDefinition定义事务基本属性

org.springframework.transaction.TransactionDefinition接口用于定义一个事务,它定义了Spring事务管理的五大属性:隔离级别、传播行为、是否只读、事务超时、回滚规则

源码如下。

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    int getPropagationBehavior();

    int getIsolationLevel();

    int getTimeout();

    boolean isReadOnly();

    @Nullable
    String getName();
}

传播机制和隔离级别在上面我们已经阐述过了,我们来看其它三个属性。

是否只读

如果将事务设置为只读,表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。

事务超时

为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。

假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。

由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。

事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。在 TransactionDefinition 中以 int 的值来表示超时时间,默认值是-1,其单位是秒。

回滚规则

在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。

3.TransactionStatus事务状态

org.springframework.transaction.TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法,用来获取或判断事务的相应状态信息。

TransactionStatus接口源码:

public interface TransactionStatus extends SavepointManager, Flushable {
    boolean isNewTransaction();	//是否是新的事务

    boolean hasSavepoint();		//是否有恢复点

    void setRollbackOnly();	//设置为只回滚

    boolean isRollbackOnly();	//是否为只回滚

    void flush();	//刷新

    boolean isCompleted();	//是否已完成
}

六、Spring事务配置方式

Spring 事务管理有两种方式:编程式事务管理、声明式事务管理
编程式事务管理通过TransactionTemplate手动管理事务,在实际应用中很少使用,我们重点来看声明式事务管理。

声明式事务管理有三种实现方式:

准备工作

1.初始化数据库表

create table account
(
  id    bigint auto_increment primary key,
  name  varchar(32) not null,
  money bigint      not null,
  constraint account_name_uindex
  unique (name)
);
insert into account (name, money) values('Bill', 2000),('Jack', 2000);

2.创建DAO实现类

public class TransferDaoImpl extends JdbcDaoSupport implements TransferDao {

    /**
     * @param name 账户名称
     * @param amount 支出金额
     */
    @Override
    public void payMoney(String name, Long amount) {

        String sql = "update account set money=money-? where name=?";
        this.getJdbcTemplate().update(sql, amount, name);
    }

    /**
     * @param name 账户名称
     * @param amount 收入金额
     */
    @Override
    public void collectMoney(String name, Long amount) {

        String sql = "update account set money=money+? where name=?";
        this.getJdbcTemplate().update(sql, amount, name);
    }
}

3.创建Service实现类(事务管理类)

public class TransferServiceImpl implements TransferService {

    private TransferDao transferDao;

    public void setTransferDao(TransferDao transferDao) {
        this.transferDao = transferDao;
    }
     /**
     * @param source 支出方账户名称
     * @param name 收入方账户名称
     * @param amount 转账金额
     */
    @Override
    public void transferMoney(String source, String destination, Long amount) {
        transferDao.payMoney(source, amount);
        int i = 100/0;//此处用于测试抛异常时是否会回滚
        transferDao.collectMoney(destination, amount);
    }
}

4.创建Spring核心配置文件

    <!-- 读取db.properties配置信息 -->
    <context:property-placeholder location="db.properties"></context:property-placeholder>
    <!-- 配置c3p0数据源 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${db.driverClass}" />
        <property name="jdbcUrl" value="${db.url}" />
        <property name="user" value="${db.username}" />
        <property name="password" value="${db.password}" />
    </bean>

    <bean id="transferDao" class="com.tx.dao.impl.TransferDaoImpl">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="transferService" class="com.tx.service.impl.TransferServiceImpl">
        <property name="transferDao" ref="transferDao" />
    </bean>
1.基于TransactionProxyFactoryBean的方式

在spring核心配置文件中添加事务管理器的配置和TransactionProxyFactoryBean代理对象。

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

    <!--配置业务层的代理-->
    <bean id="transferServiceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <!--配置目标对象-->
        <property name="target" ref="transferService" />
        <!--注入事务管理器-->
        <property name="transactionManager" ref="transactionManager" />
        <!--注入事务属性-->
        <property name="transactionAttributes">
            <props>
                <!--
                    prop的格式:
                        * PROPAGATION :事务的传播行为
                        * ISOLATION :事务的隔离级别
                        * readOnly :是否只读
                        * -Exception :发生哪些异常回滚事务
                        * +Exception :发生哪些异常不回滚事务
                -->
                <prop key="transfer*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {

    @Autowired
    TransferService transferService;

    @Resource(name="transferServiceProxy")
    TransferService transferServiceProxy;

    @Test
    public void contextLoads() {
        //注意,此处引入的是代理对象transferServiceProxy,而不是transferService
        transferServiceProxy.transferMoney("Bill","Jack", 200L);
    }
}
2.基于AspectJ的XML方式

在spring核心配置文件中添加事务管理器的配置、事务的增强以及切面。

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

   <!--配置事务的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="transfer*" propagation="REQUIRED" />
        </tx:attributes>
    </tx:advice>

    <!--配置切面-->
    <aop:config>
        <aop:pointcut id="pointcut1" expression="execution(* com.tx.service.impl.*ServiceImpl.*(..))" />
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1" />
    </aop:config>

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {

    @Autowired
    TransferService transferService;

    @Test
    public void contextLoads() {
        transferService.transferMoney("Bill","Jack", 200L);
    }
}
3.基于注解的方式

在spring核心配置文件中添加事务管理器的配置和开启事务注解。

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

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

在事务方法中添加@Transaction注解。

    @Transactional
    public void transferMoney(String source, String destination, Long amount) {

        transferDao.payMoney(source, amount);
        int i = 100/0;
        transferDao.collectMoney(destination, amount);
    }

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {

    @Autowired
    TransferService transferService;

    @Test
    public void contextLoads() {
        transferService.transferMoney("Bill","Jack", 200L);
    }
}
4.小结

在声明式事务管理的三种实现方式中,基于TransactionProxyFactoryBean的方式需要为每个进行事务管理的类配置一个TransactionProxyFactoryBean对象进行增强,所以开发中很少使用;基于AspectJ的XML方式一旦在XML文件中配置好后,不需要修改源代码,所以开发中经常使用;基于注解的方式开发较为简单,配置好后只需要在事务类上或方法上添加@Transaction注解即可,所以开发中也经常使用。

【事务隔离机制】参考
https://blog.csdn.net/starlh35/article/details/76445267
https://blog.csdn.net/zhouym_/article/details/90381606

【Spring事务管理】参考
https://www.cnblogs.com/liantdev/p/10149443.html
https://www.cnblogs.com/mseddl/p/11577846.html

版权声明:本文为weixin_43094361原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43094361/article/details/106332557

智能推荐

phpstudy的mysql版本升级至5.7

phpstudy安装的mysql版本一般都是5.5或5.4的,但是有时候做项目又必须用到mysql5.7版本,所以我们现在来看一下如何在phpstudy的环境下将mysql版本升级至5.7   温馨提醒: 先删掉所有环境变量,如果是之前有的话,不然怎么安装cmd上指向的还是原来的版本。安装完再设新的环境变量。 并且卸载掉mysqld服务mysqld remove。如果不先删除的话,可能会...

RIP/DHCP/ACL综合实验

组播: 加入组的组成员才会接受到消息,只需要将流量发送一次到组播地址 减少控制面流量,减少头部复制, RIP1  广播   有类  不支持认证 RIP2  组播   无类  (支持VLAN)、支持认证 所有距离矢量路由协议:具有距离矢量特征的协议,都会在边界自动汇总 控制平面  路由的产生是控制平面的流量 数据平面  ...

【Sublime】使用 Sublime 工具时运行python文件

使用 Sublime 工具时报Decode error - output not utf-8解决办法   在菜单中tools中第四项编译系统 内最后一项增添新的编译系统 自动新建 Python.sublime-build文件,并添加"encoding":"cp936"这一行,保存即可 使用python2 则注释encoding改为utf-8 ctr...

java乐观锁和悲观锁最底层的实现

1. CAS实现的乐观锁 CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的,也可以理解为自旋锁 JUC是指import java.util.concurrent下面的包, 比如:import java.util.concurrent.atomic.AtomicInteger; 最终实现是汇编指令:lock...

Python 中各种imread函数的区别与联系

  原博客:https://blog.csdn.net/renelian1572/article/details/78761278 最近一直在用python做图像处理相关的东西,被各种imread函数搞得很头疼,因此今天决定将这些imread总结一下,以免以后因此犯些愚蠢的错误。如果你正好也对此感到困惑可以看下这篇总结。当然,要了解具体的细节,还是应该 read the fuc...

猜你喜欢

用栈判断一个字符串是否平衡

注: (1)本文定义:左符号:‘(’、‘[’、‘{’…… 右符号:‘)’、‘]’、‘}’……. (2)所谓的字符串的符号平衡,是指字符串中的左符号与右符号对应且相等,如字符串中的如‘(&r...

JAVA环境变量配置

位置 计算机->属性->高级系统设置->环境变量 方式一 用户变量新建path 系统变量新建classpath 方式二 系统变量 新建JAVA_HOME,值为JDK路径 编辑path,前加 方式三 用户变量新建JAVA_HOME 此路径含lib、bin、jre等文件夹。后运行tomcat,eclipse等需此变量,故最好设。 用户变量编辑Path,前加 系统可在任何路径识别jav...

常用的伪类选择器

CSS选择器众多 CSS选择器及权重计算 最常用的莫过于类选择器,其它的相对用的就不会那么多了,当然属性选择器和为类选择器用的也会比较多,这里我们就常用的伪类选择器来讲一讲。 什么是伪类选择器? CSS伪类是用来添加一些选择器的特殊效果。 常用的为类选择器 状态伪类 我们中最常见的为类选择器就是a标签(链接)上的为类选择器。 当我们使用它们的时候,需要遵循一定的顺序问题,否则将可能出现bug 注意...

ButterKnife的使用介绍及原理探究(六)

前面分析了ButterKnife的源码,了解其实现原理,那么就将原理运用于实践吧。 github地址:       点击打开链接 一、自定义注解 这里为了便于理解,只提供BindView注解。 二、添加注解处理器 添加ViewInjectProcessor注解处理器,看代码, 这里分别实现了init、getSupportedAnnotationTypes、g...

1.写一个程序,提示输入两个字符串,然后进行比较,输出较小的字符串。考试复习题库1|要求:只能使用单字符比较操作。

1.写一个程序,提示输入两个字符串,然后进行比较,输出较小的字符串。 要求只能使用单字符比较操作。 参考代码: 实验结果截图:...