代码实践Spring中事务的传播机制

149 阅读4分钟

前言

最近面试有被问到,Spring中事务的传播机制是什么?如果两个事务的隔离级别不一致,会有什么影响?这一块平时用的少,看了Spring中事务的传播机制相关知识,发现竟然有7种,这一下子哪里能记得住,所以想通过代码实践来加深一下自己的印象。

理论

参考文章:

Transaction Propagation and Isolation in Spring @Transactional

图解spring中七种事务传播行为 终于有人讲明白了

查看源码知道有这7种传播机制

    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

Spring中这七种Propagation类的事务属性详解:

REQUIRED:(默认值)支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。

MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。

REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。

NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。

代码实践

每次测试都重置一下数据

@BeforeEach
void setUp() {
    Account account1 = new Account(1, "jack", BigDecimal.valueOf(4));
    Account account2 = new Account(2, "rose", BigDecimal.valueOf(6));
    accountMapper.updateByPrimaryKeySelective(account1);
    accountMapper.updateByPrimaryKeySelective(account2);
    assert 4 == getBalance(1L);
    assert 6 == getBalance(2L);
    log.info("启动检测通过");
}

检验项目的事务是否正常

@Transactional
public void test1() {
// 事务里面给账户1扣除1块钱,账户2加上1块钱。但是2/0会异常,所以事务会回滚。
    accountService.minus1Money();
    int i = 2 / 0;
    accountService.plus2Money();
}

@Test
void test1() {
    log.info("开始测试当前项目配置下,事务是否正常开启");
    try {
        testTransactionService.test1();
    } catch (Exception e) {
        log.info("捕捉到异常:    " + e.getMessage());
    }
    // 测试下来,数据应该被回滚了
    assert 4 == getBalance(1L);
    assert 6 == getBalance(2L);
}

测试默认的传播机制

/**
 * REQUIRED  (默认传播行为)
 * 支持当前事务,如果当前没有事务,就新建一个事务
 */
@Test
void test_REQUIRED() {
    log.info("事务嵌套, 传播级别都是默认的");
    List<Long> sessionIds = testTransactionService.test3();
    log.info(sessionIds.toString());
    // 默认传播机制下,加入第一个事务,所以session id是同一个
    assert sessionIds.get(0).equals(sessionIds.get(1));
}


//以下是testTransactionService.test3()
@Transactional
public List<Long> test3() {
    accountService.minus1Money();
    Long sessionId1 = getSessionId();
    Long sessionId2 = self.plus2Money();
    return Arrays.asList(sessionId1, sessionId2);
}

@Transactional
public Long plus2Money() {
    accountService.plus2Money();
    return getSessionId();
}

测试REQUIRES_NEW传播机制

/**
 * REQUIRES_NEW
 * 开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起
 */
@Test
void test_REQUIRES_NEW() {
    log.info("测试嵌套事务,传播机制为REQUIRES_NEW");
    List<Long> sessionIds = testTransactionService.test4();
    log.info(sessionIds.toString());
    // REQUIRES_NEW传播机制下,创建新事务,所以session id不是同一个
    assert !sessionIds.get(0).equals(sessionIds.get(1));
}

//下面是testTransactionService.test4()
@Transactional()
public List<Long> test4() {
    accountService.minus1Money(); // 开启了第一个事务
    Long sessionId1 = getSessionId();
    Long sessionId2 = self.plus2Money_REQUIRES_NEW();//创建新事务,并提交
    return Arrays.asList(sessionId1, sessionId2);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public Long plus2Money_REQUIRES_NEW() {
    accountService.plus2Money();
    return getSessionId();
}

测试传播机制对事务的隔离级别有什么影响


@Test
void test3() throws InterruptedException {
    // 测试默认传播机制下,不同隔离级别会有什么影响
    // 测试步骤:
    // 使用新线程,打开事务0,修改id=1的余额-1,5s后提交
    Thread thread = new Thread(() -> {
        testTransactionService.delayTransaction();
    });
    thread.start();
    Thread.sleep(1000);
    // 使用声明式事务打开事务1(读已提交),看是否能读取到id=1的修改, 嵌套一个事务(读未提交),使用默认传播机制
    List<Account> accounts = testTransactionService.test_REQUIRED_isolation(1L);
    assert accounts.get(0).getBalance().intValue() == 4;
    assert accounts.get(1).getBalance().intValue() == 4; // 隔离级别沿用已经存在的那个事务,读已提交,所以看不到修改
    thread.join();
    log.info("事务0结束, 再次查看数据");
    accounts = testTransactionService.test_REQUIRED_isolation(1L);
    assert accounts.get(0).getBalance().intValue() == 3;
    assert accounts.get(1).getBalance().intValue() == 3;
}

@Test
void test4() throws InterruptedException {
    // 测试REQUIRES_NEW传播机制下,不同隔离级别会有什么影响
    // 测试步骤:
    // 使用新线程,打开事务0,修改id=1的余额-1,5s后提交
    Thread thread = new Thread(() -> {
        testTransactionService.delayTransaction();
    });
    thread.start();
    Thread.sleep(1000);//确保事务执行了
    // 使用声明式事务打开事务1(读已提交),看是否能读取到id=1的修改, 嵌套一个事务(读未提交),使用REQUIRES_NEW传播机制
    List<Account> accounts = testTransactionService.test_REQUIRES_NEW_isolation(1L);
    assert accounts.get(0).getBalance().intValue() == 4;
    assert accounts.get(1).getBalance().intValue() == 3; //隔离级别使用新创建的那个事务,使用的是读未提交,所以未提交的事务能被看到
    thread.join();
    log.info("事务0结束,再次查看数据");
    accounts = testTransactionService.test_REQUIRES_NEW_isolation(1L);
    assert accounts.get(0).getBalance().intValue() == 3;
    assert accounts.get(1).getBalance().intValue() == 3;
}

结论

Spring中事务传播有7种机制,重点记住默认的REQUIRED和另一个叫REQUIRES_NEW的。 默认情况下,如果已经开启了事务就直接沿用原来的事务即可。 REQUIRES_NEW的话,就会挂起原先的事务,创建一个新的事务来执行,再回到原来的事务中。

隔离级别的影响随着是否是新事物而变化,也很好记住,如果沿用原来的事务,那么嵌套的那个事务他的隔离级别就沿用。如果创建了新的事务,那嵌套的这个事务就按新事物配置的隔离级别。

附件

如果想看可执行的源代码,请查看github仓库