mysql-hibernate-spring 开发的事务机制详解

本贴最后更新于 2980 天前,其中的信息可能已经时移世改

数据库事务综述

什么是事务(Transaction)

事务:同生共死, 指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)--也就是由多个 sql 语句组成,必须作为一个整体执行, 这些 sql 语句作为一个整体一起向系统提交,要么都执行、要么都不执行 .

事务特性

事务具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性, 在实际的应用环境中, 对数据的操作需要满足这四个特性, 来保证数据的完备.

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
  • 持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。

数据库系统使用日志来保证事务的原子性, 一致性和持久性, 使用锁机制来实现 数据库的隔离性.

在实际分布式服务的环境中, 由于并行, 多个数据库客户端 可能会同时进行, 这样就容易出现隔离性的问题, 针对这种情况, 可以在事务中显式地为数据加锁, 但加锁会影响数据库的并发性能, 为了权衡数据库的并发性能和隔离性, 数据库系统提供了 4 种隔离级别供用户选择.

事务的隔离级别

ANSI/ISO SQL 定义的标准隔离级别从高到低依次为:可串行化(Serializable)、可重复读(Repeatable reads)、提交读(Read committed)、未提交读(Read uncommitted)。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
  • 提交读(Read Committed):被读取的数据可以被其他事务修改。这样就可能导致不可重复读。也就是说,事务的读取数据的时候获取读锁,但是读完之后立即释放(不需要等到事务结束),而写锁则是事务提交之后才释放。释放读锁之后,就可能被其他事物修改数据。Oracle 等多数数据库默认都是该级别 (不重复读)
  • 可重复读(Repeated Read):所有被 Select 获取的数据都不能被修改,这样就可以避免一个事务前后读取数据不一致的情况。但是却没有办法控制幻读,因为这个时候其他事务不能更改所选的数据,但是可以增加数据,因为前一个事务没有范围锁。Mysql 的 InnoDB 引擎默认隔离级别。
  • 可串行化(Serializable):所有事务都一个接一个地串行执行,这样可以避免幻读(phantom reads),每次读都需要获得表级共享锁,读写相互都会阻塞。

由于事务的隔离性允许多个事务同时处理同一数据,所以,在多个事务并发操作的过程中,如果控制不好隔离级别,就有可能产生脏读、不可重复读或者幻读等现象。因此在操作数据的过程中需要合理利用数据库锁机制或者多版本并发控制机制获取更高的隔离等级,但是,随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。

数据库的锁机制

  • 共享锁(读锁、S 锁):如果事务 T 对数据 A 加上共享锁后,则其他事务只能对 A 再加共享锁,不能加排他锁,直到已释放所有共享锁。获准共享锁的事务只能读数据,不能修改数据。
  • 排他锁(排它锁、独占锁、写锁、X 锁):如果事务 T 对数据 A 加上排他锁后,则其他事务不能再对 A 加任任何类型的锁,直到在事务的末尾将资源上的锁释放为止。获准排他锁的事务既能读数据,又能修改数据。

上面提到的共享锁、排它锁相互都是有兼容/互斥关系的,可以用一个兼容性矩阵表示(y 表示兼容,n 表示不兼容):

X S
X n n
S n y

数据库管理系统中的并发控制

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。下面举例说明并发操作带来的数据不一致性问题:

现有两处火车票售票点,同时读取某一趟列车车票数据库中车票余额为 X。两处售票点同时卖出一张车票,同时修改余额为 X -1写回数据库,这样就造成了实际卖出两张火车票而数据库中的记录却只少了一张。

产生这种情况的原因是因为两个事务读入同一数据并同时修改,其中一个事务提交的结果破坏了另一个事务提交的结果,导致其数据的修改被丢失,破坏了事务的隔离性。并发控制要解决的就是这类问题.

虽然将事务串形化可以保证数据在多事务并发处理下不存在数据不一致的问题,但串行执行使得数据库的处理性能大幅度地下降,常常是你接受不了的。所以,一般来说,数据库的隔离级别都会设置为 read committed(只能读取其他事务已提交的数据),然后由应用程序使用乐观锁/悲观锁来弥补数据不一致的问题。

乐观锁

虽然名字中带“锁”,但是乐观锁并不锁住任何东西,而是在提交事务时检查自己上次读取这条记录后,是否有其他事务修改了这条记录,如果没有则提交,如果被修改了则回滚。如果并发的可能性并不大,那么锁定策略带来的性能消耗是非常小的。

常见实现方式:在数据表增加 version(或者时间戳)字段,每次事务开始时将取出 version 字段值,而后在更新数据的同时 version 增加 1(如:update xxx set data=#{data},version=version+1 where version=#{version}),如没有数据被更新,那么说明数据由其它的事务进行了更新,此时就可以判断当前事务所操作的历史快照数据。

悲观锁

和乐观锁相比,悲观锁则是一把真正的锁了,它通过 SQL 语句“select for update”锁住 select 出的那批数据,这时如果其他事务来更新这批数据时会等待。 总的来说,悲观锁相对乐观锁更安全一些,但是开销也更大,甚至可能出现数据库死锁的情况,建议只在乐观锁无法工作时才使用。

也有一种思路是在 表中添加一个 LOCK 字段, true 表示锁住, false 表示空闲, 查询时先查询记录是否空闲, 如果是锁住的, 则等待, 直到锁撤销. 如果记录处在空闲状态, 则把 LOCK 字段置为 true, 表示记录被锁住, 开始更新, 然后再将 LOCK 字段置为 false, 接触锁定.

但是我认为这种 LOCK 字段的思路是不安全的, 很可能在 客户端 A 获取空闲记录, 然后锁住记录前, 客户端 B 同时获取了记录, 出现多个客户端同时获取到锁的情况. 这样同样会出现隔离性的问题.

java 的事务支持

JDBC 事务

JDBC 只支持本地事务, 事务局限在当前事务资源内, 在 JDBC API 中, 可以通过 java.sql.Connection 类控制事务, 提供了如下控制事务的方法

  • setAutoCommit(boolean autoCommit) // 设置是否自动提交事务
  • commit() // 提交事务
  • rollback() // 撤销事务

对于新建的 Connection 实例, 默认采用自动提交事务方式, 可以通过 setAutoCommit 设置手动提交, 这样就可以把多条 sql 作为一个事务, 在操作完成后调用 commit(), 如果出现异常, 就捕获异常, 然后 手动调用 rollback()

JTA 事务

JTA 事务支持多数据源的分布式事务, 要理解 JTA 的实现原理首先需要了解其架构:它包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分.我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器则承担着所有事务参与单元的协调与控制。 根据所面向对象的不同,我们可以将 JTA 的事务管理器和资源管理器理解为两个方面:面向开发人员的使用接口(事务管理器)和面向服务提供商的实现接口(资源管理器)。

其中开发接口的主要部分即为上述示例中引用的 UserTransaction 对象,开发人员通过此接口在信息系统中实现分布式事务;而实现接口则用来规范提供商(如数据库连接提供商)所提供的事务服务,它约定了事务的资源管理功能,使得 JTA 可以在异构事务资源之间执行协同沟通。

面向开发人员的接口为 UserTransaction (使用方法如上例所示),开发人员通常只使用此接口实现 JTA 事务管理,其定义了如下的方法:

  • begin()- 开始一个分布式事务,(在后台 TransactionManager 会创建一个 Transaction 事务对象并把此对象通过 ThreadLocale 关联到当前线程上 )
  • commit()- 提交事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务提交)
  • rollback()- 回滚事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所代表的事务回滚)
  • getStatus()- 返回关联到当前线程的分布式事务的状态 (Status 对象里边定义了所有的事务状态,感兴趣的读者可以参考 API 文档 )
  • setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚

在使用 JTA 事务时, 开发者主要是和 需要先获取 UserTransaction 事务对象, 用来管理 JTA 事务, 使用过程类似 JDBC 的 Connection.

hibernate 的事务和并发支持

Session

hibernate 通过 Session 对象来处理所有的持久化操作, 对象只有处在 Session 的管理下才能持久化和开启事务. Session 通过一系列方法来管理持久化对象的状态.

对象状态转换

  • 临时状态/瞬态(transient): 刚刚 new,未被持久化,不存于 Session 缓存
  • 持久化状态(persistent): 已经被持久化,并且加入 Session 缓存
  • 删除状态(removed): 不在处理 Session 缓存中并且 Session 计划从数据库删除
  • 游离状态(detached): 持久化转化而来,不存于 Session 缓存,数据库有对应记录

刷新缓存

清理缓存又叫刷新缓存, 将缓存中的数据和数据库中的同步.

时机

  • Transaction 的 commit()方法
  • 执行查询操作时, 如果隔离级别为 readCommited 及以上, 缓存中持久化对象属性已经变化, 实施同步
  • 调用 Session 的 flush()方法

策略

session.setFlushMode(FlushMode.COMMIT)
各种查询方法 commit() flush()
FlushMode.NEVER 不清理 不清理 清理
FlushMode.MANUAL 不清理 不清理 清理
FlushMode.COMMIT 不清理 清理 清理
FlusMode.AUTO 当隔离级别不低于 read commited, 清理 清理 清理
FlusMode.ALWAYS 清理 清理 清理

/**
	 * The {@link Session} is never flushed unless {@link Session#flush}
	 * is explicitly called by the application. This mode is very
	 * efficient for read only transactions.
	 *
	 * @deprecated use {@link #MANUAL} instead.
	 */
	@Deprecated
	NEVER ( 0 ),

	/**
	 * The {@link Session} is only ever flushed when {@link Session#flush}
	 * is explicitly called by the application. This mode is very
	 * efficient for read only transactions.
	 */
	MANUAL( 0 ),

	/**
	 * The {@link Session} is flushed when {@link Transaction#commit}
	 * is called.
	 */
	COMMIT(5 ),

	/**
	 * The {@link Session} is sometimes flushed before query execution
	 * in order to ensure that queries never return stale state. This
	 * is the default flush mode.
	 */
	AUTO(10 ),

	/**
	 * The {@link Session} is flushed before every query. This is
	 * almost always unnecessary and inefficient.
	 */
	ALWAYS(20 );
	

从 session 中清除对象与清理缓存

  • 清楚对象是使对象进入游离态(脱管), 将对象从 session 的缓存中清除掉, 不再受 session 的管理,
  • 清理缓存是将缓存中的数据与数据库中的数据对比, 将更新的部分持久化到数据库中, 同时更新缓存, 由于大多数时候清理缓存后会直接调用 session.close() 关闭 session, 所以会接下会清除缓存.

JDBC 事务 和 JTA 事务

hibernate 封装了 JDBC API 和 JTA API 事务, 对于使用 Hibernate 的应用, 可以使用 hibernate API 轻松地使用 JDBC 事务 和 JTA 事务.

悲观锁

hibernate 提供了对 数据库悲观锁的良好支持. 通过 Hibernate 获取事例化对象时, 可以通过 LockMode 类指定锁模式.

  • 默认是 LockMode.NONE, 先从缓存中加载持久化对象, 如果缓存中不存在, 就通过 select 语句从数据库中加载.
  • 可以使用 LockMode.UPGRADE 获取持久化对象, 这时候 Hibernate 会默认执行 select ... for update, 即悲观锁, 如果数据库不支持 悲观锁的, 就执行普通语句.

乐观锁

hibernate 提供了对悲观锁的良好支持, 只需要在实体对象中增加一个 version 标识, 表示版本信息就可以使用 hibernate 的悲观锁实现.

当 hibernate 更新一个对象时, 会使用 session 中缓存对象的 id 和 version 去数据库中匹配对应的对象, 如果匹配不到, 会抛出异常, 说明记录已经被其他的客户端修改, 这个时候用户可以手动处理异常, 一般是立即 rollback 撤销事务.

spring 的事务和并发支持

Propagation

  • PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
  • PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。

Isolation Level(事务隔离等级):

  • Serializable:最严格的级别,事务串行执行,资源消耗最大;
  • REPEATABLE READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
  • READ COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
  • Read Uncommitted:保证了读取过程中不会读取到非法数据。隔离级别在于处理多事务的并发问题。

spring 的事务隔离等级是对数据库隔离等级的支持, 可以在使用 @Trasactional 注解事指定 Isolation Level 动态改变该事务对应的隔离级别, 实质上是在操纵数据时是否加上共享锁, 独占锁.

readOnly

事务属性中的 readOnly 标志表示对应的事务应该被最优化为只读事务, 只读事务不同的数据库有一些对应的优化, 现在说两点特别需要注意的

  • 数据库默认情况下保证了 SQL 语句级别的读一致性,即在该条 SQL 语句执行期间,它只会看到执行前点的数据状态,而不会看到执行期间数据被其他 SQL 改变的状态, 只读查询(read-only transaction)则保证了事务级别的读一致性,即在该事务范围内执行的多条 SQL 都只会看到执行前点的数据状态,而不会看到事务期间的任何被其他 SQL 改变的状态。
  • 当 spring 和 其他 ORM 框架组合时, 会动态使用其他 ORM 框架的只读策略.
    • 在 JDBC 中,指定只读事务的办法为: connection.setReadOnly(true);
    • 在 Hibernate 中,指定只读事务的办法为: session.setFlushMode(FlushMode.NEVER);此时,Hibernate 也会为只读事务提供 Session 方面的一些优化手段, 比如事务读一致性, 不清理缓存等等.

Timeout

在事务属性中还有定义“timeout”值的选项,指定事务超时时间, 在 Transaction timeout 设置生效的情况下,发生超时后,框架将主动回滚当前事务并抛出.

Rollback 设置回滚事务属性

默认情况下只有未检查异常(RuntimeException 和 Error 类型的异常)会导致事务回滚. 而受检查异常不会. 事务的回滚规则可以通过 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来定义. 这两个属性被声明为 Class[] 类型的, 因此可以为这两个属性指定多个异常类.

  • rollbackFor: 遇到时必须进行回滚
  • noRollbackFor: 一组异常类,遇到时必须不回滚

一些巨坑

OpenSessionInViewFilter

Spring 为我们提供了一个叫做 OpenSessionInViewFilter 的过滤器,他是标准的 Servlet Filter 所以我们把它按照规范配置到 web.xml 中方可使用。使用中我们必须配合使用 Spring 的 HibernateDaoSupport 或者选择 LocalSessionFactoryBean 来进行开发, 从中由 Spring 来控制 Hibernate 的 Session 在请求来的时候开启,走的时候关闭,保证了我们访问数据对象时的稳定性。 下面是 OpenSessionInViewFilter 获取 session 部分的源码.

		try {
			Session session = sessionFactory.openSession();
			session.setFlushMode(FlushMode.MANUAL);
			return session;
		}

spring 会在请求过来的时候新建 Session 对象, 然后设置 FlushMode, 再将对象放到 threadLocal 里面, 同时只在请求结束的时候, 关闭 Session. 如果执行过程中遇到事务, spring 会判断是否是 只读事务, 如果不是只读事务, 就会将 MANUAL 用 AUTO 替换掉, 来支持写入。

  • 一般我们使用 getCurrentSession 来获取 Session, 然后 Session 会在事务结束后关闭. 这种情况下对象在事务(Transaction) 和 Session 中的边界是一致的, 在事务结束后, 在针对业务 A 处理的事务结束后, SessionA 就会关闭, 对象 A 会被清除.
  • 使用 OpenSessionInViewFilter 后, Session 会在整个请求线程内有效, Session 的边界横跨多个事务, 在事务 A 结束时, Session 并不会关闭, 对象依旧在 Session 中, 依旧有再次被同步到数据库的可能. 这时候如果接下来有事务 B, 而且事务 B 不是只读事务(readOnly), Session 会在 事务 B commit() 后清理缓存, 这时候对象 A 仍旧会和数据库中的记录比对, 如果在事务 A 结束后, 修改了 对象 A 的数据, 在事务B中commit()的时候会自动将对象A 同步到数据库中.

最佳实践

Javaweb-本地事务项目

  • 设置数据库默认的隔离级别为 Read Commited
  • 使用 spring hibernate 整合
  • 使用 OpenSessionInViewFilter
  • 根据实际使用 @Transactional 注解, 默认 指定传播属性为 REQUIRED
  • 默认使用只读事务
  • 使用 Hibernate version 的乐观锁实现来保持同步性, 或者使用其他同步手段实现分布式锁, 如利用 redis 的 getset 实现分布式锁.

参考

文章

  • 精通 hibernate : Java 对象持久化技术详解.(Mastering Hibernate. Let java objects hibernate in the relational database)
  • Spring in Action
  • Java EE 企业应用实战

写在最后

本文大部分都是摘抄自以上参考文章或书中内容, 只有少部分内容是作者自己的思考和经验, 作者主要做的是一个搬运工的角色, 将所有相关知识点拼凑成一个整体.

本文是作者初学事务时对事务的一个梳理, 肯定有一些不够准确, 不够完整的地方,也只适合数据库事务入门者通过本文对事务有一个完整的认识, 真正的具体的知识精髓和细节, 请大家去查看相关的书籍, 同时也欢迎大家对该文章补充和指正

可能有部分摘抄的内容对应的参考文章有遗漏, 实在是由于参考太多, 找不到来源了, 如果哪位朋友发现了, 请联系我的邮箱 wangyiraoxiang@163.com 我会在参考资料处补上

如果有哪位朋友觉得文章的哪段摘抄冒犯了您, 请联系我的邮箱, 我会尽快删掉相关部分并联系您表达歉意

  • 事务
    23 引用 • 21 回帖 • 1 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    690 引用 • 535 回帖
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 709 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...