Кровавый случай, вызванный для обновления

база данных

Общедоступная учетная запись WeChat «Back-end Advanced», ориентированная на совместное использование серверных технологий: Java, Golang, WEB-инфраструктура, распределенное промежуточное программное обеспечение, управление услугами и т. д.
Старый водитель научил тебя всем деньгам и довел до продвинутого уровня, я не успел объяснить и сесть в автобус!

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

Место преступления

В последнее время во время вызовов RPC между некоторыми службами Dubbo компании время от времени возникало несколько серьезных проблем с тайм-аутом, из-за которых некоторые модули не могли нормально предоставлять службы. Наша база данных — Oracle.После расследования администратора баз данных было обнаружено, что некоторое время выполнения sql очень велико.Для сравнения было обнаружено, что все эти sql с длительным временем выполнения имеют для обновления пессимистические блокировки, поэтому соответствующие разработчики проверяют бизнес-код, соответствующий sql и обнаруживаем, что For update не выполняется в транзакции Spring, но согласно здравому смыслу, если for update не добавляется с транзакцией Spring, Mybatis поможет нам зафиксировать освобождение ресурсов после каждого выполнения.Проблема параллелизма должна заключаться в том, что соответствующий ресурсы не блокируются грязные данные вместо блокировки. Но после отладки кода параллельное выполнение без транзакции Spring действительно заблокируется.

анализ случая

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

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

public void forupdateByTransaction() throws Exception {
  // 主线程获取独占锁
  reentrantLock.lock();
  
  new Thread(() -> transactionTemplate.execute(transactionStatus -> {
    // select * from forupdate where name = #{name} for update
    this.forupdateMapper.findByName("testforupdate");
    System.out.println("==========for update==========");
    countDownLatch.countDown();
    // 阻塞不让提交事务
    reentrantLock.lock();
    return null;
  })).start();
  
  countDownLatch.await();
  
  System.out.println("==========for update has countdown==========");
  this.forupdateMapper.updateByName("testforupdate");
  System.out.println("==========update success==========");

  reentrantLock.unlock();
}

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

Параллельное выполнение без транзакции Spring

public void forupdateByConcurrent() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
    }).start();
  }

}

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

После получения результатов теста обнаружено, что если есть 2 или более объектов подключения с разными идентификаторами, выполняющих sql, произойдет блокировка, но Mysql не будет блокироваться.Что касается того, почему Mysql не будет блокироваться, я объясню вам позже.

Из-за используемого нами пула соединений druid его autoCommit по умолчанию имеет значение true, поэтому я установил для параметра autoCommit пула соединений druid значение false в это время, снова запустил тестовый код и обнаружил, что oracle не будет блокироваться в это время, давайте сначала запомните этот тест В результате я проведу вас через волну исходного кода ниже, чтобы объяснить это явление.

Умный, вы можете подумать, что базовый исходный код Mybatis инкапсулирует для нас некоторые повторяющиеся операции. Например, когда мы выполняем оператор sql, mybatis автоматически выполняет фиксацию или откат для нас. Это также является основным требованием среды JDBC, поэтому, поскольку Mybatis помогает Мы сделали коммит, обновление for должно быть выпущено, почему проблема с блокировкой все еще возникает? Если вы можете придумать этот вопрос, это показывает, что вы серьезный мыслитель.Мы также сначала запоминаем этот вопрос, а объяснение будет позже.

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

private void forupdateByConcurrentAndTransaction() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> transactionTemplate.execute(transactionStatus -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
      return null;
    })).start();
  }
}

Этот анализ случая предназначен в основном для проверки того, связано ли оно с транзакцией Spring.Я установил для параметра autoCommit пула ссылок druid значение true и false соответственно и обнаружил, что обновление for выполняется одновременно при упаковке транзакции Spring, и никакой блокировки не происходит Из результатов теста кажется, что это во многом связано с транзакциями Spring.

Теперь мы подведем итоги теста анализа случая:

  1. Транзакция не зафиксирована, и пессимистическая блокировка для обновления не будет снята;
  2. Если оператор обновления выполняется одновременно без транзакции Spring, если для обновления выполняется более двух соединений с разными идентификаторами, произойдет явление блокировки, но Mysql не будет блокироваться;
  3. Оператор for update выполняется одновременно без транзакции Spring, и autocommit=false пула соединений druid не будет блокироваться;
  4. Добавьте транзакцию Spring для одновременного выполнения оператора for update без блокировки.

Вставьте адрес тестового кода:GitHub.com/obj кодирование/он…

Волна исходного кода

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

Менеджер транзакций Mybatis

Вы заметили, что до сих пор я делал упор на транзакции Spring.На самом деле, с точки зрения базы данных, sql является транзакцией, если она выполняется между START TRANSACTION и COMMIT или ROLLBACK, а транзакция Spring, которую я подчеркиваю, относится к транзакциям управляемый Spring, и Mybatis также имеет свой собственный менеджер транзакций, обычно мы используем Mybatis с Spring, а Spring интегрирует Mybatis В пакете Mybatis-spring есть класс SpringManagedTransaction, который является Mybatis Менеджер транзакций JDBC в системе Spring , Mybatis использует его для управления жизненным циклом соединения JDBC, хотя его имя начинается с Spring, но оно не имеет ничего общего с менеджером транзакций Spring.

Mybatis создаст сессию SqlSession при выполнении sql.По поводу SqlSession, однокурсник Чжун сидящий рядом со мной ранее спрашивал меня о механизме создания SqlSession.Я специально написал статью.Если интересно,можете прочитать,и повторяться не буду это здесь. заявил:

"Одноклассник Чжун, это для тебя!"

При создании SqlSession соответственно создается диспетчер транзакций:

org.mybatis.spring.transaction.SpringManagedTransactionFactory#newTransaction:

public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
  return new SpringManagedTransaction(dataSource);
}

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

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

org.mybatis.spring.transaction.SpringManagedTransaction#openConnection:

private void openConnection() throws SQLException {
  this.connection = DataSourceUtils.getConnection(this.dataSource);
  this.autoCommit = this.connection.getAutoCommit();
  this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
  if (LOGGER.isDebugEnabled()) {
    LOGGER.debug(
      "JDBC Connection ["
      + this.connection
      + "] will"
      + (this.isConnectionTransactional ? " " : " not ")
      + "be managed by Spring");
  }
}

Здесь объект соединения будет получен из пула соединений с базой данных, а затем значение autoCommit в объекте соединения будет присвоено SpringManagedTransaction! Можно понять, что в менеджере транзакций Mybatis под системой Spring значение autoCommit перезаписывается пулом соединений с базой данных! Следующий журнал отладки также показывает, что этот объект подключения JDBC не управляется вашим Spring, Mybatis может управлять им самостоятельно, так что не участвуйте вслепую в вашем Spring.

После выполнения sql Mybatis автоматически зафиксирует для нас Давайте посмотрим на прокси-сервер sqlSession SqlSessionTemplate:

org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor:

if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
  // force commit even on non-dirty sessions because some databases require
  // a commit/rollback before calling close()
  sqlSession.commit(true);
}

Считается, что если он не управляется транзакцией Spring, операция фиксации будет принудительной.Мы нажимаем и обнаруживаем, что наконец вызывается метод фиксации менеджера транзакций Mybatis:

org.mybatis.spring.transaction.SpringManagedTransaction#commit:

public void commit() throws SQLException {
  if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
    }
    this.connection.commit();
  }
}

Проблема возникает здесь, как я уже говорил ранее, autoCommit используемого нами пула соединений с базой данных druid по умолчанию имеет значение true, и когда менеджер транзакций получает объект соединения, он назначает автофиксацию соединения диспетчеру транзакций. верно, Mybatis считает, что соединение автоматически зафиксировало транзакцию. Поскольку это вне моего контроля, Mybatis, естественно, не будет совершать транзакцию снова.

По результатам теста, после установки autoCommit druid в false феномена блокировки не произойдет, то есть Mybaits выполнит следующую операцию фиксации. Итак, вопрос в том, когда автофиксация соединения = true, есть ли фиксация? По результатам теста видно, что фиксации нет. Здесь мы объясним на уровне базы данных, потому что автофиксация базы данных Oracle компании использует значение по умолчанию false, то есть транзакцию фиксации необходимо явно зафиксировать, прежде чем ее можно будет зафиксировать. Вот почему, когда у друида autoCommit=false, одновременное выполнение не вызовет блокировку, потому что Mybatis уже помог нам автоматически зафиксировать.

И почему Mysql все еще не блокируется, когда autoCommit=true у друида? Сначала я включаю печать журнала Mysql:

set global general_log = 1;

Глядя на журнал, я обнаружил, что Mysql установит autocommit=1 для каждого выполненного sql, то есть автоматически зафиксирует транзакцию, без необходимости явно зафиксировать фиксацию, каждый sql является транзакцией.

Весенний менеджер транзакций

В приведенном выше анализе случая параллельное выполнение транзакции Spring не приведет к блокировке.Очевидно, что транзакция Spring должна была совершить какие-то неописуемые действия.Существует много менеджеров транзакций Spring, и здесь мы используем подключение к базе данных.Менеджер пула называется DataSourceTransactionManager. Чтобы гибко управлять детальной областью транзакции, я использую декларативную транзакцию. Мы продолжаем проходить по исходному коду и отслеживать его на всем пути от записи транзакции. Мы обнаруживаем, что первый шаг для вызова метода doBegin:

org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin:

// Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
if (con.getAutoCommit()) {
  txObject.setMustRestoreAutoCommit(true);
  if (logger.isDebugEnabled()) {
    logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
  }
  con.setAutoCommit(false);
}

Мы обнаружили в методе doBegin, что он тайно подделал значение autoCommit объекта подключения и установил его в false. Каждый должен понять принцип здесь. Транзакции управления Spring фактически устанавливают текущий объект подключения не до выполнения sql. в режиме автоматической фиксации следующий sql не будет автоматически зафиксирован.В ожидании окончания транзакции менеджер транзакций Spring поможет нам зафиксировать транзакцию. Вот почему одновременное выполнение транзакций Spring не вызывает блокировки, принцип такой же, как описанный в Mybatis выше.

org.springframework.jdbc.datasource.DataSourceTransactionManager#doCleanupAfterCompletion:

// Reset connection.
Connection con = txObject.getConnectionHolder().getConnection();
try {
  if (txObject.isMustRestoreAutoCommit()) {
    con.setAutoCommit(true);
  }
  DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel());
}
catch (Throwable ex) {
  logger.debug("Could not reset JDBC Connection after transaction", ex);
}

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

напиши в конце

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

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

公众号「后端进阶」,专注后端技术分享!