介绍

基于ACID事务属性的关系数据库强一致性模型。 在本文中,我们将阐明对资源本地事务和JTA事务使用不同的事务隔离级别和各种配置模式的背后原因。

隔离和一致性

在关系数据库系统中,原子性和持久性是严格的属性,而一致性和隔离性或多或少是可配置的。 我们甚至不能将一致性与隔离性分开,因为这两个属性始终是相关的。

隔离级别越低,系统获得的一致性越差。 从最小到最一致,有四个隔离级别:

  • 读未提交
  • READ COMMITTED(防止脏读)
  • 可重复读取(防止脏和不可重复读取)
  • 可序列化(防止脏的,不可重复的读取和幻像读取)

尽管最一致的SERIALIZABLE隔离级别是最安全的选择,但大多数数据库默认改为READ COMMITTED。 根据阿姆达尔定律 ,为了容纳更多的并发事务,我们必须减少数据处理的串行部分。 锁获取间隔越短,数据库可以处理的请求越多。

隔离度

如我们先前所展示的, 应用程序级可重复读取与乐观锁定机制配对,对于防止长时间对话中的更新丢失非常方便。

在高度并发的环境中,乐观锁定可能会导致高事务失败率。 与其他任何排队机制一样,悲观锁定在提供足够的锁定获取时间间隔时可能会容纳更多事务。

数据库和隔离级别

除MySQL(使用REPEATABLE_READ)外,大多数关系数据库系统的默认隔离级别为READ_COMMITTED。 所有数据库都允许您设置默认的事务隔离级别。

通常,数据库在多个应用程序之间共享,并且每个应用程序都有其自己的特定交易要求。 对于大多数事务,READ_COMMITTED隔离级别是最佳选择,我们仅应针对特定业务案例覆盖它。

这种策略被证明是非常有效的,它使我们对所有SQL事务的子集具有更严格的隔离级别。

数据源隔离级别

JDBC Connection对象允许我们为在该特定连接上发出的所有事务设置隔离级别。 建立新的数据库连接是一个资源消耗过程,因此大多数应用程序都使用连接池 DataSource 。 连接池数据源还可以设置默认的事务隔离级别:

•  DBCP

•  DBCP2

•  光ikaCP

•  骨CP

•  Bitronix交易管理器

与全局数据库隔离级别设置相比,DataSource级别的事务隔离配置更加方便。 每个应用程序可以设置自己的特定并发控制级别。

我们甚至可以定义多个数据源,每个数据源都有一个定义的隔离级别。 这样,我们可以动态选择特定的隔离级别的JDBC连接。

休眠隔离级别

因为它必须同时支持资源本地事务和JTA事务,所以Hibernate提供了一种非常灵活的连接提供程序机制。

JTA事务需要XAConnection,并且JTA事务管理器负责提供XA兼容连接。

资源本地事务可以使用资源本地 DataSource ,在这种情况下,Hibernate提供了多个连接提供程序选项:

  • 驱动程序管理器连接提供程序(不合并连接,因此仅用于简单的测试方案)
  • C3P0连接提供程序(委派连接以获取对内部C3P0连接池数据源的调用)
  • 数据源连接提供程序(委派连接以获取对外部数据源的调用。

Hibernate提供了一个称为hibernate.connection.isolation的事务隔离级别配置,因此我们将检查所有上述连接提供者在获得此特定设置后的行为。

为此,我们将要:

@Override
protected SessionFactory newSessionFactory() {
	Properties properties = getProperties();

	return new Configuration()
			.addProperties(properties)
			.addAnnotatedClass(SecurityId.class)
			.buildSessionFactory(
					new StandardServiceRegistryBuilder()
							.applySettings(properties)
							.build()
	);
}
@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();
            }
        }
    }

唯一不同的是连接提供程序配置。

驱动程序管理器连接提供程序

驱动程序管理器连接提供程序为配置的数据库驱动程序提供了基本的DataSource包装器。 您仅应将其用于测试方案,因为它不提供专业的连接池机制。

@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

Hibernate会话关联的JDBC连接正在使用SERIALIZABLE事务隔离级别,因此hibernate.connection.isolation配置适用于此特定的连接提供程序。

C3P0连接提供者

Hibernate还提供了一个内置的C3P0连接提供程序。 像前面的示例一样,我们只需要提供驱动程序配置设置,然后Hibernate代表我们实例化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 <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就会在请求新的Connection时使用它。 这次,我们将创建一个成熟的DataSource对象,并将其传递给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

这次似乎没有考虑hibernate.connection.isolation 。 Hibernate不会覆盖外部数据源,因此此设置在这种情况下是无用的。

如果使用外部数据源(例如,通过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

Java Enterprise事务隔离支持

Hibernate具有内置的Transaction API抽象层 ,可将数据访问层与事务管理拓扑( 资源本地或JTA)隔离开。 虽然我们只能使用Hibernate事务抽象来开发应用程序,但将这种责任委托给中间件技术( JEE或Spring )更为常见。

Java企业版

JTA(Java事务API规范)定义了应如何由符合JEE的应用服务器管理事务。 在客户端,我们可以使用TransactionAttribute批注来划分事务边界。 尽管我们可以选择正确的事务传播设置,但对于隔离级别我们不能这样做。

JTA不支持事务范围的隔离级别,因此我们必须诉诸于供应商特定的配置才能为XA DataSource提供特定的事务隔离设置。

弹簧

Spring @Transactional批注用于定义事务边界。 与JEE相对,此批注允许我们配置:

  • 隔离级别
  • 异常类型回滚策略
  • 传播
  • 只读
  • 超时

正如我将在本文稍后演示的那样,隔离级别设置仅可用于资源本地事务。 因为JTA不支持事务范围的隔离级别,所以Spring提供了IsolationLevelDataSourceRouter来克服使用应用程序服务器JTA DataSources时的这一缺点。

因为大多数DataSource实现只能采用默认的事务隔离级别,所以我们可以有多个这样的DataSource,每个为特定的事务隔离级别提供连接。

逻辑事务(例如@Transactional )隔离级别设置由IsolationLevelDataSourceRouter自省,因此将连接获取请求委托给特定的DataSource实现,该实现可以为具有相同事务隔离级别设置的JDBC连接提供服务。

因此,即使在JTA环境中,事务隔离路由器也可以提供独立于供应商的解决方案,以基于每个事务覆盖默认数据库隔离级别。

Spring事务范围的隔离级别

接下来,我将测试Spring事务管理对资源本地事务和JTA事务的支持。

为此,我将介绍一个事务性业务逻辑服务Bean:

@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事务管理器:

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

当调用我们的业务逻辑服务时,这是我们得到的:

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

JPA事务管理器只能使用一个数据源,因此它只能发出资源本地事务。 在这种情况下,Spring事务管理器能够覆盖默认的DataSource隔离级别(在本例中为READ COMMITTED)。

JTA交易经理

现在,让我们看看切换到JTA事务时会发生什么。 如前所述,Spring仅提供逻辑事务管理器,这意味着我们还必须提供物理JTA事务管理器。

传统上,企业应用服务器(例如Wildfly和WebLogic )负责提供符合JTA的事务管理器。 如今,还有各种各样的独立JTA事务管理器:


•  比特龙

•  Atomikos

•  红帽纳拉亚纳(RedHat Narayana)

在此测试中,我们将使用Bitronix:

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

运行先前的测试时,我们得到以下异常:

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

因此,让我们启用自定义隔离级别设置并重新运行测试:

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

该测试为我们提供了以下输出:

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

结论

事务管理绝对不是一件容易的事,并且有了所有可用的框架和抽象层,它确实变得比人们想象的要复杂。

因为数据完整性对于大多数业务应用程序非常重要,所以唯一的选择是掌握当前的项目数据层框架堆栈。

  • Hibernate和JPA可用的代码。