Давайте поговорим об управлении транзакциями Spring (устройство) от начала до конца.

Java задняя часть
Давайте поговорим об управлении транзакциями Spring (устройство) от начала до конца.

Это 3-й день моего участия в Gengwen Challenge.Подробности мероприятия смотрите:Обновить вызов

Ставь лайк и потом смотри, вырабатывай полезную привычку

управление транзакциями,一个被说烂的也被看烂的话题,还是八股文中的基础股之一。但除了八股文中需要熟读并背诵的那些个传播行为之外,背后的“为什么”和核心原理更为重要。 ​

Перед тем, как написать эту статью, я также прочитал несколько статей о принципе управления транзакциями, но большинство из них представляют собой тупой перевод исходного кода, и я продолжаю аннотировать исходный код. Несмотря на то, что эта статья о переводе исходного кода полезна, она не очень полезна для читателей: легко запутаться в некоторых деталях кода, и она не может помочь читателям быстро понять общую картину управления транзакциями и дизайнерских идей. ​

В этой статье будут шаг за шагом проанализированы дизайнерские идеи управления транзакциями Spring с точки зрения дизайна (менеджер транзакций будет разработан, можете ли вы его использовать?)

Зачем нужно управление транзакциями?

Давайте сначала посмотрим, нет ли диспетчера транзакций, еслиХотите иметь несколько операций (методов/классов) в одной транзакцииЧто делать:

// MethodA:
public void methodA(){
	Connection connection = acquireConnection();
    try{
        int updated = connection.prepareStatement().executeUpdate();
        methodB(connection);
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        releaseConnection(connection);
    }
}

// MethodB:
public void methodB(Connection connection){
	int updated = connection.prepareStatement().executeUpdate();
}

Или использовать ThreadLocal для хранения подключения?

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

// MethodA:
public void methodA(){
	Connection connection = acquireConnection();
	connHolder.set(connection);
    try{
        int updated = connection.prepareStatement().executeUpdate();
        methodB();
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        releaseConnection(connection);
        connHolder.remove();
    }
}

// MethodB:
public void methodB(){
    Connection connection = connHolder.get();
	int updated = connection.prepareStatement().executeUpdate();
}

Все еще немного отвратительно, немного более абстрактно? Извлеките операцию привязки Connection как общедоступный метод:

static ThreadLocal<Connection> connHolder = new ThreadLocal<>();

private void bindConnection(){
	Connection connection = acquireConnection();
    connHolder.set(connection);
}

private void unbindConnection(){
	releaseConnection(connection);
    connHolder.remove();
}

// MethodA:
public void methodA(){
    try{
        bindConnection();
        int updated = connection.prepareStatement().executeUpdate();
        methoB();
        connection.commit();
    }catch (Exception e){
        rollback(connection);
    }finally {
        unbindConnection();
    }
}

// MethodB:
public void methodB(){
    Connection connection = connHolder.get();
	int updated = connection.prepareStatement().executeUpdate();
}

Теперь это выглядит лучше, но у меня есть новое требование: я хочу, чтобы метод B был новой транзакцией независимо, коммит и откат отдельно, не затрагивая метод A. ​

Это... Но это немного сложно сделать.Соединение было привязано к ThreadLocal, и будет сложно обработать новую транзакцию. ​

Если это сложнее, метод B должен вызывать метод C, а для метода C также требуется независимая транзакция... ​

Более того, каждая операция привязки/развязки слишком глупа, если какой-то метод забывает написать unbind, и в конце концов происходит утечка соединения, это еще не конец! ​

К счастью, Spring предоставляет диспетчер транзакций, который помогает нам решить эту серию болевых точек.

Управление весной транзакции, чтобы решить проблему?

Spring предоставляет управление транзакциями, чтобы помочь нам управлять ресурсами, связанными с транзакциями, такими как связь JDBC, SQLSession Hibernate. Если вышеуказанное соединение связывается с ThreadLocal, чтобы решить этот способ поделиться транзакцией, нам помогло управление весенней транзакцией. ​

Это также может помочь нам иметь дело с вложенными транзакциями в сложных сценариях, таких как независимые транзакции метода B/methodC, упомянутые ранее.

Что такое вложенная транзакция?

Взяв приведенный выше пример в качестве примера, метод B вызывается в методе A, оба метода имеют операции с базой данных, и оба требуют транзакций:

// MethodA:
public void methodA(){
    int updated = connection.prepareStatement().executeUpdate();
    methodB();
    // ...
}

// MethodB:
public void methodB(){
    // ...
}

**Сценарии, в которых есть транзакции в нескольких цепочках вызовов методов, являются вложенными транзакциями. ** Однако следует отметить, что это не означает, что несколько методов используют транзакцию для вызова вложенности, даже если это другая транзакция, до тех пор, пока она находится в цепочке вызовов этого метода, это вложенная транзакция. .

Что такое поведение распространения транзакций?

那调用链中的子方法,是用一个新事务,还是使用当前事务呢?这个子方法决定使用新事务还是当前事务(或不使用事务)的策略,就叫事务传播。 ​

В управлении транзакциями SpringСтратегия обработки транзакций для этого подметода называется поведением распространения..

Какие способы распространения транзакций существуют?

Управление транзакциями Spring поддерживает различные способы распространения, которые не будут здесь публиковаться. ​

Но после классификации этих видов коммуникативного поведения остается не что иное, как следующие три типа:

  1. Приоритет текущих транзакций
  2. Не использовать текущую транзакцию, создать новую транзакцию
  3. не использовать транзакцию

Например, в приведенном выше примере независимая транзакция methodB/methodC относится ко второму поведению распространения — не использовать текущую транзакцию, создать новую транзакцию.

посмотреть на каштаны

с Spring JDBC + SpringАннотированная версияпример бизнеса. При поведении распространения транзакций по умолчанию метод A и метод B будут использовать одно и то же соединение в одной транзакции.

@Transactional
public void methodA(){
    jdbcTemplate.batchUpdate(updateSql, params);
    methodB();
}

@Transactional
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

Что делать, если я хочу, чтобы метод B не использовал транзакцию метода А и сам создавал новое соединение/транзакцию? Просто настройте аннотацию @Transactional:

@Transactional
public void methodA(){
    jdbcTemplate.batchUpdate(updateSql, params);
    methodB();
}

// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

Это так просто. Нам не нужно беспокоиться о таких операциях, как получение соединения/совместное использование нескольких методов. Соединение/совместное использование нескольких методов + эксклюзивное соединение/отправка/освобождение соединений. Spring делает это за нас.

Как откатиться?

В аннотированной версии управления транзакциями стратегия отката по умолчанию заключается в откате при возникновении исключения. Эта стратегия по умолчанию очень хороша, даже откат для нас решен, и нет необходимости вручную откатывать. ​

Но что, если во вложенной транзакции дочерний метод не зависит от новой транзакции? В это время, даже если возникнет исключение, оно может откатить только подтранзакцию и не может напрямую повлиять на предыдущую транзакцию.

可如果这个抛出的异常不是 sql 导致的,比如校验不通过或者其他的异常,此时应该将当前的事务回滚吗? ​

Это действительно не обязательно, кто сказал, что если выброшено исключение, то оно будет откатываться, а исключение не откатываться, верно? ​

Конечно! Генерация исключений и откат транзакций — это две проблемы, которые можно связать вместе или решать по отдельности.

// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务

// 指定 Exception 也不会滚
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

Используйте разные конфигурации для каждой транзакции/соединения

В дополнение к распространению и откату также можно использовать разные конфигурации для каждой транзакции/соединения, например разные уровни изоляции:

@Transactional
public void methodA(){
    jdbcTemplate.batchUpdate(updateSql, params);
    methodB();
}

// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
// 这个事务/连接中使用 RC 隔离级别,而不是默认的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void methodB(){
    jdbcTemplate.batchUpdate(updateSql, params);
}

В дополнение к уровню изоляции, разумеется, поддерживаются и другие конфигурации соединения JDBC, такие как только чтение. Таким образом, хотя нам не нужно явно получать соединение/сеанс, мы все же можем настроить разные параметры для каждой транзакции в гнезде, что очень гибко.

Краткое описание функций

Что ж, теперь, когда вы поняли все основные функции управления транзакциями Spring, давайте обобщим эти основные функциональные моменты:

  1. Управление подключением / ресурсами - нет необходимости вручную получить ресурсы, делиться ресурсами, выпустить ресурсы
  2. Поддержка вложенных транзакций — поддерживает использование различных стратегий ресурсов и стратегий отката во вложенных транзакциях.
  3. Используйте разные конфигурации для каждой транзакции/соединения

Модель диспетчера транзакций (TransactionManager)

На самом деле, подумайте хорошенько, есть только две основные операции управления транзакциями: фиксация и откат. На этих двух операциях основаны так называемые распространение, вложение, откат и т.п. ​

Таким образом, Spring абстрагирует основную функциональность управления транзакциями вМенеджер транзакций,基于这个事务管理器核心,可以实现多种事务管理的方式。 ​

Этот основной менеджер транзакций имеет только три функциональных интерфейса:

  1. Получить ресурсы транзакции, ресурс может быть произвольным, например jdbc connection/hibernate mybatis session, а затем привязать и сохранить
  2. совершить транзакцию- Зафиксировать указанный ресурс транзакции
  3. транзакция отката- Откат указанного ресурса транзакции
interface PlatformTransactionManager{
    // 获取事务资源,资源可以是任意的,比如jdbc connection/hibernate mybatis session之类
	TransactionStatus getTransaction(TransactionDefinition definition)
			throws TransactionException;
    
    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;
    
    // 回滚事务
    void rollback(TransactionStatus status) throws TransactionException;
}

Определение транзакции — TransactionDefinition

Помните приведенную выше аннотацию @Transactional, которая определяет такие свойства, как поведение распространения, уровень изоляции, стратегию отката, доступ только для чтения и т. д. Это определение операции транзакции. ​

При получении ресурсов транзакции необходимо выполнить различные настройки в соответствии с определением этой транзакции:

  1. Например, если настроена новая транзакция, то при получении ресурсов транзакции нужно создать новую, а не существующую.
  2. Например, если уровень изоляции настроен, при первом создании ресурса (соединения) необходимо установить распространение для соединения.
  3. Например, если настроено свойство только для чтения, то при первом создании ресурса (Connection) нужно установить readOnly для Connection

为什么要单独用一个 TransactionDefinition 来存储事务定义,直接用注解的属性不行吗? ​

Конечно, можно, но управление транзакциями аннотаций — это только автоматическая передача, предоставляемая Spring, а также есть управление транзакциями ручной передачи, подходящее для старых драйверов (будет представлено позже); ручная передача не использует аннотации, поэтому отдельный Модель определения транзакции построена таким образом, чтобы ее можно было достичь универсальной.

Статус транзакции — TransactionStatus

Так как при вложенной транзакции транзакция каждого подметода может быть разной, то должен быть статус транзакции подметода — TransactionStatus, который используется для хранения некоторых данных и состояния текущей транзакции, таких как ресурсы транзакции (Connection ), статус отката и т.д.

Получить ресурсы транзакции

Первым шагом диспетчера транзакций является получение/создание ресурсов в соответствии с определением транзакции.Самым трудным шагом на этом этапе является различение поведения распространения.Логика при разных поведениях распространения не одинакова. ​

«При распространении по умолчанию используйте текущую транзакцию», как подсчитать текущую транзакцию? ​

Для хранения ресурсов транзакций, если они уже существуют, существует текущая транзакция, и вы можете напрямую получить сохраненные ресурсы транзакций. Пример в начале статьи также демонстрирует, что если вы хотите, чтобы несколько методов использовали одну и ту же транзакцию без всякого смысла, вы можете использовать ThreadLocal для ее хранения, что просто и грубо. ​

Весна также делает это, но это сложнее и абстрагирует слой.Диспетчер синхронизации ресурсов транзакций — TransactionSynchronizationManager (далее в этой статье — TxSyncMgr)., В этом диспетчере синхронизации для хранения ресурсов транзакций используется ThreadLocal (для удобства понимания в этой статье максимально не вставляется некритичный исходный код). ​

Остальное - выполнять разные стратегии в соответствии с разным поведением распространения.После классификации есть только 3 условные ветви:

  1. В настоящее время существует транзакция — разная обработка в соответствии с разным поведением распространения
  2. Текущей транзакции нет, но необходимо открыть новую транзакцию
  3. Не используйте транзакции вообще - это редко используется
public final TransactionStatus getTransaction(TransactionDefinition definition) {
    //创建事务资源 - 比如 Connection
    Object transaction = doGetTransaction();
    
    if (isExistingTransaction(transaction)) {
        // 处理当前已有事务的场景
        return handleExistingTransaction(def, transaction, debugEnabled);
    }else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
				def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){
        
        // 开启新事务
    	return startTransaction(def, transaction, debugEnabled, suspendedResources);
    }else {
    	// 彻底不用事务
    }
    
    // ...
}

Позвольте мне сначала представитьВетвь 2 — текущей транзакции нет, но необходимо запустить новую транзакцию, логика относительно проста. Просто создайте новый ресурс транзакции и привяжите его к ThreadLocal:

private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
			boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {
		
    	// 创建事务
		DefaultTransactionStatus status = newTransactionStatus(
				definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    	
    	// 开启事务(beginTx或者setAutoCommit之类的操作)
    	// 然后将事务资源绑定到事务资源管理器 TransactionSynchronizationManager
		doBegin(transaction, definition);

Теперь вернитесь в ветку1 — в настоящее время есть транзакция — обрабатывается по-разному в зависимости от поведения распространения, это немного более хлопотно. Из-за необходимости независимых транзакций подметодов TransactionSynchronizationManager может хранить только один ресурс транзакции.

Приостановить и выздоровление (резюме)

Spring использует дизайн **Suspend (Приостановить)-Resume (Возобновить)**, чтобы решить эту проблему обработки вложенных ресурсов. Когда подметоду требуется независимая транзакция, он приостанавливает текущую транзакцию, удаляет текущий ресурс транзакции из TxSyncMgr и сохраняет ресурс приостановленной транзакции в новом состоянии транзакции TransactionStatus при создании состояния новой транзакции; в конце подметода, вам нужно только снова извлечь ресурс приостановленной транзакции из состояния транзакции подметода и повторно привязать его к TxSyncMgr для завершения операции восстановления. ​

Весь процесс приостановки-возобновления показан на следующем рисунке: ​

spring_tx_suspend_resume (2).png Примечание. Операция приостановки выполняется на этапе получения ресурсов транзакции, а операция возобновления выполняется в конце подметода (фиксация или откат).

Таким образом, каждый TransactionStatus будет сохранять отложенные ресурсы перед транзакцией.Если цепочка вызовов методов очень длинная и каждый раз это новая транзакция, то этот TransactionStatus будет выглядеть как связанный список:spring_tx_suspend_status.png

совершить транзакцию

После получения ресурсов и завершения операции пришло время зафиксировать транзакцию.Эта операция фиксации относительно проста и состоит всего из двух шагов:

  1. В настоящее время это новая транзакция
  2. Обработка ожидающих ресурсов

Откуда ты знаешь, что это новый бизнес? ​

Каждый раз, когда транзакция вложена, будет создан новый TransactionStatus, и этот статус транзакции будет записывать, является ли текущая транзакция новой транзакцией. Если все несколько подметодов используют транзакционный ресурс, то ничто, кроме TransactionStatus первого из них, создавшего транзакционный ресурс, не является новой транзакцией. ​

Как показано на рисунке ниже, когда A -> B -> C, поскольку все BC используют текущую транзакцию, хотя ресурсы транзакции, используемые ABC, одинаковы, только TransactionStatus A является новой транзакцией, а BC — нет; затем отправить в BC. Во время транзакции фиксация фактически не будет вызываться, а операция фиксации будет вызываться только тогда, когда она возвращается к A для выполнения операции фиксации.spring_tx_suspend_status (1).pngВот объяснение того, почему новые транзакции должны быть зафиксированы, в то время как существующим транзакциям не нужно ничего делать:

Поскольку для новой транзакции операция фиксации здесь уже завершена, для сценария не новой транзакции предварительная транзакция (т. е. текущая транзакция) не была выполнена, и за ней могут стоять другие операции с базой данных, так что это зафиксировано. операция должна быть выполнена создателем текущей транзакции, который не может быть представлен здесь.

транзакция отката

Кроме фиксации есть еще и откат, логика отката транзакции аналогична логике фиксации транзакции:

  1. Если это новая транзакция, она будет откатана, причины указаны выше.
  2. Если это не новая транзакция, просто установите флаг отката
  3. Обработка ожидающих ресурсов

Примечание. Диспетчер транзакций не содержит стратегии отката.Стратегия отката — это расширенная функция АОП-версии управления транзакциями, но эта функция не принадлежит основному диспетчеру транзакций.

Автоматическая и механическая коробка передач

Функции управления транзакциями Spring работают вокруг вышеупомянутого менеджера транзакций, предоставляя три способа управления транзакциями, а именно:

  1. Управление транзакциями для XML AOP - относительно старый и сейчас мало используемый
  2. Аннотированная версия управления транзакциями — @Transactional
  3. TransactionTemplate — ручное управление транзакциями, также известное как программное управление транзакциями.

автоматическая коробка передач

XML/@Transactional Два вида управления аннотациями, основанные на АОП, класс входа — TransactionInterceptor, который является перехватчиком АОП, который отвечает за вызов диспетчера транзакций для реализации управления транзакциями. ​

Поскольку основные функции реализованы в диспетчере транзакций, этот перехватчик АОП очень прост, достаточно вызвать диспетчер транзакций.Основной (псевдо) код выглядит следующим образом:

public Object invoke(MethodInvocation invocation) throws Throwable {
    
    // 获取事务资源
	Object transaction = transactionManager.getTransaction(txAttr);    
    Object retVal;
    
    try {
        // 执行业务代码
    	retVal = invocation.proceedWithInvocation();
        
        // 提交事务
        transactionManager.commit(txStatus);
    } catch (Throwable ex){
        // 先判断异常回滚策略,然后调用事务管理器的 rollback
    	rollbackOn(ex, txStatus);
    } 
}

Этот автоматический управление AOP и транзакциями также добавляет игровой процесс стратегии отката отката, это транзакцииTEmplate не имеет механической передачи, но эта функция не находится в диспетчере транзакций, просто повышенная версия дела AOP.

механическая коробка передач

TransactionTemplateЭто ручное управление транзакциями, хоть и нет удобства для аннотаций, но, к счастью, оно гибкое, а исключения/откаты можно контролировать самостоятельно. ​

Так что эта реализация проще, стратегии отката исключений нет, а специальный метод отката нужно задавать самостоятельно (по умолчанию откатывается любое исключение), основной (псевдо) код такой:

public <T> T execute(TransactionCallback<T> action) throws TransactionException {
	
    // 获取事务资源
    TransactionStatus status = this.transactionManager.getTransaction(this);
    T result;
    try {
        
        // 执行 callback 业务代码
        result = action.doInTransaction(status);
    }
    catch (Throwable ex) {
        
        // 调用事务管理器的 rollback
        rollbackOnException(status, ex);
    }
    
    提交事务
    this.transactionManager.commit(status);
	}
}

Почему такая удобная АКПП, а еще МКПП?

Поскольку автоматическая передача более гибкая, вы можете играть как хотите, например, я могу выполнять несколько операций с базой данных одним методом, но использовать разные ресурсы транзакций:

Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                       new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
			// update 0
            int rows0 = jdbcTemplate.update(...);
            
            // update 1
            int rows1 = jdbcTemplate.update(...);
            return rows0 + rows1;
        }
    });

Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
            
            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });

В приведенном выше примере, через транзакциюTemplate мы можем точно контролировать ресурсы Update0 / UPDATE1 и использование одинаковых уровней изоляции транзакций, в то время как UPDATE2 используют отдельные транзакционные ресурсы и не требуют нового класса аннотированного способа.

Возможно ли это сделать своими руками?

Конечно, пока мы используем один и тот же экземпляр диспетчера транзакций, потому что привязка ресурсов к синхронному диспетчеру ресурсов выполняется в диспетчере транзакций. ​

В версии управления транзакциями AOP вы также можете использовать ручное управление транзакциями для продолжения операций, а также можете использовать тот же ресурс транзакций. ​

Например, в следующем коде update1/update2 все еще находится в транзакции, и транзакция не будет зафиксирована после завершения обратного вызова update2, а транзакция будет зафиксирована в TransactionInterceptor, когда метод A завершится.

@Transactional
public void methodA(){
    
    // update 1
	jdbcTemplate.update(...);
    new TransactionTemplate((PlatformTransactionManager) transactionManager,
                                        new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
    .execute(new TransactionCallback<Integer>() {
        @Override
        public Integer doInTransaction(TransactionStatus status) {
            
            // update 2
            int rows2 = jdbcTemplate.update(...);
            return rows2;
        }
    });
   
}

Суммировать

Ядром управления транзакциями Spring является абстрактный менеджер транзакций. Несколько методов XML/@Transactional/TransactionTemplate основаны на этом менеджере транзакций. Основная реализация трех методов не сильно отличается, но вход отличается.spring_tx_architecture.png

本文为了方便理解,省略了大量的非关键实现细节,可能会导致有部分描述不严谨的地方,如有问题欢迎评论区留言。 ​

Нелегко быть оригинальным, и несанкционированная перепечатка запрещена. Если моя статья полезна для вас, пожалуйста, поставьте лайк/добавьте в избранное/подпишитесь, чтобы поддержать и поддержать ее ❤❤❤❤❤❤