Spring @Transactional - 隔离级别、传播行为

548

有人可以通过现实世界的例子解释一下@Transactional注解中隔离级别传播行为参数是什么吗?

基本上,我应该在何时以及为什么要选择更改它们的默认值。

10个回答

541

好问题,虽然不容易回答。

传播性

定义事务之间的关系。常见选项:

  • REQUIRED:代码总是在一个事务中运行。如果可用,创建一个新的事务或重用一个事务。
  • REQUIRES_NEW:代码总是在一个新的事务中运行。如果存在一个事务,则挂起当前事务。

@Transactional 的默认值是 REQUIRED,这通常是你想要的。

隔离级别

定义事务之间的数据契约。

  • ISOLATION_READ_UNCOMMITTED: 允许脏读。
  • ISOLATION_READ_COMMITTED: 不允许脏读。
  • ISOLATION_REPEATABLE_READ: 如果在同一事务中读取两次某行,结果将始终相同。
  • ISOLATION_SERIALIZABLE: 按顺序执行所有事务。

在多线程应用程序中,不同级别具有不同的性能特征。如果您了解“脏读”概念,您将能够选择一个好的选项。

默认值可能因数据库而异。例如,对于MariaDB,默认值为REPEATABLE READ


当发生脏读的示例:
  thread 1   thread 2      
      |         |
    write(x)    |
      |         |
      |        read(x)
      |         |
    rollback    |
      v         v 
           value (x) is now dirty (incorrect)

所以一个理智的默认设置(如果可以这样说)可能是ISOLATION_READ_COMMITTED,它只允许您读取已被其他正在运行的事务提交的值,并与传播级别REQUIRED相结合。如果您的应用有其他需求,那么您可以从这里开始工作。
在输入provideService例程时,总是会创建一个新的事务,并在离开时完成。
public class FooService {
    private Repository repo1;
    private Repository repo2;

    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void provideService() {
        repo1.retrieveFoo();
        repo2.retrieveFoo();
    }
}

如果我们使用了REQUIRED,当进入例程时,如果事务已经打开,那么该事务将保持打开状态。 还要注意的是,rollback的结果可能会有所不同,因为多个执行可能参与同一个事务。
我们可以通过测试轻松验证行为,并查看在传播水平不同的情况下结果如何有所不同。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:/fooService.xml")
public class FooServiceTests {

    private @Autowired TransactionManager transactionManager;
    private @Autowired FooService fooService;

    @Test
    public void testProvideService() {
        TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
        fooService.provideService();
        transactionManager.rollback(status);
        // assert repository values are unchanged ... 
}

传播级别为

  • REQUIRES_NEW: 我们期望 fooService.provideService() 不会被回滚,因为它创建了自己的子事务。

  • REQUIRED: 我们期望所有内容都被回滚,且后端存储不会发生变化。


那个最后的链接与你所说的有什么关系?根据链接文档,似乎是会话(session)指定了当前事务,而不是会话工厂(session factory)。 - Donal Fellows
@Donal,哦,抱歉没有表达清楚。我的意思是,自从sessionFactory.getCurrentTransaction()被添加后,就不再需要运行HibernateTemplate来管理事务了。我已经将其删除 :) - Johan Sjöberg
我的问题只是关于链接指向哪里的,真的。 :-) - Donal Fellows
如何获取当前事务中所做的更改 - http://stackoverflow.com/questions/36132667/get-the-data-which-is-inserted-using-transactional-in-the-same-transaction - Prasanna Kumar H A

390

PROPAGATION_REQUIRED = 0; 如果对于方法M1已经启动了DataSourceTransactionObject T1。如果另一个方法M2需要事务对象,则不会创建新的事务对象。同一对象T1用于M2。

PROPAGATION_MANDATORY = 2; 方法必须在事务内运行。如果没有正在进行的现有事务,则会抛出异常。

PROPAGATION_REQUIRES_NEW = 3; 如果已经为方法M1启动了DataSourceTransactionObject T1,并且它正在进行中(执行方法M1)。如果另一个方法M2开始执行,则T1将暂停以持续方法M2,并为M2创建新的DataSourceTransactionObject T2。M2在其自己的事务上下文中运行。

PROPAGATION_NOT_SUPPORTED = 4; 如果对于方法M1已经启动了DataSourceTransactionObject T1。如果同时运行另一个方法M2。则M2不应在事务上下文中运行。T1被暂停直到M2完成。

PROPAGATION_NEVER = 5; 没有任何方法在事务上下文中运行。


隔离级别: 它关乎在并发事务活动的影响下,一个事务可能会受到多个其他并发事务的影响。它支持跨多个表的数据保持一致性。它涉及在数据库中锁定行和/或表。

多个事务的问题

情景1。如果T1事务从由另一个并发事务T2写入的A1表中读取数据。如果在T2上进行回滚,则T1获取的数据是无效的。例如,a=2是原始数据。如果T1读取了由T2写入的a=1。如果T2回滚,那么a=1将在数据库中回滚到a=2。但是现在,T1具有a=1,但在数据库表中已更改为a=2。

情景2。如果T1事务从A1表中读取数据。如果另一个并发事务(T2)更新A1表上的数据。那么T1读取的数据与A1表中的数据不同。因为T2已经更新了A1表上的数据。例如,如果T1读取a=1并且T2更新a=2。然后a!= b。

情景3。如果T1事务从A1表中读取带有特定行数的数据。如果另一个并发事务(T2)在A1表上插入更多行。则T1读取的行数与A1表中的行数不同。

情景1称为脏读

情景2称为不可重复读

情景3称为幻影读

因此,隔离级别是防止情景1、情景2、情景3发生的程度。 您可以通过实施锁定来获得完全的隔离级别。这样可以防止并发读取和写入相同数据。但它会影响性能。隔离级别取决于应用程序需要多少隔离。

ISOLATION_READ_UNCOMMITTED:允许读取尚未提交的更改。它受到情景1、情景2、情景3的影响。

ISOLATION_READ_COMMITTED:允许从已提交的并发事务中读取。它可能会受

public class TransactionBehaviour {
   // set is either using xml Or annotation
    DataSourceTransactionManager manager=new DataSourceTransactionManager();
    SimpleTransactionStatus status=new SimpleTransactionStatus();
   ;
  
    
    public void beginTransaction()
    {
        DefaultTransactionDefinition Def = new DefaultTransactionDefinition();
        // overwrite default PROPAGATION_REQUIRED and ISOLATION_DEFAULT
        // set is either using xml Or annotation
        manager.setPropagationBehavior(XX);
        manager.setIsolationLevelName(XX);
       
        status = manager.getTransaction(Def);
    
    }

    public void commitTransaction()
    {
       
      
            if(status.isCompleted()){
                manager.commit(status);
        } 
    }

    public void rollbackTransaction()
    {
       
            if(!status.isCompleted()){
                manager.rollback(status);
        }
    }
    Main method{
        beginTransaction()
        M1();
        If error(){
            rollbackTransaction()
        }
         commitTransaction();
    }
   
}

您可以使用不同的隔离级别和传播行为进行调试并查看结果。


如何获取当前事务中所做的更改 - http://stackoverflow.com/questions/36132667/get-the-data-which-is-inserted-using-transactional-in-the-same-transaction - Prasanna Kumar H A
4
“隔离级别”和“传播行为”之间有什么互动关系?如果方法1使用隔离级别(例如READ_COMMITTED)开始一个事务,然后稍后调用隔离级别为REPEATABLE_READ的方法2,那么无论方法2指定了什么传播行为(例如仅REQUIRED),它肯定必须在自己的新事务中执行。 - Cornel Masson
这真的很晚了,但是当PROPAGATION_REQUIRES_NEW时,如果另一个新调用发生在M1中(比如M1.1),那么T1(由M1使用)会发生什么? - Tim Z.
@CornelMasson 我认为我有一个非常类似于你提出的问题。我为此创建了一个特定的SO问题 - payne
并发事务还存在另一个问题:我称之为“场景0:丢失更新”,而“读未提交”可以避免这种情况。 - Mahozad

136

其他答案已经详细解释了每个参数;然而,你要求一个现实世界的例子,这里有一个可以阐明不同传播选项目的的例子:

假设你负责实现一个注册服务,在该服务中向用户发送确认电子邮件。你想出了两个服务对象,一个用于注册用户,另一个用于发送电子邮件,后者在前者内部调用。例如像这样:

/* Sign Up service */
@Service
@Transactional(Propagation=REQUIRED)
class SignUpService{
 ...
 void SignUp(User user){
    ...
    emailService.sendMail(User);
 }
}

/* E-Mail Service */
@Service
@Transactional(Propagation=REQUIRES_NEW)
class EmailService{
 ...
 void sendMail(User user){
  try{
     ... // Trying to send the e-mail
  }catch( Exception)
 }
}

您可能已经注意到第二项服务属于传播类型 REQUIRES_NEW ,而且很有可能会抛出异常(SMTP服务器关闭,无效的电子邮件或其他原因)。 您可能不希望整个过程回滚,例如从数据库中删除用户信息或其他事情; 因此,您将在单独的事务中调用第二项服务。

回到我们的例子,这次您关心数据库安全性,因此按以下方式定义DAO类:

/* User DAO */
@Transactional(Propagation=MANDATORY)
class UserDAO{
 // some CRUD methods
}

这意味着每当创建一个DAO对象,也就是潜在访问数据库时,我们需要确保该调用是从我们的一个服务内部发出的,这意味着必须存在一个活动事务;否则,将会抛出异常。因此传播类型为MANDATORY


34
REQUIRES_NEW的完美例子。 - Ravi K Thapliyal
6
好的解释!顺便问一下传播的默认值是什么?如果你能像这样为隔离提供一个例子,那就更好了。非常感谢。 - Prakash K
5
@PrakashK 默认值是REQUIRED。(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Propagation.html) - ihebiheb

67

隔离级别定义了一个事务对数据仓库所做的更改如何影响其他同时并发的事务,以及更改后的数据何时以及如何对其他事务可用。当我们使用Spring框架定义一个事务时,我们也能够配置该事务在哪个隔离级别下执行。

@Transactional(isolation=Isolation.READ_COMMITTED)
public void someTransactionalMethod(Object obj) {

}

READ_UNCOMMITTED隔离级别表示一个事务可以读取其他事务未提交的数据。

READ_COMMITTED隔离级别表示一个事务不能读取其他事务未提交的数据。

REPEATABLE_READ隔离级别表示如果一个事务多次从数据库中读取同一条记录,那么每次读取的结果必须相同。

SERIALIZABLE隔离级别是所有隔离级别中最严格的。事务在所有级别(读取级别、范围级别和写入级别)上都被执行锁定,因此它们似乎是按照序列化方式执行的。

传播性是指决定业务方法应如何封装在逻辑或物理事务中的能力。

Spring REQUIRED行为意味着如果当前bean方法执行上下文中已经存在一个打开的事务,则将使用相同的事务。

REQUIRES_NEW行为意味着容器始终会创建一个新的物理事务。

NESTED行为使嵌套的Spring事务使用相同的物理事务,但在嵌套调用之间设置保存点,这样内部事务也可以独立于外部事务回滚。

MANDATORY行为表示必须已经存在一个打开的事务。如果不存在,则容器将抛出异常。

NEVER行为表示必须不存在已经打开的事务。如果存在一个事务,容器将抛出异常。

NOT_SUPPORTED行为将在任何事务范围之外执行。如果已经存在打开的事务,则会暂停它。

SUPPORTS行为将在已经存在打开的事务范围内执行。如果没有已经打开的事务,该方法仍将以非事务方式执行。


4
如果您能说明何时使用哪一个,将更加有益。 - Kumar Manish
给一些例子,对于初学者来说会非常有帮助。 - nitinsridar
请澄清这个疑问,隔离级别只涉及数据库操作还是服务层中发生的所有操作?如果它与服务层中的所有操作相关联,那么read_uncommitted是什么意思? - codemania23

43
一个事务表示与数据库的一次工作单元。 事务传播:Spring允许您指定事务在方法调用和嵌套事务中的传播方式。 事务隔离:Spring允许您指定事务的隔离级别,以确保它们与其他并发事务隔离。
在Spring中,TransactionDefinition接口定义了符合Spring规范的事务属性。@Transactional注解描述了方法或类上的事务属性。
@Autowired
private TestDAO testDAO;

@Transactional(propagation=TransactionDefinition.PROPAGATION_REQUIRED,isolation=TransactionDefinition.ISOLATION_READ_UNCOMMITTED)
public void someTransactionalMethod(User user) {

  // Interact with testDAO

}

传播(繁殖):用于交易之间的关系。 (类似于Java线程间通信)
+-------+---------------------------+------------------------------------------------------------------------------------------------------+
| value |        Propagation        |                                             Description                                              |
+-------+---------------------------+------------------------------------------------------------------------------------------------------+
|    -1 | TIMEOUT_DEFAULT           | Use the default timeout of the underlying transaction system, or none if timeouts are not supported. |
|     0 | PROPAGATION_REQUIRED      | Support a current transaction; create a new one if none exists.                                      |
|     1 | PROPAGATION_SUPPORTS      | Support a current transaction; execute non-transactionally if none exists.                           |
|     2 | PROPAGATION_MANDATORY     | Support a current transaction; throw an exception if no current transaction exists.                  |
|     3 | PROPAGATION_REQUIRES_NEW  | Create a new transaction, suspending the current transaction if one exists.                          |
|     4 | PROPAGATION_NOT_SUPPORTED | Do not support a current transaction; rather always execute non-transactionally.                     |
|     5 | PROPAGATION_NEVER         | Do not support a current transaction; throw an exception if a current transaction exists.            |
|     6 | PROPAGATION_NESTED        | Execute within a nested transaction if a current transaction exists.                                 |
+-------+---------------------------+------------------------------------------------------------------------------------------------------+

隔离性:隔离性是数据库事务的ACID(原子性、一致性、隔离性、持久性)属性之一。隔离性决定了事务完整性对其他用户和系统的可见性。它用于资源锁定,即并发控制,确保在给定时间点只有一个事务可以访问该资源。
锁定感知:隔离级别决定了锁定的持续时间。
+---------------------------+-------------------+-------------+-------------+------------------------+
| Isolation Level Mode      |  Read             |   Insert    |   Update    |       Lock Scope       |
+---------------------------+-------------------+-------------+-------------+------------------------+
| READ_UNCOMMITTED (Low)    |  uncommitted data | Allowed     | Allowed     | No Lock                |
| READ_COMMITTED (Default)  |   committed data  | Allowed     | Allowed     | Lock on Committed data |
| REPEATABLE_READ           |   committed data  | Allowed     | Not Allowed | Lock on block of table |
| SERIALIZABLE (High)       |   committed data  | Not Allowed | Not Allowed | Lock on full table     |
+---------------------------+-------------------+-------------+-------------+------------------------+

阅读感知:以下出现了3种主要问题:
- 脏读:从另一个事务中读取未提交的数据。 - 不可重复读:从另一个事务中读取已提交的更新。 - 幻读:从另一个事务中读取已提交的插入和/或删除。
下面是一个图表,显示了哪个事务隔离级别解决了并发问题的哪些问题:
+---------------------------+--------------+----------------------+----------------+
| Isolation Level Mode      |  Dirty reads | Non-repeatable reads | Phantoms reads |
+---------------------------+--------------+----------------------+----------------+
| READ_UNCOMMITTED          | X            | X                    | X           |
| READ_COMMITTED (Default)  | solves       | X                    | X           |
| REPEATABLE_READ           | solves       | solves               | X           |
| SERIALIZABLE              | solves       | solves               | solves      |
+---------------------------+--------------+----------------------+----------------+

举例



25

在大多数情况下,你不应该使用 Read Uncommited,因为它并不真正符合 ACID 标准。作为默认的起点,Read Commmited 是一个不错的选择。只有在报告、数据滚动或聚合等场景下,才需要使用 Repeatable Read。请注意,许多数据库(包括 postgres)实际上并不支持 Repeatable Read,你必须使用 Serializable。与 Java 中的 synchronized 类似,Serializable 对于那些你知道完全独立于其他任何事情都必须发生的事情非常有用。Serializable 与 REQUIRES_NEW 传播方式相辅相成。

我对所有运行 UPDATE 或 DELETE 查询以及 "service" 层函数使用 REQUIRES。对于只运行 SELECT 的 DAO 层函数,我使用 SUPPORTS,如果已经启动了事务(即从服务函数调用),则会参与到该事务中。


15

事务隔离和事务传播虽然相关,但它们是两个截然不同的概念。在这两种情况下,默认值都在客户端边界组件上通过使用 声明式事务管理 或者 编程式事务管理 进行自定义。每个隔离级别和传播属性的详细信息可以在下面的参考链接中找到。

事务隔离

对于运行中的两个或多个事务/连接到数据库的情况,一个事务中查询所做的更改何时会影响到另一个事务中的查询。它也涉及对用于将此事务中的更改与其他事务隔离开来的数据库记录锁定的类型。通常由参与事务的数据库/资源实现。

.

事务传播

在企业应用程序中,针对任何给定的请求/处理,都会涉及许多组件来完成工作。其中一些组件标记了事务的边界(开始/结束)将在各自的组件及其子组件中使用。对于这些组件的事务边界,事务传播指定了相应组件是否参与事务以及如果调用组件已经创建/启动了事务,则会发生什么,或者没有事务。这与Java EE事务属性相同。通常由客户端事务/连接管理器实现。

参考:


1
很好,所有信息都在一个地方,链接非常有帮助,谢谢@Gladwin Burboz。 - nitinsridar

10

我已经运行了outerMethodmethod_1method_2并使用不同的传播模式。

以下是不同传播模式的输出。

外部方法

    @Transactional
    @Override
    public void outerMethod() {
        customerProfileDAO.method_1();
        iWorkflowDetailDao.method_2();
    }

方法一

    @Transactional(propagation=Propagation.MANDATORY)
    public void method_1() {
        Session session = null;
        try {
            session = getSession();
            Temp entity = new Temp(0l, "XXX");
            session.save(entity);
            System.out.println("Method - 1 Id "+entity.getId());
        } finally {
            if (session != null && session.isOpen()) {

            }
        }
    }

方法二

    @Transactional()
    @Override
    public void method_2() {
        Session session = null;
        try {
            session = getSession();
            Temp entity = new Temp(0l, "CCC");
            session.save(entity);
            int i = 1/0;
            System.out.println("Method - 2 Id "+entity.getId());
        } finally {
            if (session != null && session.isOpen()) {

            }
        }
    }
    • OuterMethod - 没有事务
    • Method_1 - Propagation.MANDATORY
    • Method_2 - 仅使用事务注释
    • 输出:方法1将抛出异常,指示不存在事务
    • OuterMethod - 没有事务
    • Method_1 - 仅使用事务注释
    • Method_2 - Propagation.MANDATORY
    • 输出:方法2将抛出异常,指示不存在事务
    • 输出:方法1将在数据库中持久化记录。
    • OuterMethod - 使用事务
    • Method_1 - 仅使用事务注释
    • Method_2 - Propagation.MANDATORY
    • 输出:方法2将在数据库中持久化记录。
    • 输出:方法1将在数据库中持久化记录。 -- 这里主要外部现有事务用于方法1和方法2
    • OuterMethod - 使用事务
    • Method_1 - Propagation.MANDATORY
    • Method_2 - 仅使用事务注释并引发异常
    • 输出:数据库中不会保存任何记录,表明已执行回滚。
    • OuterMethod - 使用事务
    • Method_1 - Propagation.REQUIRES_NEW
    • Method_2 - Propagation.REQUIRES_NEW并引发1/0异常
    • 输出:方法2将引发异常,因此不会持久化方法2的记录。
    • 输出:方法1将在数据库中持久化记录。
    • 输出:方法1没有回滚。

2
我们可以为此添加:
@Transactional(readOnly = true)
public class Banking_CustomerService implements CustomerService {

    public Customer getDetail(String customername) {
        // do something
    }

    // these settings have precedence for this method
    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateCustomer(Customer customer) {
        // do something
    }
}

0
你可以像这样使用:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public EventMessage<ModificaOperativitaRapporto> activate(EventMessage<ModificaOperativitaRapporto> eventMessage) {
//here some transaction related code
}

你也可以使用这个东西:

public interface TransactionStatus extends SavepointManager {
    boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    void flush();
    boolean isCompleted();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接