关于 HibernateOptimisticLockingFailureException: Row was updated or deleted by another transaction 的问题
异常
一个在线系统报告下面的异常:
2020-07-29 15:22:48.921 ERROR --- [io-8080-exec-67] vlog_api.UserController : controller 出现异常!
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Object of class [vlog.UserInvitation] with identifier [13]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [vlog.UserInvitation#13]
at org.springframework.orm.hibernate5.SessionFactoryUtils.convertHibernateAccessException(SessionFactoryUtils.java:284)
at org.springframework.orm.hibernate5.HibernateTransactionManager.convertHibernateAccessException(HibernateTransactionManager.java:802)
at org.springframework.orm.hibernate5.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:638)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:152)
at grails.gorm.transactions.GrailsTransactionTemplate.execute(GrailsTransactionTemplate.groovy:91)
at vlog.UserService.wxLogin(UserService.groovy)
at vlog_api.UserController.wxLogin(UserController.groovy:100)
at vlog_api.UserController.wxLogin(UserController.groovy)
at java.lang.invoke.VirtualHandle.invokeExact_thunkArchetype_L(VirtualHandle.java:130)
at java.lang.invoke.AsTypeHandle.invokeExact_thunkArchetype_X(AsTypeHandle.java:49)
at org.grails.core.DefaultGrailsControllerClass$MethodHandleInvoker.invoke(DefaultGrailsControllerClass.java:223)
at org.grails.core.DefaultGrailsControllerClass.invoke(DefaultGrailsControllerClass.java:188)
at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAdapter.groovy:90)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
主要信息是:
org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: Object of class [vlog.UserInvitation] with identifier [13]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [vlog.UserInvitation#13]
Hibernate 乐观锁失败异常的触发原因分析
当我们调用 userInvitation.save() 方法时,Hibernate 发出下面的 SQL 语句,有两条:
update user_invitation set version=?, date_created=?, last_updated=?, invitation_code=?, user_id=? where id=? and version=?
insert into user_invitation_user (user_invitation_invited_users_id, user_id) values (?, ?)
如果此时,version 字段已经被另外一个事务修改了,因为当前事务的隔离级别是 “read committed” 所以只要另外的事务提交了修改,那么 Hibernate 的这句 update 就会失败,触发 “乐观锁失败异常” HibernateOptimisticLockingFailureException。
出错的事务中,只要调用 save() 方法就会触发乐观锁检查机制,所以,我们需要在调用 save() 前检查一下 entity 是否 dirty 了?如果没有 dirty 就不要调用 save() 方法,这样就可以减少 save() 次数,避免乐观锁异常出现的次数。
另外,可以看到 Hibernate 在处理保存关联集合属性的时候,效率不高,因为多发出了一次 update 语句,而不是直接 insert 一条新记录,如果要做到只 insert 关联集合的记录,那么需要单独使用 HQL 来做了。
解决办法
0. 本场景下最好是用“通过检查 entity 是否 dirty 再调用 save()”的方法来减少异常次数
且如果保存失败,不要抛出异常,而是记录日志,跳过本次保存或者重试一次。
1. 将事务的隔离级别提高到 “串行化 Serializable”
对性能影响太大,对于并发非常小的场景才可以用。
2. 在抛出异常的时候捕获该异常
捕获乐观锁失败异常,重新读取被改写了的数据,然后执行保存。
3. 或者不用 hibernate 的domain对象以及关联属性
而是直接插入一条记录。
4. 要么将工作放到异步队列中去串行地做
例如通过使用消息队列,然后一条条地串行处理。
探索
- 这篇SO的情况有点相似,row-was-updated-or-deleted-by-another-transaction-or-unsaved-value-mapping-was
- hibernate-optimistic-locking-exception 说明了hibernate 乐观锁异常的发生情况。
- an-entity-modeling-strategy-for-scaling-optimistic-locking/ 说明了一个解决方案。即将一个Domain类分为多个,每一个子类负责一部分会被单独修改的属性,以此来解决非重叠属性修改时的乐观锁失败问题。但这个模式不能滥用,否则会造成关联表过多的问题。
重现乐观锁的一个Grails单元测试
为了验证对乐观锁失效原因的理解,我编写了下面的单元测试代码,成功重现了乐观锁失效场景。
class UserServiceSpec extends HibernateSpec implements ServiceUnitTest<UserService>, AutowiredTest {
@NotTransactional
def "测试 HibernateOptimisticLockingFailureException"() {
when:
def errors = null
// 因为Grails的HibernateSpec会自动开启一个事务,从setup方法一直到测试方法结束,且测试方法结束时会自动提交、回滚事务
// 所以这里我们需要创建新线程、打开一个新session、事务,避免出现因为事务未结束而造成的死锁情况
Thread t1 = new Thread({
User.withNewSession { session ->
User invitor, user1, user2
// 在一个独立的事务中先创建用户、用户邀请记录
User.withNewTransaction {
// 邀请者是销售
invitor = createUser()
invitor.sales = true
invitor.save(failOnError: true)
user1 = createUser().save(failOnError: true)
user2 = createUser().save(failOnError: true)
service.generateInvitationParams(invitor)
}
// 准备模拟异常事务
User.withNewTransaction {
UserInvitation userInvitation = UserInvitation.findByUser(invitor)
// 在新的事务中修改关联属性,应该不产生错误
User.withNewTransaction {
UserInvitation userInvitation2 = UserInvitation.findByUser(invitor)
userInvitation2.addToInvitedUsers(user2)
userInvitation2.save(failOnError: true)
}
userInvitation.addToInvitedUsers(user1)
// 这里应该有乐观锁错误
userInvitation.save()
errors = userInvitation.errors
}
}
})
t1.start()
t1.join()
then:
!transactionStatus.completed
println errors
errors
}
}