2. 什么是事务?
事务是一个包含一个或多个语句的原子操作。之所以称为原子操作,是因为事务中的所有语句要么全部成功(提交),要么全部失败(回滚),即“全有或全无”。ACID特性中的“A”就代表事务的原子性。
另一个关键点是:InnoDB引擎中所有语句都会成为事务,即使没有显式声明,也会隐式形成事务。当并发加入后,这个概念会变得更复杂。此时需要理解ACID中的“I”——隔离性。
理解隔离级别对于权衡性能与一致性至关重要。但在深入讨论隔离级别前,请记住:InnoDB中所有语句都是事务,都可以提交或回滚。如果没有显式声明事务,数据库会自动创建一个,并根据autocommit属性决定是否提交。
2.1. 隔离级别
本文以MySQL默认的可重复读隔离级别为例。它能保证同一事务内的读取一致性:第一次读取会建立快照(时间点),后续所有读取都与此快照一致。更多细节可参考MySQL官方文档。维护这种快照虽有一定开销,但能保证较高的一致性。
不同数据库可能有不同的隔离级别名称或选项,但大多类似。
3. 为什么使用事务及何时使用?
理解了事务的概念和特性后,我们来讨论只读事务。如前所述,InnoDB中所有语句都是事务,可能涉及锁定和快照等操作。但对于纯查询语句,事务协调相关的开销(如标记事务ID等内部结构)可能是不必要的。这时只读事务就派上用场了。
我们可以通过START TRANSACTION READ ONLY
语法显式声明只读事务。MySQL也能自动检测只读事务,但显式声明能触发更多优化。读密集型应用可以利用这些优化,节省数据库集群的资源消耗。
3.1. 应用层与数据库层
需要认识到,应用中的持久层可能包含多层抽象,每层职责不同。为简化说明,这些抽象最终会影响应用与数据库的交互方式。以Spring应用为例,这些层的作用包括:
- DAO层:作为业务逻辑与持久化细节的桥梁
- 事务抽象层:处理应用级事务复杂性(Begin/Commit/Rollback)
- JPA抽象层:提供厂商无关的标准API
- ORM框架:JPA的具体实现(如Hibernate)
- JDBC层:负责与数据库实际通信
关键点在于:这些因素都可能影响事务行为。但我们将重点讨论直接影响行为的属性组。通常这些属性可在全局或会话级别设置。属性列表很长,我们只讨论两个关键属性(你应该已经熟悉它们)。
3.2. 事务管理
JDBC驱动通过关闭autocommit属性启动事务。这相当于执行BEGIN TRANSACTION
语句,之后所有语句必须提交或回滚才能结束事务。
在全局级别设置此属性时,数据库会将所有请求视为手动事务,要求用户显式提交或回滚。但如果用户在会话级别覆盖此设置,全局设置将失效。因此许多驱动默认关闭此属性,确保行为一致且应用可控。
接下来,可使用transaction属性定义是否允许写操作。但有个陷阱:即使在只读事务中,仍可操作使用TEMPORARY
关键字创建的临时表。该属性同样有全局和会话作用域,但应用中通常在会话级别处理。
使用连接池时,由于连接复用特性,框架/库必须确保新事务开始前会话处于干净状态。为此可能执行一些清理语句。
我们知道读密集型应用可通过只读事务优化资源使用。但开发者常忽略:切换事务设置会导致额外的数据库往返通信,影响连接吞吐量。
在MySQL中,全局设置示例:
SET GLOBAL TRANSACTION READ WRITE;
SET autocommit = 0;
/* 事务操作 */
commit;
会话级别设置示例:
SET SESSION TRANSACTION READ ONLY;
SET autocommit = 1;
/* 事务操作 */
3.3. 实用技巧
对于单查询事务,启用autocommit可节省往返通信开销。如果这是应用中的主要场景,使用单独配置为只读且默认启用autocommit的数据源效果更佳。
当事务包含多个查询时,应使用显式只读事务。创建只读数据源也能避免读写事务切换,节省往返开销。但如果存在混合读写负载,管理额外数据源的复杂性可能得不偿失。
处理多语句事务时,还需考虑隔离级别对结果和性能的影响。为简化,示例中均使用默认隔离级别(可重复读)。
4. 实践应用
现在从应用角度,探讨如何处理这些属性及哪些层能控制此行为。实现方式因框架而异,以JPA和Spring为例,其他场景可触类旁通。
4.1. JPA
使用JPA/Hibernate定义只读事务:
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-unit");
EntityManager entityManager = entityManagerFactory.createEntityManager();
entityManager.unwrap(Session.class).setDefaultReadOnly(true);
entityManager.getTransaction().begin();
entityManager.find(Book.class, id);
entityManager.getTransaction().commit();
注意:JPA没有定义只读事务的标准方式,因此需要获取Hibernate的底层Session进行设置。
4.2. JPA+Spring
使用Spring事务管理会更简洁:
@Transactional(readOnly = true)
public Book getBookById(long id) {
return entityManagerFactory.createEntityManager().find(Book.class, id);
}
Spring会负责开启、关闭事务并设置事务模式。使用Spring Data JPA时,已内置此类配置。
Spring JPA仓库基类已将所有方法标记为只读事务。在类级别添加@Transactional
可覆盖方法行为。
也可在数据源配置中设置只读连接和autocommit属性。当应用仅需读取时,这能进一步提升性能:
@Bean
public DataSource readOnlyDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8");
config.setUsername("baeldung");
config.setPassword("baeldung");
config.setReadOnly(true);
config.setAutoCommit(true);
return new HikariDataSource(config);
}
但这仅适用于单查询为主的场景。使用Spring Data JPA时,还需禁用默认事务:
@Configuration
@EnableJpaRepositories(enableDefaultTransactions = false)
@EnableTransactionManagement
public class Config {
// 数据源及其他持久化Bean定义
}
此后需完全手动添加@Transactional(readOnly=true)
。不过大多数应用无需此配置,除非确定能带来显著收益。
4.3. 语句路由
更实际的场景是配置两个数据源:写数据源和只读数据源,然后在组件级别指定使用哪个数据源。这种方式能更高效处理读连接,避免确保会话状态所需的额外命令。
实现方式多样,首先创建路由数据源类:
public class RoutingDS extends AbstractRoutingDataSource {
public RoutingDS(DataSource writer, DataSource reader) {
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put("writer", writer);
dataSources.put("reader", reader);
setTargetDataSources(dataSources);
}
@Override
protected Object determineCurrentLookupKey() {
return ReadOnlyContext.isReadOnly() ? "reader" : "writer";
}
}
关于路由数据源的细节很多。简单说,此类会根据应用请求返回合适数据源。我们使用ReadOnlyContext
类在运行时维护数据源上下文:
public class ReadOnlyContext {
private static final ThreadLocal<AtomicInteger> READ_ONLY_LEVEL = ThreadLocal.withInitial(() -> new AtomicInteger(0));
// 默认构造函数
public static boolean isReadOnly() {
return READ_ONLY_LEVEL.get()
.get() > 0;
}
public static void enter() {
READ_ONLY_LEVEL.get()
.incrementAndGet();
}
public static void exit() {
READ_ONLY_LEVEL.get()
.decrementAndGet();
}
}
接下来定义数据源并注册到Spring上下文:
// 前述注解
public Config {
// 其他Bean...
@Bean
public DataSource routingDataSource() {
return new RoutingDS(
dataSource(false, false),
dataSource(true, true)
);
}
private DataSource dataSource(boolean readOnly, boolean isAutoCommit) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost/baeldung?useUnicode=true&characterEncoding=UTF-8");
config.setUsername("baeldung");
config.setPassword("baeldung");
config.setReadOnly(readOnly);
config.setAutoCommit(isAutoCommit);
return new HikariDataSource(config);
}
// 其他Bean...
}
最后,创建注解告知Spring何时使用只读上下文。定义@ReaderDS
注解:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface ReaderDS {
}
使用AOP将组件执行包装到上下文中:
@Aspect
@Component
public class ReadOnlyInterception {
@Around("@annotation(com.baeldung.readonlytransactions.mysql.spring.ReaderDS)")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
try {
ReadOnlyContext.enter();
return joinPoint.proceed();
} finally {
ReadOnlyContext.exit();
}
}
}
通常建议在尽可能高的层级添加注解。为简化示例,我们在仓库层添加(组件中仅包含单查询):
public interface BookRepository extends JpaRepository<BookEntity, Long> {
@ReaderDS
@Query("Select t from BookEntity t where t.id = ?1")
BookEntity get(Long id);
}
可见,这种配置通过完整只读事务和避免会话上下文切换,能更高效处理只读操作,从而显著提升应用吞吐量和响应速度。
5. 总结
本文探讨了只读事务及其优势,理解了MySQL InnoDB引擎的处理机制,以及配置影响应用事务的主要属性。此外,还讨论了通过专用资源(如独立数据源)实现进一步优化的可能性。