【问题标题】:JUnit testing a call to @transactional @Async method causes Lock wait timeout exceededJUnit 测试对@transactional @Async 方法的调用导致超出锁定等待超时
【发布时间】:2011-07-04 04:28:53
【问题描述】:

我正在尝试测试异步运行的服务方法 (@Async)。

这里是异步方法:

@Async
@Transactional(propagation=Propagation.SUPPORTS, isolation = Isolation.READ_UNCOMMITTED)
public Future<UserPrefs> checkLanguagePreference(long id) {

    UserPrefs prefs = prefsDao.retrieveUserPreferences(id);
    if(prefs == null || !StringUtils.hasLength(prefs.getLanguage())) {
        //Save a new sms-command object
        SmsBean command = SmsHelper.buildSmsCommand();
        if(! smsDao.checkSameCommandExists(id, command)) {

            smsDao.saveSms(id, new SmsBean[] {command}); //Will wait until Lock wait timeout
        }
    }
    return new AsyncResult<UserPrefs>(prefs);
}

这里是调用异步的测试方法:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location = "...")
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@TestExecutionListeners( {  DependencyInjectionTestExecutionListener.class,
  DirtiesContextTestExecutionListener.class,
  TransactionalTestExecutionListener.class })
public class MessagingServiceTest {

   @Before
   public void setUp() {        
     //Avant tout mettre tout les sms en lu 
     smsDao.deleteAllSms(1);
     sessionFactory.getCurrentSession().flush();

     //On vérifie bien qu'il n y a plus de sms
     List<SmsBean> list = smsDao.getNewSmsList(1);
     assertEquals(0,list.size());
   }

   @Test
   public void checkLanguagePreferenceTest() throws InterruptedException, ExecutionException {
     User user = (User) sessionFactory.getCurrentSession().load(User.class, new Long(1));//idUser = 1
     // We explicitly blank the preference from db
     prefsDao.saveLanguagePref(new UserPrefs("",user));

     Future<UserPrefs> prefs =  messagingService.checkLanguagePreference(user.getId()); 
     System.out.println("wait completion of async task");           
     prefs.get();
     System.out.println("Async task has finished");
   }
}

执行 prefs.get() 时,出现此错误:

原因:org.springframework.orm.hibernate3.HibernateJdbcException:Hibernate 数据访问时的 JDBC 异常:SQL 的 SQLException [插入 SmsBean(目标、消息、来源、sens、状态、USER_ID)值(?、?、? , ?, ?, ?)]; SQL 状态 [41000];错误代码[1205];无法插入:

引起:java.sql.SQLException: Lock wait timeout exceeded;尝试重启事务

    在 com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1075)
    在 com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3562)
    在 com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3494)
    在 com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1960)
    在 com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2114)
    在 com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2696)
    在 com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2105)
    在 com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2398)
    在 com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2316)
    在 com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2301)
    在 org.apache.commons.dbcp.DelegatingPreparedStatement.executeUpdate(DelegatingPreparedStatement.java:101)
    在 org.hibernate.id.IdentityGenerator$GetGeneratedKeysDelegate.executeAndExtract(IdentityGenerator.java:94)
    在 org.hibernate.id.insert.AbstractReturningDelegate.performInsert(AbstractReturningDelegate.java:57)
    ... 39 更多

出现这种情况是因为 setup 方法中的 smsDao.deleteAllSms 持有 sms 表的锁。

我怎样才能正确避免这种锁定超时并能够成功运行我的测试?

感谢您的帮助。

仅供参考,这是一些控制台输出:

调试 - 添加带有属性的事务方法“checkLanguagePreferenceTest”:PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; '' 调试 - 显式事务定义 [PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; ''] 找到测试上下文 [[TestContext@b76fa testClass = MessagingServiceTest, locations = array['file:src/main/resources/myapp-context.xml', 'file:src/main/resources/myapp-data.xml ', 'file:src/main/resources/myapp-services.xml'], testInstance = fr.myapp.service.MessagingServiceTest@b01d43, testMethod = checkLanguagePreferenceTest@MessagingServiceTest, testException = [null]]] 调试 - 为测试类 [class fr.myapp.service.MessagingServiceTest] 检索到 @TransactionConfiguration [@org.springframework.test.context.transaction.Tran sactionConfiguration(defaultRollback=false, transactionManager=txManager)] 调试 - 为类 [class fr.myapp.service.MessagingServiceTest] 检索到 TransactionConfigurationAttributes [[TransactionConfigurationAttributes@5f7d3f transactionManagerName = 'txManager', defaultRollback = false]] 调试 - 返回单例 bean 'txManager' 的缓存实例 调试 - 创建名称为 [checkLanguagePreferenceTest] 的新事务:PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED; '' 调试 - 为 Hibernate 事务打开新会话 [org.hibernate.impl.SessionImpl@666a53] 调试 - 准备 Hibernate 会话的 JDBC 连接 [org.hibernate.impl.SessionImpl@666a53] 调试 - 将 JDBC 连接 [org.apache.commons.dbcp.PoolableConnection@1bde3d2] 的隔离级别更改为 2 调试 - 将 Hibernate 事务公开为 JDBC 事务 [org.apache.commons.dbcp.PoolableConnection@1bde3d2] 调试 - 没有方法级 @Rollback 覆盖:使用默认回滚 [false] 测试上下文 [[TestContext@b76fa testClass = MessagingServiceTest, locations = array['file:src/main/resources/myapp-context.xml', 'file :src/main/resources/myapp-data.xml', 'file:src/main/resources/myapp-services.xml'], testInstance = fr.myapp.service.MessagingServiceTest@b01d43, testMethod = checkLanguagePreferenceTest@MessagingServiceTest, testException = [空]]] INFO - 开始事务(1):事务管理器 [org.springframework.orm.hibernate3.HibernateTransa ctionManager@17753a8];回滚 [假] 休眠:从 SmsBean 中删除 USER_ID=? 休眠:选择 user0_.id 作为 id3_1_,user0_.email 作为 email3_1_,user0_.login 作为 login3_1_,user0_.passwd 作为 passwd3_1_,smsbeans1_.USER_ID 作为 USER7_3_3_,smsbeans1_.id 作为 id3_,smsbeans1_.id 作为 id0_0_,smsbeans1_.destination 作为 destinat , smsbeans1_.message 作为 message0_0_, smsbeans1_.origin 作为 origin0_0_, smsbeans1_.sens 作为 sens0_0_, smsbeans1_.status 作为 status0_0_, smsbeans1_.USER_ID 作为 USER7_0_0_ 来自用户 user0_ 左外连接 SmsBean smsbeans1_ on user0_.id=smsbeans1_.USER_id =? 休眠:从用户 user0_ 中选择 user0_.id 作为 id3_,user0_.email 作为 email3_,user0_.login 作为 login3_,user0_.passwd 作为 passwd3_ 从用户 user0_ where user0_.login=? 休眠:从 user_prefs userprefs0_ where userprefs0_.USER_ID=? 等待异步任务完成 调试 - 返回单例 bean 'txManager' 的缓存实例 信息 - 检查语言偏好(1) 调试 - 打开休眠会话 调试 - 为新的 Hibernate 会话注册 Spring 事务同步 休眠:从 user_prefs userprefs0_ where userprefs0_.USER_ID=? 休眠:选择 user0_.id 作为 id3_1_,user0_.email 作为 email3_1_,user0_.login 作为 login3_1_,user0_.passwd 作为 passwd3_1_,smsbeans1_.USER_ID 作为 USER7_3_3_,smsbeans1_.id 作为 id3_,smsbeans1_.id 作为 id0_0_,smsbeans1_.destination 作为 destinat , smsbeans1_.message 作为 message0_0_, smsbeans1_.origin 作为 origin0_0_, smsbeans1_.sens 作为 sens0_0_, smsbeans1_.status 作为 status0_0_, smsbeans1_.USER_ID 作为 USER7_0_0_ 来自用户 user0_ 左外连接 SmsBean smsbeans1_ on user0_.id=smsbeans1_.USER_id =? INFO - 检查相同的短信命令是否已经存在 Hibernate: select * from smsbean S where S.USER_ID=?和 S.status=?和 S.message=? DEBUG - 在事务同步时刷新 Hibernate Session //这里死锁: Hibernate: 插入 SmsBean (destination, message, origin, sens, status, USER_ID) 值 (?, ?, ?, ?, ?, ?) 调试 - 关闭休眠会话 58799 [SimpleAsyncTaskExecutor-1] 警告 org.hibernate.util.JDBCExceptionReporter - SQL 错误:1205,SQLState:41000 58799 [SimpleAsyncTaskExecutor-1] 错误 org.hibernate.util.JDBCExceptionReporter - 超过锁定等待超时;尝试重启事务

已解决,但仅供参考,我之前在 MySQL 论坛上创建了一个线程,从 DBMS 的角度来看我为什么会出现这种死锁。这是链接(也很好解释):

http://forums.mysql.com/read.php?97,409237,409237#msg-409237

【问题讨论】:

    标签: java mysql hibernate spring junit


    【解决方案1】:

    如果您可以容忍测试的同步执行,您还可以配置一个使用 SyncTaskExecutor 而不是其线程对等点的测试上下文。

    换句话说:

    <bean id="myExecutor" class="org.springframework.core.task.SyncTaskExecutor"/>
    

    代替:

    <task:executor id="myExecutor" pool-size="5"/>
    

    然后一切都在同一个线程中运行,您不必处理由竞争事务引起的数据库锁定问题。

    【讨论】:

      【解决方案2】:

      由于您的测试被声明为@Transactional,因此您有一个大事务分散在setUp 方法和您的测试方法的执行上。此事务与异步操作中启动的另一个事务发生死锁(异步操作等待释放主事务获取的锁,主事务等待异步操作完成)。

      您可以通过将主事务分解为几个单独的事务来解决它:

      @Before
      @Transactional // separate transaction for setUp
      public void setUp() {        
          //Avant tout mettre tout les sms en lu 
          smsDao.deleteAllSms(1);
          sessionFactory.getCurrentSession().flush();
      
          //On vérifie bien qu'il n y a plus de sms
          List<SmsBean> list = smsDao.getNewSmsList(1);
          assertEquals(0,list.size());
      }
      
      @Test
      @Transactional(propagation = NEVER) // Disable main transaction
      public void checkLanguagePreferenceTest() throws InterruptedException, ExecutionException {
      
          // Programmatic transaction for test preparation
          User user = tx.execute(new TransactionCallback<User>() {
              public User doInTransaction(TransactionStatus status) {
                  User user = (User) sessionFactory.getCurrentSession().load(User.class, new Long(1));//idUser = 1
                  // We explicitly blank the preference from db
                  prefsDao.saveLanguagePref(new UserPrefs("",user));    
                  return user;
              }
          });
      
          Future<UserPrefs> prefs = messagingService.checkLanguagePreference(user.getId()); 
          System.out.println("wait completion of async task");            
          prefs.get();
          System.out.println("Async task has finished");
      }
      
      private TransactionTemplate tx;
      
      @Autowired
      public void setPtm(PlatformTransactionManager ptm) {
          tx = new TransactionTemplate(ptm);
      }
      

      【讨论】:

        猜你喜欢
        • 2016-12-16
        • 2011-01-07
        • 1970-01-01
        • 2022-09-29
        • 1970-01-01
        • 2019-11-20
        • 2017-02-01
        • 2010-10-27
        • 2021-10-15
        相关资源
        最近更新 更多