Spring事务管理


什么是事务

事务是逻辑上的一组操作,要么都执行,要么都不执行。

原子性

事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;

一致性

执行事务前后,数据保持一致;

持久性

一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

隔离性

并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;

Spring 事务接口介绍

  • PlatformTransactionManager 事务管理器
  • TransactionDefinition 事务定义形象(隔离、传播、超时、只读)
  • TransactionStatus 事务具体的运行状态

PlatformTransactionManager

Spring并不直接管理事务,而是提供了多种事务管理器 ,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 Spring事务管理器的接口是: org.springframework.transaction.PlatformTransactionManager ,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

PlatformTransactionManager

事务 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 使用Spring Jdbc或者MyBatis进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager 使用Hibernate5.0版本进行持久化数据使用
org.springframework.orm.jpa.JpaTransactionManager 使用Jpa持久化使用
org.springframework.kafka.transaction.KafkaTransactionManager 使用Kafka事务时使用

TransactionDefinition

事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition类 ,这个类就定义了一些基本的事务属性。

TransactionDefinition

隔离级别

在不考虑隔离性的情况下,会引发如下问题

  • 脏度

一个事务读取了另一个事务改写但为提交的数据,如果这些数据被回滚,则独到的数据时无效的

  • 不可重复度

在同一事务中,多次读取同一数据返回的结果不一致

  • 幻读

一个事务读取了几行记录后,另一个事务插入一些记录,幻读就发生了

在后来的查询中,第一个事务就会发现有些原来没有的记录

隔离级别 导致的问题
ISOLATION_DEFAULT 使用数据库默认的隔离级别(spring默认)
ISOLATION_READ_UNCOMMITTED 允许读取还未提交的改变了的数据,可能导致脏、幻、不可重复读
ISOLATION_READ_COMMITTED 允许在并发事务已经提交后读取。可防止脏读,但幻、不可重复读任可发生
ISOLATION_REPEATABLE_READ 对相同字段的多次读取是一致的,除非数据被事务本身改变。可防止脏、不可重复读,但幻读仍可能发生
ISOLATION_SERIALIZABLE 完全服从ACID的隔离级别,事务只能一个一个执行,避免了脏读、不可重复读、幻读。执行效率慢,使用时慎重

注:MySQL使用的是REPEATABLE_READ;Oracle使用的是READ_COMMITTED

传播行为

解决业务层方法之间的相互调用的问题,事务传递方式。

常量名称 常量解释
PROPAGATION_REQUIRED 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是 Spring 默认的事务的传播。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 如果事务存在,挂起当前事务,创建一个新的事务
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。

TransactionStatus

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

PlatformTransactionManager.getTransaction(…)方法返回一个 TransactionStatus 对象。返回的TransactionStatus 对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事务)。

TransactionStatus-diagram

TransactionStatus

TransactionExecution

Spring中的事务管理

编程式的事务管理

使用TransactionTemplate手动管理事务,需要修改代码,侵入性相对较大,一般不使用

声明式的事务管理

TransactionProxyFactoryBean

需要为每个进行事务管理的类,配置一个TransactionProxyFactoryBean进行增强处理

基于AspectJ的AOP方式(xml)

早期纯粹的spring mvc配置形式,一旦配置好以后不需要修改任何代码

基于@Transactional注解方式

现在spring boot常用方式,只需要在需要事务处理的类或者方法添加@Transactional。下面使用注解方式简单做一个转账的事务模拟。

使用转账模拟事务管理

使用转账模拟一下事务处理,这里演示两种操作数据库的方式,用于对比查看

项目结构

➜  example-transaction git:(master) ✗ tree
.
├── example-transaction.iml
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── iogogogo
    │   │           └── transfer
    │   │               ├── TransferApplication.java
    │   │               ├── entity
    │   │               │   └── TransferAccount.java
    │   │               ├── jpa
    │   │               │   └── TransferRepository.java
    │   │               ├── mapper
    │   │               │   └── TransferMapper.java
    │   │               └── service
    │   │                   ├── TransferService.java
    │   │                   └── impl
    │   │                       └── TransferServiceImpl.java
    │   └── resources
    │       ├── application.yml
    │       └── transfer.sql
    └── test
        └── java
            └── com
                └── iogogogo
                    └── transfer
                        └── TransferApplicationTests.java

新建数据库表

drop table if exists transfer_account;

create table transfer_account
(
    id    int primary key auto_increment,
    name  varchar(255) unique not null,
    money float default null
);

insert into transfer_account (id, name, money)
values (1, 'jack.zhang', 1000);
insert into transfer_account (id, name, money)
values (2, 'kevin.yu', 1000);

select *
from transfer_account;

pom配置

<dependencies>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1.tmp</version>
  </dependency>
</dependencies>

application.yml

server:
  port: 8084
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/transfer?characterEncoding=utf8&useSSL=false&allowMultiQueries=true
    username: root
    password: MySQL@123
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari: # http://blog.didispace.com/Springboot-2-0-HikariCP-default-reason/
      minimum-idle: 5
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 30000
      pool-name: TransferHikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
  jpa:
    database-platform: org.hibernate.dialect.MySQL57Dialect
    open-in-view: false
    show-sql: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        use_sql_comments: true
        format_sql: true
logging:
  level:
    com.iogogogo.transfer.mapper: debug

Jpa操作数据库

package com.iogogogo.transfer.jpa;

import com.iogogogo.transfer.entity.TransferAccount;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * Created by tao.zeng on 2020/2/16.
 */
@Repository
public interface TransferRepository extends JpaRepository<TransferAccount, Long> {

    /**
     * @param in    转入人
     * @param money 金额
     */
    @Modifying
    @Query(value = "update transfer_account set money = money + ?2 where name = ?1", nativeQuery = true)
    void inMoney(String in, float money);

    /**
     * @param out   转出人
     * @param money 金额
     */
    @Modifying
    @Query(value = "update transfer_account set money = money - :money where name = :name", nativeQuery = true)
    void outMoney(@Param("name") String out, @Param("money") float money);
}

业务层模拟一个转账操作

package com.iogogogo.transfer.service.impl;

import com.iogogogo.transfer.jpa.TransferRepository;
import com.iogogogo.transfer.mapper.TransferMapper;
import com.iogogogo.transfer.service.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Created by tao.zeng on 2020/2/16.
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Autowired
    private TransferRepository transferRepository;

    @Autowired
    private TransferMapper transferMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public void transfer(String in, String out, float money) {
        // 这里使用的是jpa操作数据库
        transferRepository.outMoney(out, money);
        transferRepository.inMoney(in, money);
    }
}

新建测试类

package com.iogogogo.transfer;

import com.iogogogo.transfer.service.TransferService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Created by tao.zeng on 2020/2/16.
 */
@Slf4j
@Ignore
@org.junit.runner.RunWith(SpringRunner.class)
@org.springframework.boot.test.context.SpringBootTest(classes = TransferApplication.class)
public class TransferApplicationTests {

    @Autowired
    private TransferService transferService;

    @Test
    public void test1() {
        transferService.transfer("jack.zhang", "kevin.yu", 200);
    }
}

但我们运行时,会抛出如下异常:


org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query

    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:403)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:257)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:153)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:149)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy99.outMoney(Unknown Source)
    at com.iogogogo.transfer.service.impl.TransferServiceImpl.transfer(TransferServiceImpl.java:28)
    at com.iogogogo.transfer.TransferApplicationTests.test1(TransferApplicationTests.java:24)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: javax.persistence.TransactionRequiredException: Executing an update/delete query
    at org.hibernate.internal.AbstractSharedSessionContract.checkTransactionNeededForUpdateOperation(AbstractSharedSessionContract.java:409)
    at org.hibernate.query.internal.AbstractProducedQuery.executeUpdate(AbstractProducedQuery.java:1601)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:409)
    at com.sun.proxy.$Proxy115.executeUpdate(Unknown Source)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution$ModifyingExecution.doExecute(JpaQueryExecution.java:238)
    at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:88)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:154)
    at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:142)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:618)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:605)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:366)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:99)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
    ... 39 more


那是因为使用jpa的时候,涉及到@Modifying就表示是增删改操作,那么就必须添加@Transactional,当然如果是单纯的查询就不需要了(spring考虑的挺全面)。当加上@Transactional就可以正常运行了。

我们可以在代码上手动给他添加一个异常,再来看结果。这里我们加了一个int i = 1 / 0;会产生一个除以0的异常,因为有@Transactional的存在,整个执行是不能成功的。

@Override
@Transactional
public void transfer(String in, String out, float money) {
  // 这里使用的是jpa操作数据库
  transferRepository.outMoney(out, money);
  int i = 1 / 0;
  transferRepository.inMoney(in, money);
}

MyBatis-Plus操作数据库

package com.iogogogo.transfer.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.iogogogo.transfer.entity.TransferAccount;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;

/**
 * Created by tao.zeng on 2020/2/16.
 */
@Repository
public interface TransferMapper extends BaseMapper<TransferAccount> {

    /**
     * @param in    转入人
     * @param money 金额
     */
    @Update("update transfer_account set money = money + #{money} where name = #{name}")
    void inMoney(@Param("name") String in, @Param("money") float money);

    /**
     * @param out   转出人
     * @param money 金额
     */
    @Update("update transfer_account set money = money - #{money} where name = #{name}")
    void outMoney(@Param("name") String out, @Param("money") float money);
}

业务层模拟一个转账操作

package com.iogogogo.transfer.service.impl;

import com.iogogogo.transfer.jpa.TransferRepository;
import com.iogogogo.transfer.mapper.TransferMapper;
import com.iogogogo.transfer.service.TransferService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * Created by tao.zeng on 2020/2/16.
 */
@Service
public class TransferServiceImpl implements TransferService {

    @Autowired
    private TransferRepository transferRepository;

    @Autowired
    private TransferMapper transferMapper;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public void transfer(String in, String out, float money) {
        // 这里使用的是MyBatis-Plus操作数据库
        transferMapper.outMoney(out, money);
          // int i = 1 / 0;
        transferMapper.inMoney(in, money);
    }
}

在没有异常情况下,就算不加@Transactional是可以直接操作成功的,但是如果 int i = 1 / 0;存在,outMoney方法还是可以正常执行的,这样就会造成钱被扣了但是对方却没有收到的情况,所以这时候就需要添加@Transactional让他进行事务处理,这里mybatis就没有jpa处理的好了。

@Transactional 介绍

参数 描述
readOnly 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)
rollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
rollbackForClassName 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称:@Transactional(rollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(rollbackForClassName={“RuntimeException”,”Exception”})
noRollbackFor 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName=”RuntimeException”)指定多个异常类名称:@Transactional(noRollbackForClassName={“RuntimeException”,”Exception”})
propagation 该属性用于设置事务的传播行为,具体取值可参考表6-7。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
isolation 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
timeout 该属性用于设置事务的超时秒数,默认值为-1表示永不超时

以上参考:spring事务注解

总结

以上介绍了一下spring中的事务接口和事务的传播行为以及隔离级别,也通过转账的例子演示了有事务和没有事务的区别,再有了spring boot以后,基本都是使用注解的形式进行事务处理。

源码地址:https://github.com/iogogogo/life-example/tree/master/example-transaction


文章作者: 阿牛
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿牛 !
评论
  目录