Обратите внимание на транзакции Spring, чтобы избежать крупных транзакций.

Java

задний план

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

дело, наверное, у всехCRUDКороль не новичок в этом.В основном, есть несколько запросов на запись, которые должны использовать транзакции, и Spring использует транзакции очень просто, только один@TransactionalАннотируйте его, как в следующем примере:

    @Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        return order.getId();
    }

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

Добросовестное использование транзакций

Что бы вы сделали для приведенного выше кода для создания заказа, если вам сейчас нужно добавить требование, отправить сообщение в очередь сообщений или вызвать RPC после создания заказа? Многие студенты сначала подумают о вызове непосредственно в методе транзакции:

    @Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        sendRpc();
        sendMessage();
        return order.getId();
    }

Такой код появится в бизнесе, написанном многими людьми.В транзакции rpc вложен, и некоторые операции не в БД вложены.При нормальных обстоятельствах нет проблем в написании таким образом.Как только не-БД пишут операции появляются медленнее, или трафик относительно велик, возникнет проблема больших транзакций. Поскольку транзакция не была зафиксирована, соединение с базой данных будет занято. В это время вы можете спросить, ничего, если я расширим соединение с базой данных, 1000 не будет работать, я уже упоминал в предыдущей статье, что размер пула соединений с базой данных все равно будет влиять на производительность нашей базы данных, поэтому подключение к базе данных не расширяется столько, сколько вы хотите.

Тогда как мы должны его оптимизировать? Я могу подумать об этом тщательно, наша не-БД операция на самом деле является ACID, который не удовлетворяет наши дела, тогда вы хотите написать внутри транзакции, чтобы мы могли извлечь ее здесь.

    public int createOrder(Order order){
        createOrderService.createOrder(order);
        sendRpc();
        sendMessage();
    }

В этом методе сначала вызывается порядок создания транзакции, а затем вызываются другие операции, не относящиеся к БД. Если теперь нам нужна более сложная логика, например, отправка успешного запроса RPC в случае успешного создания заказа и отправка неудачного запроса RPC в случае сбоя, мы можем выполнить следующее преобразование из приведенного выше кода:

    public int createOrder(Order order){
        try {
            createOrderService.createOrder(order);
            sendSuccessedRpc();
        }catch (Exception e){
            sendFailedRpc();
            throw e;
        }
    }

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

TransactionSynchronizationManager

В транзакции Spring предоставляются некоторые методы инструментов, которые помогают нам выполнить это требование. существуетTransactionSynchronizationManagerМетод, который позволяет нам зарегистрировать callBack с транзакцией, представлен в:

public static void registerSynchronization(TransactionSynchronization synchronization)
			throws IllegalStateException {

		Assert.notNull(synchronization, "TransactionSynchronization must not be null");
		if (!isSynchronizationActive()) {
			throw new IllegalStateException("Transaction synchronization is not active");
		}
		synchronizations.get().add(synchronization);
	}

TransactionSynchronization — это обратный вызов нашей транзакции, который предоставляет нам некоторые точки расширения:

public interface TransactionSynchronization extends Flushable {

	int STATUS_COMMITTED = 0;
	int STATUS_ROLLED_BACK = 1;
	int STATUS_UNKNOWN = 2;
	
	/**
	 * 挂起时触发
	 */
	void suspend();

	/**
	 * 挂起事务抛出异常的时候 会触发
	 */
	void resume();


	@Override
	void flush();

	/**
	 * 在事务提交之前触发
	 */
	void beforeCommit(boolean readOnly);

	/**
	 * 在事务完成之前触发
	 */
	void beforeCompletion();

	/**
	 * 在事务提交之后触发
	 */
	void afterCommit();

	/**
	 * 在事务完成之后触发
	 */
	void afterCompletion(int status);
}

Мы можем использовать метод AfterComplettion для реализации нашей бизнес-логики выше:

    @Transactional
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_COMMITTED){
                    sendSuccessedRpc();
                }else {
                    sendFailedRpc();
                }
            }
        });
        return order.getId();
    }

Здесь мы напрямую реализуем afterCompletion, который определяется статусом транзакции, какой RPC мы должны отправить. Конечно, мы можем дополнительно инкапсулироватьTransactionSynchronizationManager.registerSynchronizationИнкапсуляция его в виде транзакционного Util может сделать наш код более лаконичным.

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

Яма послеЗавершения

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

В Spring AbstractPlatformTransactionManager код для обработки фиксации выглядит следующим образом:

private void processCommit(DefaultTransactionStatus status) throws TransactionException {
		try {
			boolean beforeCompletionInvoked = false;
			try {
				prepareForCommit(status);
				triggerBeforeCommit(status);
				triggerBeforeCompletion(status);
				beforeCompletionInvoked = true;
				boolean globalRollbackOnly = false;
				if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
					globalRollbackOnly = status.isGlobalRollbackOnly();
				}
				if (status.hasSavepoint()) {
					if (status.isDebug()) {
						logger.debug("Releasing transaction savepoint");
					}
					status.releaseHeldSavepoint();
				}
				else if (status.isNewTransaction()) {
					if (status.isDebug()) {
						logger.debug("Initiating transaction commit");
					}
					doCommit(status);
				}
				// Throw UnexpectedRollbackException if we have a global rollback-only
				// marker but still didn't get a corresponding exception from commit.
				if (globalRollbackOnly) {
					throw new UnexpectedRollbackException(
							"Transaction silently rolled back because it has been marked as rollback-only");
				}
			}
	

			// Trigger afterCommit callbacks, with an exception thrown there
			// propagated to callers but the transaction still considered as committed.
			try {
				triggerAfterCommit(status);
			}
			finally {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
			}

		}
		finally {
			cleanupAfterCompletion(status);
		}
	}

Здесь нам нужно только обратить внимание на последние несколько строк кода.Мы можем обнаружить, что наш triggerAfterCompletion является предпоследней логикой выполнения.Когда все коды будут выполнены, наш cleanupAfterCompletion будет выполнен, и наше возвратное соединение с базой данных также находится в состоянии Этот раздел В коде это замедляет получение соединения с базой данных.

Как оптимизировать

Как оптимизировать вышеуказанную проблему? Здесь есть три варианта оптимизации:

  • Операция не БД упоминается вне транзакции.Этот метод также является самым примитивным методом выше.Для какой-то простой логики его можно извлечь,но для какой-то сложной логики,например,вложенность транзакций,afterCompletion вызывается во вложенности , Это потребует гораздо больше работы и может привести к проблемам.
  • Выполняя это асинхронно с несколькими потоками, улучшается скорость возврата пула соединений с базой данных Это подходит для регистрации afterCompletion и записи его в конце транзакции, а также непосредственного помещения того, что нужно сделать, в другие потоки, чтобы это сделать. Однако если регистрация afterCompletion происходит между нашими транзакциями, такими как вложенные транзакции, это приведет к тому, что последующая бизнес-логика будет выполняться параллельно с транзакцией.
  • Имитирует регистрацию обратного вызова транзакции Spring и реализует новые аннотации. Вышеупомянутые два метода имеют свои недостатки, поэтому мы, наконец, приняли этот метод и реализовали пользовательскую аннотацию.@MethodCallBack, Поместите эту аннотацию на транзакцию, используя указанное выше, а затем используйте аналогичный регистрационный код.
    @Transactional
    @MethodCallBack
    public int createOrder(Order order){
        orderDbStorage.save(order);
        orderItemDbStorage.save(order.getItems());
        MethodCallbackHelper.registerOnSuccess(() -> sendSuccessedRpc());
         MethodCallbackHelper.registerOnThrowable(throwable -> sendFailedRpc());
        return order.getId();
    }

Через третий метод мы в основном нужно только заменить все места, где мы регистрируем обратный вызов транзакции, и его можно использовать нормально.

Давайте поговорим о большом бизнесе

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

  • Если выполняется много манипуляций с данными, например вставка большого количества данных в транзакцию, время выполнения транзакции, естественно, становится очень большим.
  • Конкуренция за блокировки велика, когда все соединения работают с одними и теми же данными одновременно, будут очереди и ожидания, а время транзакций, естественно, увеличится.
  • В транзакции есть другие операции, не относящиеся к БД, такие как некоторые запросы RPC.Некоторые люди говорят, что мой RPC очень быстрый и не увеличит время выполнения транзакции, но сам запрос RPC является нестабильным фактором, на который влияют многие факторы, колебания сети, Нисходящие службы реагируют медленно.Если эти факторы возникают, будет большое количество транзакций, которые занимают много времени, что может привести к зависанию Mysql и вызвать лавину.

В приведенных выше трех ситуациях первые две могут быть не особенно распространены, но в третьей транзакции много операций не БД, что очень часто для нас.Обычно причиной такой ситуации часто являются наши собственные привычки и нормы, новички Или какие-то неопытные люди пишут код, они часто сначала пишут большой метод, добавляют аннотации транзакций прямо в этот метод, а потом уже добавляют в него, не важно какая логика, просто челнок, как на картинке ниже То же самое:

Конечно, некоторые люди хотят участвовать в том, что такое распределенная транзакция, но неверный подход для распределенной транзакции CAN Cance Focus Cita, также может использовать аннотацию, сможет помочь вам сделать распределенную транзакцию.

Наконец

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

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

Если вы считаете, что эта статья полезна для вас, то ваше внимание и пересылка - самая большая поддержка для меня, O(∩_∩)O: