Рубрики
Без рубрики

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

Автор оригинала: Vlad Mihalcea.

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

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

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

  • ЧИТАТЬ НЕЗАФИКСИРОВАННЫМ
  • ЧТЕНИЕ ЗАФИКСИРОВАНО (защита от грязного чтения)
  • ПОВТОРЯЕМОЕ ЧТЕНИЕ (защита от грязного и неповторяемого чтения)
  • СЕРИАЛИЗУЕМЫЙ (защита от грязных, неповторяемых считываний и фантомных считываний)

Хотя наиболее согласованный уровень СЕРИАЛИЗУЕМОЙ изоляции был бы самым безопасным выбором, большинство баз данных по умолчанию вместо этого выполняют ЧТЕНИЕ С ФИКСАЦИЕЙ. Согласно закону Amdahl , для обеспечения большего количества одновременных транзакций мы должны сократить последовательную часть обработки наших данных. Чем короче интервал получения блокировки, тем больше запросов может обработать база данных.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. Создайте SessionFactory
  2. Откройте новый сеанс и проверьте уровень изоляции связанной транзакции подключения

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

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

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

@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;
}

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

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

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

Поставщик соединений C3P0

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

@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;
}

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

Dec 19, 2014 11:02:56 PM com.mchange.v2.log.MLog 
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 работает и для внутреннего поставщика C3P0connectionprovider.

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

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

@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;
}    

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

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

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

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

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

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;
}

Создание следующего вывода:

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

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

Java Enterprise Edition

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

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

Весна

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

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

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

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

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

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

Уровни изоляции в области транзакций Spring

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

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

@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:

    
        
    

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

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

JpaTransactionManager может использовать только один источник данных, поэтому он может выдавать только локальные транзакции ресурсов. В таких сценариях Spring transaction manager может переопределить уровень изоляции источника данных по умолчанию (который в нашем случае ФИКСИРУЕТСЯ для ЧТЕНИЯ).

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

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

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

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




    
    

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

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

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


    
    
    

Тест дает нам следующий результат:

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

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

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

// 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());
}

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

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

Код доступен для гибернации и JPA .