Spring Bootでトランザクション(@Transactional)の伝搬属性(propagation)を試す

Spring Bootでトランザクション(@Transactional)の伝搬属性(propagation)について確認したメモです。


Spring Bootでトランザクションの伝搬属性について試してみました。
伝搬属性は種類が多いのと、すでにトランザクションが存在している場合と存在していない場合で挙動が異なるので
かなりややこしいです。

伝搬属性

伝搬属性はトランザクションの伝搬レベルを設定する属性です。
すでにトランザクションが存在している場合にどのように伝搬するのかを設定します。
Springのトランザクションでは下記の伝搬レベルが定義されています。

  • REQUIRED
  • REQUIRES_NEW
  • SUPPORTS
  • NOT_SUPPORTED
  • MANDATORY
  • NESTED
  • NEVER

Propagation (Spring Framework 4.3.10.RELEASE API)

Maven

下記のdependencyを追加しました。
DBはH2を使います。JDBCで接続します。

pom.xml

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

JDBCのログを出力するために、log4jdbc-spring-boot-starterを追加しました。
これでデフォルトでspyログが出力されます。

pom.xml

<dependency>
    <groupId>com.integralblue</groupId>
    <artifactId>log4jdbc-spring-boot-starter</artifactId>
    <version>1.0.1</version>
</dependency>

テスト用クラス

DBを更新するメソッドを持ったServiceAとServiceBを作成しました。

ServiceAには各伝搬レベルで更新するメソッドがあります。

ServiceA.java

    // default
    @Transactional(propagation = Propagation.REQUIRED)
    public void required() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void requiresNew() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.SUPPORTS)
    public void supports() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void notSupported() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.MANDATORY)
    public void mandatory() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.NESTED)
    public void nested() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

    @Transactional(propagation = Propagation.NEVER)
    public void never() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Andy'", 21);
    }

ServiceBでは、トランザクションを開始してDBを更新した後、
ServiceAの各伝搬レベルのメソッドを呼んでいます。

ServiceB.java

    // default
    @Transactional(propagation = Propagation.REQUIRED)
    public void required() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.required();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void requiresNew() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.requiresNew();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void supports() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.supports();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void notSupported() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.notSupported();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void mandatory() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.mandatory();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void nested() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.nested();
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void never() {
        jdbc.update("UPDATE person SET age=? WHERE name = 'Bobby'", 20);
        myServiceA.never();
    }

それぞれServiceAとServiceBでどのようにトランザクションが伝搬されるか確認します。

伝搬レベル

REQUIRED (default)

REQUIREDはトランザクションが存在しない場合、新規にトランザクションを開始し、
すでに存在する場合はそのトランザクションを利用します。
REQUIREDがdefaultの伝搬レベルになっています。

f:id:pppurple:20170830230845j:plain:w400

ServiceA直接
ログの下記の間がトランザクションになっています。

4. Connection.setAutoCommit(false) returned
      :
      :
4. Connection.commit() returned

新規にトランザクションが開始されていることが分かります。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@36183389
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ServiceBでトランザクションが新規作成され、
その後ServiceAの処理では新規作成されず既存のトランザクションを利用しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@21270ae7
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 2 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@5bb27803
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 0 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
REQUIRES_NEW

REQUIRES_NEWはトランザクションが存在しない場合、新規にトランザクションを開始し、
すでにトランザクションが存在する場合でも新規にトランザクションを開始します。

f:id:pppurple:20170830231159j:plain:w400

ServiceA直接
新規にトランザクションが開始されていることが分かります。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@3222821c
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ServiceBでトランザクションが新規作成され、
その後ServiceAの処理では新規にトランザクションが作成されているのが分かります。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@11e23f42
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.connection                          : 5. Connection opened
jdbc.audit                               : 5. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 5. Connection.getAutoCommit() returned true
// トランザクション2 開始
jdbc.audit                               : 5. Connection.setAutoCommit(false) returned
jdbc.audit                               : 5. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 5. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@5298d8ca
jdbc.audit                               : 5. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 0 msec}
jdbc.audit                               : 5. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 5. PreparedStatement.close() returned
jdbc.audit                               : 5. Connection.commit() returned
// トランザクション2 終了
jdbc.audit                               : 5. Connection.setAutoCommit(true) returned
jdbc.audit                               : 5. Connection.isReadOnly() returned false
jdbc.connection                          : 5. Connection closed
jdbc.audit                               : 5. Connection.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
SUPPORTS

SUPPORTSはトランザクションが存在しない場合、トランザクションを使用せず、
すでに存在する場合はそのトランザクションを利用します。

f:id:pppurple:20170830231257j:plain:w400

ServiceA直接
下記のようなログがなく、トランザクションが開始されていません。

4. Connection.setAutoCommit(false) returned
      :
      :
4. Connection.commit() returned

トランザクションを使用せずに直接更新しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
// トランザクションなしで更新
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@6d15f646
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ServiceBでトランザクションが新規作成されて、
その後ServiceAの処理では新規作成されず既存のトランザクションを利用しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@4a31b11d
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@57af01c1
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
NOT_SUPPORTED

NOT_SUPPORTEDはトランザクションを使用しません。
すでにトランザクションが存在する場合はそのトランザクションを一時停止し、トランザクションを使用せずに処理を実行後、
停止していたトランザクションを再開します。

f:id:pppurple:20170830231310j:plain:w400

ServiceA直接
トランザクションを使用せずに直接更新しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
// トランザクションなしで更新
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@4f28c6d9
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ServiceBでトランザクションが新規作成されて、
ServiceAでは新規にコネクションを作成してトランザクションを使用せずにDBを更新し、
その後、ServiceBのトランザクションが再開して終了しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@3c6f913
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 2 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.connection                          : 5. Connection opened
jdbc.audit                               : 5. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 5. PreparedStatement.new PreparedStatement returned
// 新しいコネクションでトランザクションなしで更新
jdbc.audit                               : 5. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@fa4d0f5
jdbc.audit                               : 5. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 5. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 5. PreparedStatement.close() returned
jdbc.connection                          : 5. Connection closed
jdbc.audit                               : 5. Connection.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
MANDATORY

MANDATORYはすでにトランザクションが存在することを前提にします。
トランザクションが存在しない場合、例外が発生します。
すでに存在する場合はそのトランザクションを利用します。

f:id:pppurple:20170830231334j:plain:w400

ServiceA直接
トランザクションが存在しないため例外が発生しています。

2017-08-19 21:08:20.649 ERROR 73901 --- o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'] with root cause

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
                         :
                         :
                         :

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ServiceBでトランザクションが新規作成され、
その後ServiceAの処理ではそのトランザクションを利用しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@2bc05c21
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 2 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@610ccf8
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 0 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
NESTED

NESTEDはネストしたトランザクションを作成します。
トランザクションが存在しない場合、新規にトランザクションを開始し、
すでに存在する場合はそのトランザクションを利用しますが、その部分だけネストしたトランザクションのように処理されます。

f:id:pppurple:20170830231345j:plain:w400

ServiceA直接

新規にトランザクションが開始されていることが分かります。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@1bc2f58
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 2 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)
ログを見てみると、ネストした部分にはSAVEPOINTが設定され、ServiceBの処理が正常に終わるとSAVEPOINTが解除されています。
SAVEPOINTを設定することでネストしたトランザクションでも内側だけロールバックすることが出来ます。
(ただし部分的なトランザクションはDBの実装に依存するので、H2の場合はSAVEPOINTを利用しているようです)

下記の様にネストした部分にはSAVEPOINTが設定されています。

4. Connection.setSavepoint(SAVEPOINT_1) returned sp0: id=0 name=SAVEPOINT_1
                         :
                         :
4. Connection.releaseSavepoint(sp0: id=0 name=SAVEPOINT_1) returned
jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@29b25138
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 2 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.getMetaData() returned dbMeta4: conn9: url=jdbc:h2:mem:testdb user=SA
// SAVEPOINTを設定
jdbc.audit                               : 4. Connection.setSavepoint(SAVEPOINT_1) returned sp0: id=0 name=SAVEPOINT_1
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@4f28c6d9
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 0 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 0
jdbc.audit                               : 4. PreparedStatement.close() returned
// SAVEPOINTを解除
jdbc.audit                               : 4. Connection.releaseSavepoint(sp0: id=0 name=SAVEPOINT_1) returned
jdbc.audit                               : 4. Connection.commit() returned
// トランザクション1 終了
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
NEVER

NEVERはトランザクションを使用しません。
すでにトランザクションが存在する場合は例外が発生します。

f:id:pppurple:20170830232040j:plain:w400

ServiceA直接

トランザクションを使用せずに直接更新しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
// トランザクションを使用せずに更新
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Andy') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@25294da3
jdbc.audit                               : 4. PreparedStatement.setObject(1, 21) returned
jdbc.sqlonly                             : UPDATE person SET age=21 WHERE name = 'Andy'
jdbc.sqltiming                           : UPDATE person SET age=21 WHERE name = 'Andy'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned

ServiceB⇒ServiceA (ServiceB経由でServiceA呼び出し)

すでにトランザクションが存在するため例外が発生しています。

jdbc.connection                          : 4. Connection opened
jdbc.audit                               : 4. Connection.new Connection returned
jdbc.audit                               : null. DataSource.getConnection() returned
jdbc.audit                               : 4. Connection.getAutoCommit() returned true
// トランザクション1 開始
jdbc.audit                               : 4. Connection.setAutoCommit(false) returned
jdbc.audit                               : 4. PreparedStatement.new PreparedStatement returned
jdbc.audit                               : 4. Connection.prepareStatement(UPDATE person SET age=? WHERE name = 'Bobby') returned net.sf.log4jdbc.sql.jdbcapi.PreparedStatementSpy@50e1fd41
jdbc.audit                               : 4. PreparedStatement.setObject(1, 20) returned
jdbc.sqlonly                             : UPDATE person SET age=20 WHERE name = 'Bobby'
jdbc.sqltiming                           : UPDATE person SET age=20 WHERE name = 'Bobby'
 {executed in 1 msec}
jdbc.audit                               : 4. PreparedStatement.executeUpdate() returned 1
jdbc.audit                               : 4. PreparedStatement.close() returned
jdbc.audit                               : 4. Connection.rollback() returned
jdbc.audit                               : 4. Connection.setAutoCommit(true) returned
jdbc.audit                               : 4. Connection.isReadOnly() returned false
jdbc.connection                          : 4. Connection closed
jdbc.audit                               : 4. Connection.close() returned
// すでにトランザクションが存在しているため例外発生
2017-08-19 21:13:38.859 --- o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'] with root cause

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
                         :
                         :
                         :

終わり。

ソースは下記にあげときました。

github.com