Статьи

Руководство для начинающих по уровням изоляции транзакций в корпоративной Java

Вступление

Модель строгой согласованности реляционной базы данных основана на свойствах транзакции ACID . В этой статье мы рассмотрим причины использования разных уровней изоляции транзакций и различных шаблонов конфигурации как для локальных ресурсов, так и для транзакций JTA .

Изоляция и последовательность

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

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

  • ЧИТАТЬ БЕЗ КОММИТЕТЫ
  • READ COMMITTED (защита от грязного чтения)
  • REPEATABLE READ (защита от грязных и неповторяющихся чтений)
  • SERIALIZABLE (защита от грязных, неповторяющихся чтений и фантомных чтений)

Хотя наиболее последовательный уровень изоляции SERIALIZABLE был бы наиболее безопасным, большинство баз данных по умолчанию вместо этого используют READ COMMITTED. Согласно закону Амдаля , для обеспечения возможности одновременного выполнения большего количества транзакций нам необходимо сократить последовательную долю обработки данных. Чем короче интервал получения блокировки, тем больше запросов может обработать база данных.

Уровни изоляции

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

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

База данных и уровни изоляции

Помимо MySQL (который использует REPEATABLE_READ), уровень изоляции по умолчанию для большинства систем реляционных баз данных — READ_COMMITTED. Все базы данных позволяют вам установить уровень изоляции транзакции по умолчанию.

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

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

Уровень изоляции источника данных

Объект JDBC Connection позволяет нам устанавливать уровень изоляции для всех транзакций, выполненных для этого конкретного соединения. Установление нового соединения с базой данных является ресурсоемким процессом, поэтому большинство приложений используют пул соединений DataSource . DataSource пула соединений также может установить уровень изоляции транзакции по умолчанию:

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

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

Уровень изоляции в спящем режиме

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

Транзакции JTA требуют XAConnection, и администратор транзакций JTA отвечает за обеспечение XA-совместимых соединений.

Ресурс локальных транзакций может использовать ресурс локальный DataSource, и для этого сценария Hibernate предлагает несколько вариантов поставщика соединений:

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

Hibernate предлагает конфигурацию уровня изоляции транзакции, называемую hibernate.connection.isolation , поэтому мы собираемся проверить, как ведут себя все вышеупомянутые поставщики соединений при использовании этого конкретного параметра.

Для этого мы собираемся:

  1. Создайте SessionFactory:
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    @Override
    protected SessionFactory newSessionFactory() {
        Properties properties = getProperties();
     
        return new Configuration()
                .addProperties(properties)
                .addAnnotatedClass(SecurityId.class)
                .buildSessionFactory(
                        new StandardServiceRegistryBuilder()
                                .applySettings(properties)
                                .build()
        );
    }
  2. Откройте новый сеанс и проверьте уровень изоляции связанной транзакции подключения:
    01
    02
    03
    04
    05
    06
    07
    08
    09
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Test
        public void test() {
            Session session = null;
            Transaction txn = null;
            try {
                session = getSessionFactory().openSession();
                txn = session.beginTransaction();
                session.doWork(new Work() {
                    @Override
                    public void execute(Connection connection) throws SQLException {
                        LOGGER.debug("Transaction isolation level is {}", Environment.isolationLevelToString(connection.getTransactionIsolation()));
                    }
                });
                txn.commit();
            } catch (RuntimeException e) {
                if ( txn != null && txn.isActive() ) txn.rollback();
                throw e;
            } finally {
                if (session != null) {
                    session.close();
                }
            }
        }

Единственное, что отличается, это конфигурация провайдера соединений.

Поставщик подключений диспетчера драйверов

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

01
02
03
04
05
06
07
08
09
10
11
12
13
@Override
protected Properties getProperties() {
    Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
        //driver settings
        properties.put("hibernate.connection.driver_class", "org.hsqldb.jdbcDriver");
        properties.put("hibernate.connection.url", "jdbc:hsqldb:mem:test");
        properties.put("hibernate.connection.username", "sa");
        properties.put("hibernate.connection.password", "");
        //isolation level
        properties.setProperty("hibernate.connection.isolation", String.valueOf(Connection.TRANSACTION_SERIALIZABLE));
    return properties;
}

Тест генерирует следующий вывод:

1
2
WARN  [main]: o.h.e.j.c.i.DriverManagerConnectionProviderImpl - HHH000402: Using Hibernate built-in connection pool (not for production use!)
DEBUG [main]: c.v.h.m.l.t.TransactionIsolationDriverConnectionProviderTest - Transaction isolation level is SERIALIZABLE

Соединение JDBC, связанное с сеансом Hibernate, использует уровень изоляции транзакции SERIALIZABLE, поэтому конфигурация hibernate.connection.isolation работает для этого конкретного поставщика соединений.

C3P0-провайдер

Hibernate также предлагает встроенный C3P0-провайдер. Как и в предыдущем примере, нам нужно только предоставить параметры конфигурации драйвера и Hibernate создать пул соединений C3P0 от нашего имени.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Override
protected Properties getProperties() {
    Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
        //log settings
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.show_sql", "true");
        //driver settings
        properties.put("hibernate.connection.driver_class", "org.hsqldb.jdbcDriver");
        properties.put("hibernate.connection.url", "jdbc:hsqldb:mem:test");
        properties.put("hibernate.connection.username", "sa");
        properties.put("hibernate.connection.password", "");
        //c3p0 settings
        properties.put("hibernate.c3p0.min_size", 1);
        properties.put("hibernate.c3p0.max_size", 5);
        //isolation level
        properties.setProperty("hibernate.connection.isolation", String.valueOf(Connection.TRANSACTION_SERIALIZABLE));
    return properties;
}

Тест генерирует следующий вывод:

1
2
3
4
5
Dec 19, 2014 11:02:56 PM com.mchange.v2.log.MLog <clinit>
INFO: MLog clients using java 1.4+ standard logging.
Dec 19, 2014 11:02:56 PM com.mchange.v2.c3p0.C3P0Registry banner
INFO: Initializing c3p0-0.9.2.1 [built 20-March-2013 10:47:27 +0000; debug? true; trace: 10]
DEBUG [main]: c.v.h.m.l.t.TransactionIsolationInternalC3P0ConnectionProviderTest - Transaction isolation level is SERIALIZABLE

Таким образом, конфигурация hibernate.connection.isolation работает и для внутреннего поставщика соединений C3P0.

Поставщик подключения к источникам данных

Hibernate не заставляет вас использовать определенный механизм провайдера соединений. Вы можете просто предоставить DataSource, и Hibernate будет использовать его всякий раз, когда запрашивается новое соединение. На этот раз мы создадим полноценный объект DataSource и передадим его через конфигурацию hibernate.connection.datasource .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected Properties getProperties() {
    Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
        //log settings
        properties.put("hibernate.hbm2ddl.auto", "update");
        //data source settings
        properties.put("hibernate.connection.datasource", newDataSource());
        //isolation level
        properties.setProperty("hibernate.connection.isolation", String.valueOf(Connection.TRANSACTION_SERIALIZABLE));
    return properties;
}
 
protected ProxyDataSource newDataSource() {
        JDBCDataSource actualDataSource = new JDBCDataSource();
        actualDataSource.setUrl("jdbc:hsqldb:mem:test");
        actualDataSource.setUser("sa");
        actualDataSource.setPassword("");
        ProxyDataSource proxyDataSource = new ProxyDataSource();
        proxyDataSource.setDataSource(actualDataSource);
        proxyDataSource.setListener(new SLF4JQueryLoggingListener());
        return proxyDataSource;
}

Тест генерирует следующий вывод:

1
DEBUG [main]: c.v.h.m.l.t.TransactionIsolationExternalDataSourceConnectionProviderTest - Transaction isolation level is READ_COMMITTED

На этот раз hibernate.connection.isolation , похоже, не принимается во внимание. Hibernate не переопределяет внешние источники данных, поэтому эти настройки в этом сценарии бесполезны.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
protected ProxyDataSource newDataSource() {
    JDBCDataSource actualDataSource = new JDBCDataSource();
    actualDataSource.setUrl("jdbc:hsqldb:mem:test");
    actualDataSource.setUser("sa");
    actualDataSource.setPassword("");
    Properties properties = new Properties();
    properties.setProperty("hsqldb.tx_level", "SERIALIZABLE");
    actualDataSource.setProperties(properties);
    ProxyDataSource proxyDataSource = new ProxyDataSource();
    proxyDataSource.setDataSource(actualDataSource);
    proxyDataSource.setListener(new SLF4JQueryLoggingListener());
    return proxyDataSource;
}

Генерация следующего вывода:

1
DEBUG [main]: c.v.h.m.l.t.TransactionIsolationExternalDataSourceExternalconfgiurationConnectionProviderTest - Transaction isolation level is SERIALIZABLE

Поддержка изоляции транзакций Java Enterprise

Hibernate имеет встроенный уровень абстракции API транзакций , изолирующий уровень доступа к данным от топологии управления транзакциями ( локальный ресурс или JTA). Хотя мы можем разработать приложение, используя только абстракцию транзакций Hibernate, гораздо чаще делегировать эту ответственность промежуточной технологии ( JEE или Spring ).

Java Enterprise Edition

JTA (спецификация Java Transaction API) определяет, как транзакции должны управляться сервером приложений, совместимым с JEE. На стороне клиента мы можем разграничить границы транзакции, используя аннотацию TransactionAttribute . Хотя у нас есть возможность выбрать правильный параметр распространения транзакции, мы не можем сделать то же самое для уровня изоляции.

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

весна

Spring @Transactional аннотация используется для определения границы транзакции. В отличие от JEE, эта аннотация позволяет нам настраивать:

  • уровень изоляции
  • политика отката типов исключений
  • распространение
  • только для чтения
  • Тайм-аут

Как я покажу позже в этой статье, настройки уровня изоляции легко доступны только для локальных транзакций ресурса . Поскольку JTA не поддерживает уровни изоляции на уровне транзакций, Spring предлагает IsolationLevelDataSourceRouter, чтобы преодолеть этот недостаток при использовании JTA DataSources сервера приложений.

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

Параметр уровня изоляции логической транзакции (например, @Transactional ) анализируется IsolationLevelDataSourceRouter, и поэтому запрос на получение соединения делегируется конкретной реализации DataSource, которая может обслуживать соединение JDBC с тем же параметром уровня изоляции транзакции.

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

Уровни изоляции в рамках транзакций Spring

Далее я собираюсь протестировать поддержку управления транзакциями Spring для локальных ресурсов и транзакций JTA.

Для этого я представлю транзакционный компонент Service Bean:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Service
public class StoreServiceImpl implements StoreService {
 
    protected final Logger LOGGER = LoggerFactory.getLogger(getClass());
 
    @PersistenceContext(unitName = "persistenceUnit")
    private EntityManager entityManager;
 
    @Override
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void purchase(Long productId) {       
        Session session = (Session) entityManager.getDelegate();
        session.doWork(new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                LOGGER.debug("Transaction isolation level is {}", Environment.isolationLevelToString(connection.getTransactionIsolation()));
            }
        });
    }
}

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

Миграция с локального ресурса на транзакции XA — это просто деталь конфигурации, оставляя фактический код бизнес-логики без изменений. Это было бы невозможно без дополнительного уровня абстракции управления транзакциями и сквозной поддержки AOP .

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

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

Сначала мы собираемся протестировать диспетчер транзакций JPA:

1
2
3
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

При звонке в нашу службу бизнес-логики мы получаем следующее:

1
DEBUG [main]: c.v.s.i.StoreServiceImpl - Transaction isolation level is SERIALIZABLE

Диспетчер транзакций JPA может использовать только один источник данных, поэтому он может выдавать только локальные транзакции ресурса . В таких сценариях диспетчер транзакций Spring может переопределить уровень изоляции источника данных по умолчанию (в нашем случае это READ COMMITTED).

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

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

Традиционно именно сервер приложений предприятия (например, Wildfly , WebLogic ) отвечал за обеспечение JTA-совместимого менеджера транзакций. В настоящее время существует большое разнообразие автономных менеджеров транзакций JTA:

В этом тесте мы собираемся использовать Bitronix:

1
2
3
4
5
6
7
8
<bean id="jtaTransactionManager" factory-method="getTransactionManager"
      class="bitronix.tm.TransactionManagerServices" depends-on="btmConfig, dataSource"
      destroy-method="shutdown"/>
 
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    <property name="transactionManager" ref="jtaTransactionManager"/>
    <property name="userTransaction" ref="jtaTransactionManager"/>
</bean>

При запуске предыдущего теста мы получаем следующее исключение:

1
org.springframework.transaction.InvalidIsolationLevelException: JtaTransactionManager does not support custom isolation levels by default - switch 'allowCustomIsolationLevels' to 'true'

Итак, давайте включим настройку пользовательского уровня изоляции и повторно запустим тест:

1
2
3
4
5
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    <property name="transactionManager" ref="jtaTransactionManager"/>
    <property name="userTransaction" ref="jtaTransactionManager"/>
    <property name="allowCustomIsolationLevels" value="true"/>
</bean>

Тест дает нам следующий вывод:

1
DEBUG [main]: c.v.s.i.StoreServiceImpl - Transaction isolation level is READ_COMMITTED

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

Для WebLogic Spring предлагает WebLogicJtaTransactionManager для устранения этого ограничения, как мы можем видеть в следующем фрагменте исходного кода Spring:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Specify isolation level, if any, through corresponding WebLogic transaction property.
if (this.weblogicTransactionManagerAvailable) {
    if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
        try {
            Transaction tx = getTransactionManager().getTransaction();
            Integer isolationLevel = definition.getIsolationLevel();
            /*
            weblogic.transaction.Transaction wtx = (weblogic.transaction.Transaction) tx;
            wtx.setProperty(ISOLATION_LEVEL_KEY, isolationLevel);
            */
            this.setPropertyMethod.invoke(tx, ISOLATION_LEVEL_KEY, isolationLevel);
        }
        catch (InvocationTargetException ex) {
            throw new TransactionSystemException(
                    "WebLogic's Transaction.setProperty(String, Serializable) method failed", ex.getTargetException());
        }
        catch (Exception ex) {
            throw new TransactionSystemException(
                    "Could not invoke WebLogic's Transaction.setProperty(String, Serializable) method", ex);
        }
    }
}
else {
    applyIsolationLevel(txObject, definition.getIsolationLevel());
}

Вывод

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

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