关于 HibernateOptimisticLockingFailureException: Row was updated or deleted by another transaction 的问题

14 篇文章 0 订阅

异常

一个在线系统报告下面的异常:

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. 要么将工作放到异步队列中去串行地做

例如通过使用消息队列,然后一条条地串行处理。

探索

重现乐观锁的一个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
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值