企业级 Java 应用中事务隔离级别入门

原文地址:http://java.dzone.com/articles/beginners-guide-transaction

引言

关系数据库的强一致性模型是基于 ACDI 这四个事务特性建立。
这篇文章中,我们将会讲解使用不用级别的事务隔离背后的原因,并且介绍事物隔离级别在 各种resource local 和 JTA 事务中的配置形式。

(译注:ACDI 是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这篇文章可以在阅读完本文之后参考阅读,也许更加容易理解。)

隔离性和一致性

在关系型数据库中,原子性和持久性是比较严格的特性,隔离性和一致性则可以或多或少的进行配置,而他们之间经常存在着联系,我们有时甚至不能把他们拆分成两个事务特性。隔离级别越低,事务系统的一致性就越弱,从最弱到最强的一致性,有如下四种隔离级别:

  • READ UNCOMMITTED
  • READ COMMITTED ( 防止脏读)
  • REPEATABLE READ (防止脏读和不可重复读)
  • SERIALIZABLE (防止脏读、不可重复读和幻读)

(译注:关于脏读、不可重复读和幻读这些概念,请看我从这篇文章中摘抄的解释。

归纳一下,以上提到了事务并发所引起的跟读取数据有关的问题,各用一句话来描述一下:

  • 脏读:事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。
  • 不可重复读:事务 A 读取了事务 B 已提交的更改数据。
  • 幻读:事务 A 读取了事务 B 已提交的新增数据。

尽管最强一致性的 SERIALIZABLE 隔离级别会是最安全的选择,但是大多数的数据库默认使用 READ COMMITTED 来代替。根据阿姆达尔定律,为了容纳更多的并发事务,就必须要降低数据处理的串行分数。获取锁的间隔越短,数据库所能处理的请求越多。

隔离级别

像前面说的那样,应用程序级别的 REPEATABLE READ 搭配乐观锁机制可以很方便的防止在长会话时丢失更新。但在高并发的情况下,乐观锁可能会导致事务的高失败率,而像其他的队列机制一样悲观锁,如果有足够的获取锁的时间间隔,可能会容纳更多的事务。

数据库和它的隔离级别

除了使用了 REPEATABLE_READ 隔离级别的 MySQL 数据库,其他大多数关系型数据库系统的默认隔离级别是 READ_COMMITTED,当然,所有的数据库系统都允许你设置想使用的事务隔离级别。当多个应用程序共享一个数据库,而每个应用都有自己具体的事务需求的时候,对大多数的事务采用 READ_COMMITTED 隔离级别会是最好的选择,我们只需对特定的业务案例重新设置。这个策略被证明非常有效,它允许我们对全部 SQL 事务的一个子集指定更加严格的隔离级别。

数据源的隔离级别

JDBC 的 Connection 对象允许我们对所有发生在特定 connection 的事务设置隔离级别,而建立一个新的数据库连接是消耗资源的过程,所以,大多数的应用程序使用连接池数据源,连接池数据源也能设置如下的默认隔离级别:

  • DBCP
  • DBCP2
  • HikariCP
  • BoneCP
  • Bitronix Transaction Manager

和数据库的全局隔离级别的设置比起来,数据源级别的事务隔离配置更加方便,每个应用程序都能设置自己特定的并发控制级别。甚至我们可以定义多个预先配置好隔离级别的数据源,这样我们就能动态的选择一个数据源来动态选择指定隔离级别的 JDBC Connection。

Hibernate 的隔离级别

因为 Hibernate 需要支持 resource localJTA 事务,它提供了一个灵活的 Connection 获取机制。

(译注:关于 resource local 和 JTA 的区别,看这个问题 和这个问题

JTA 事务需要使用 XAConnectionXAConnection 是 JTA 事务管理器提供的符合分布事务要求的 Connection。
Resource local 事务可以使用 reasource local 数据源,在这种情况下,Hibernate 提供了多种获取 Connection 的方式:

  • Driver Manager Connection Provider (doesn’t pool connections and therefore it’s only meant for simple testing scenarios)
  • C3P0 Connection Provider (delegating connection acquiring calls to an internal C3P0 connection pooling DataSource)
  • DataSource Connection Provider (delegating connection acquiring calls to an external DataSource.)

Hibernate 提供的事务隔离级别的配置叫做 hibernate.connection.isolation,我们来看看前面说的 connection 提供者在指定了具体的设置之后是怎么工作的。

实验开始, 1. 创建 Session Factory

1
2
3
4
5
6
7
8
9
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()
);
}
  1. 获取一个 session 并测试关联的 connection 的事务隔离级别
1
2
3
4
5
6
7
8
9
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();
}
}
}

connection provider 的配置是会造成差异的因素,下面来看具体的配置。

Driver Manager Connection Provider

Driver Manager Connection Provider 提供了一个对数据源驱动中 Connection provider 的简单包装,这种方式只能在测试环境中使用,因为它没有提供专业的连接池机制。

1
2
3
4
5
6
7
8
9
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

Hibernate Session 关联的 JDBC Connection 是使用 SERIALIZABLE 事务隔离级别,所以 hibernate.connection.isolation 是可以在这个 connection provider 上生效的。

C3P0 Connection Provider

Hibernate 同时也提供了内建的 C3P0 Connection provider,像上一个例子一样,我们只需要设置驱动配置,Hibernate 就会替我们实例化 C3P0 连接池。

1
2
3
4
5
6
7
8
9
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
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 上也是生效的。

DataSource Connection Provider

Hibernate 不会强迫你使用特定的 Connection provider 机制,你可以提供一个数据源,当有新的请求获取 Connection 的时候,Hibernate 就会使用它。现在我们来创建一个成熟的数据源对象,并且通过 hibernate.connection.datasource 的配置告诉 Hibernate。

1
2
3
4
5
6
7
8
9
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),那么你需要在外部数据源上设置事务的隔离级别。我们可以通过设置外部数据源来修复这个测试用例

1
2
3
4
5
6
7
8
9
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 EE 的事务隔离支持

Hibernate 有一个内建的事务 API 抽象层,用来隔离数据访问层和事务管理结构(resource local 和 JTA),我们可以在应用中单独使用 Hibernate 的事务抽象,但更常见的做法是将这个职责委托给中间件技术(Java EE 或者 Spring)。

Java Enterprise Edition

JTA(Java Transaction API specification)定义了 Java EE 兼容的应用服务器是怎么管理事务的,在应用端,我们可以使用 TransactionAttribute 这个注解来划分事务边界,这样我们有使用正确事务传播设置的选择,但是对事务的隔离级别却没有选择。

JTA 不支持事务范畴的隔离级别,因此我们不得不使用一个有着特定事务隔离配置的 XA 数据源作为“供应商”。

Spring

Spring 的注解 @Transactional用来定义事务的边界,和 J2EE 相反的是,这个注解允许如下配置:- isolation level- exception types rollback policy- propagation- read-only- timeout就将要展示的那样,这个事务隔离级别的设置其实只会在 Resource local 的事务中生效,因为 JTA 不支持事务级别的隔离。Spring 提供了 IsolationLevelDataSourceRouter 来克服使用应用服务器的 JTA 数据源时的这个缺点。因为大多数的数据源实现都只取默认的事务隔离级别,我们可以使用多个这样的数据源,每个专门为特定的事务隔离级别的 Connection 服务。逻辑事务(如 @Transactional)隔离级别的设置是由IsolationLevelDataSourceRouter 内省得到的。因此,即使在 JTA 环境下,事务隔离路由系统也能提供一个独立的覆盖数据源默认的事务隔离级别的方案。

Spring 事务范畴的隔离级别

接下来测试 Spring 事务管理对 resource local 和 JTA 事务的支持。因此,先创建一个有事务的业务逻辑类:

1
2
3
4
5
6
7
8
9
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 事务管理只是实际中 resource local 和 JTA 事务管理器的门面。实际业务代码不变的情况下,现在从 resource local 迁移到 XA 事务只是更改配置的事,但如果没有额外的事务管理抽象层和交叉切面 AOP 的支持,这个也不可能会实现。下面测试不同的事务管理器是如何支持事务范畴的隔离级别设置的。

JPA transaction manager

配置如下:

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

在这个配置下,Spring 事务管理是能够取代默认数据源的隔离级别的。

JTA transaction manager

现在来看看当我们切换到 JTA 事务的时候会发生什么。就像我前面说的,Spring 只提供了一个逻辑上的事务管理层,这意味着我们需要自己提供一个物理的 JTA 事务管理器。以前,创建一个兼容 JTA 的事务管理器是企业应用服务器(如 Wildfly,WebLogic)的责任。现在还有种类繁多的独立 JTA 事务管理器:- Bitronix- Atomikos- RedHat Narayana我们将使用 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

尽管经过了额外的配置,事务范畴上的隔离级别还是没有替代底层的数据源 Connection,因为这是 JTA 事务管理器默认的行为。对于 WebLogic 服务器,Spring 提供了 WebLogicJtaTransactionManager 来处理这个限制,我们可以看到 Spring 的源代码中有如下代码:

1
2
3
4
5
6
7
8
9
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());
}

结论

事务管理绝不是一件简单的事,尤其是当有这个多框架和抽象层,它正变得超人想象的复杂。因为数据完整对大多数的商业应用都很重要,你能做的就是掌握你当前数据层框架的技术栈。

(全文完)