小径分叉的花园

Khotyn 的网志,记录生活,记录想法

14 Sep 2013

Spring 事务的传播特性

最近工作中涉及到了一个分布式事务的产品,这个产品是在 Spring 的事务上做的,我对其中涉及到的 Spring 的事务的传播特性不是很了解,所以今天花了一个下午的时间认真了解了一下,写了一堆的测试代码。

进入正题,Spring 的事务的传播特性分为以下的七种:

  • PROPAGATION_REQUIRED
  • PROPAGATION_SUPPORTS
  • PROPAGATION_MANDATORY
  • PROPAGATION_REQUIRES_NEW
  • PROPAGATION_NOT_SUPPORTED
  • PROPAGATION_NEVER
  • PROPAGATION_NESTED

下面一种种来解释:

PROPAGATION_REQUIRED

默认的事务传播特性,通常情况下我们用这个事务传播特性就可以了。如果当前事务上下文中没有事务,那么就会新创建一个事务。如果当前的事务上下文中已经有一个事务了,那么新开启的事务会开启一个新的逻辑事务范围,但是会和原有的事务共用一个物理事务,我们暂且不关心逻辑事务和物理事务的区别,先看看这样会导致怎样的代码行为:

@Override
public void txRollbackInnerTxRollbackPropagationRequires() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务设置了 setRollbackOnly,
                    status.setRollbackOnly();
                }
            });
        }
    });
}

这段代码在一个嵌套的事务内部(内外层的事务都是 PROPAGATION_REQUIRED 的)设置了回滚,那么对于外部的事务来说,它会收到一个 UnexpectedRollbackException,因为内部的事务和外部的事务是共用一个物理事务的,所以显然内部的事务必然会导致外部事务的回滚,但是因为这个回滚并不是外部事务自己设置的,所以外层事务回滚的时候会需要抛出一个 UnexpectedRollbackException,让事务的调用方知道这个回滚并不是外部事务自己想要回滚,是始料未及的。

但是,如果内层的事务不是通过设置 setRollbackOnly() 来回滚,而是抛出了 RuntimeException 来回滚,那么外层的事务接收到了内层抛出的 RuntimeException 也会跟着回滚,这个是可以预料到的行为,所以不会有 UnexpectedRollbackException

PROPAGATION_SUPPORTS

PROPAGATION_SUPPORTS 的特性是如果事务上下文中已经存在一个事务,那么新的事务(传播特性为 PROPAGATION_SUPPORTS)就会和原来的事务共用一个物理事务,其行为和 PROPAGATION_REQUIRED 一样。但是,如果当前事务上下文中没有事务,那么 PROPAGATION_SUPPORTS 就按无事务的方式执行代码:

@Override
public void txRollbackInnerTxRollbackPropagationSupports() {
    supportsTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus status) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            throw new CustomRuntimeException();
        }
    });
}

看上面这段代码,虽然我们在事务(PROPAGATION_SUPPORTS 的)中抛出了一个 RuntimeException,但是因为其事务上下文中没有事务存在,所以这段代码实际上是以无事务的方式执行的,因此代码中的 jdbcTemplate.update() 操作也不会被回滚。

PROPAGATION_MANDATORY

PROPAGATION_MANDATORY 要求事务上下文中必须存在事务,如果事务上下文中存在事务,那么其行为和 PROPAGATION_REQUIRED 一样。如果当前事务上下文中没有事务,那么就会抛出 IllegalTransactionStateException,比如下面这段代码就会这样:

@Override
public void txRollbackInnerTxRollbackPropagationMandatory() {
    mandatoryTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
        }
    });
}

PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRES_NEW 无论当前事务上下文中有没有事务,都会开启一个新的事务,并且和原来的事务完全是隔离的,外层事务的回滚不会影响到内层的事务,内层事务的回滚也不会影响到外层的事务(这个说法得稍微打点折扣:因为如果内层抛出 RuntimeException 的话,那么外层还是会收到这个异常并且触发回滚),我们分析下几段代码:

@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {

            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });

            // 外部事务发生回滚,内部事务应该不受影响还是能够提交
            throw new RuntimeException();
        }
    });
}

这段代码外层的事务回滚了,但是不会影响到内层的事务的提交,内层事务不受外层的事务的影响。再看:

@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew2() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            // Nested transaction committed.
            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务发生回滚,但是外部事务不应该发生回滚
                    status.setRollbackOnly();
                }
            });
        }
    });
}

这段代码在内层事务上设置了 setRollbackOnly,内层事务肯定会回滚,但是由于内层事务和外层事务是隔离的,所以外层事务不会被回滚。再看:

@Override
public void txRollbackInnerTxRollbackPropagationRequiresNew3() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");

            requiresNewTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务抛出 RuntimeException,外部事务接收到异常,依旧会发生回滚
                    throw new RuntimeException();
                }
            });
        }
    });
}

这段代码在内层事务抛出了一个 RuntimeException,虽然内层事务和外层事务在事务上是隔离,但是 RuntimeException 显然还会抛到外层去,所以外层事务也会发生回滚。

PROPAGATION_NOT_SUPPORTED

PROPAGATION_NOT_SUPPORTED 不管当前事务上下文中有没有事务,代码都会在按照无事务的方式执行,看下面这段代码:

@Override
public void txRollbackInnerTxRollbackPropagationNotSupport() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            notSupportedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });
            // 外部事务回滚,不会把内部的也连着回滚 
            transactionStatus.setRollbackOnly();
        }
    });
}

上面这段代码中虽然外部事务发生了回滚,但是由于内部的事务是 PROPAGATION_NOT_SUPPORTED,根本不在外层的事务范围内,所以内层事务不会发生回滚。

PROPAGATION_NEVER

PROPAGATION_NEVER 如果当前事务上下文中存在事务,就会抛出 IllegalTransactionStateException 异常,自己也会按照非事务的方式执行

@Override
public void txRollbackInnerTxRollbackPropagationNever2() {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");
            neverTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                }
            });
        }
    });
}

比如上面这段代码,在一个 PROPAGATION_REQUIRES 里面嵌入了一个 PROPAGATION_NEVER,内层就会抛出一个 IllegalTransactionStateException,导致外层事务被回滚。

PROPAGATION_NESTED

PROPAGATION_NESTED 只能应用于像 DataSource 这样的事务,可以通过在一个事务内部开启一个 PROPAGATION_NESTED 而达到一个事务内部有多个保存点的效果,一个内嵌的事务发生回滚,只会回滚到它自己的保存点,外层事务还会继续,比如下面这段代码:

@Override
public void txRollbackInnerTxRollbackPropagationNested() {
    nestedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            jdbcTemplate.update("insert into user (name, password) values (?, ?)", "Huang",
                "1111112");

            nestedTransactionTemplate.execute(new TransactionCallbackWithoutResult() {
                @Override
                protected void doInTransactionWithoutResult(TransactionStatus status) {
                    jdbcTemplate.update("insert into user (name, password) values (?, ?)",
                        "Huang", "1111112");
                    // 内部事务设置了 rollbackOnly,外部事务应该不受影响,可以继续提交
                    status.setRollbackOnly();
                }
            });
        }
    });
}

内层的事务发生了回滚,只会回滚其内部的操作,不会影响到外层的事务。

总结

Spring 的事务传播特性种类繁多,大多数都来自于 EJB 的事务,大家可以自己写一些小的程序来测试 Spring 各个事务特性的行为,加深印象,我自己也写了一个工程,通过单元测试去测试各个事务传播特性的行为,大家有兴趣的话,可以下过来跑一下:https://github.com/khotyn/spring-tx-test

Categories