iBatis锁机制导致的交易阻塞

前段时间生产上出现了数据库锁太多的告警,其中等待的锁短时间内达到了6000个,好在十分钟后锁很快解除,否则可能会有数据库宕机的风险。此问题的具体表现如下:

  1. 其中一台应用服务器在凌晨2点钟出现大量交易变慢,导致weblogic线程超时阻塞,进而导致服务重启1
  2. 其他服务器在2点钟某一个账务交易出现大量长交易,但其他账务交易、查询交易均正常;
  3. 数据库某个限额统计表在2点多出现大量的行锁,触发数据库告警。
  4. 数据库在2点10分恢复正常,行锁也跟着消失

根据数据库锁的信息,结合那段时间的交易流水,可以确定是由于某个大客户的同一帐号的转账导致的。进一步梳理了代码,没有发现死锁的问题;另外这个客户日间交易量更大,理论上不应该出现这种问题。

进一步分析出问题的应用服务器怀疑是由于iBatis的锁导致的(原来生产上也出现过由于IBatis导致的锁定问题),但在什么情况下会出现长时间的行锁呢?目前iBatis有三个相关参数:maxRequests、maxSessions、maxTransactions,目前生产上分别设置的是100,60, 30。但根据iBatis的文档看maxRequests的值建议是maxTransactions的是10倍。为了验证是否是由于此参数的问题,我们在非功能环境进行了测试。

1. maxSessions=90,数据库连接数60,只做查询:

为了看出问题我们去掉了索引使查询交易耗时边长。此测试发现当查询并发小于等于60时没问题,但大于60时会出现连接不够的错误。

2. maxSessions=90,数据库连接数100,只做查询:

发现查询并发数增大(超过数据库连接)并不会导致交易失败,但查询时长会边长。这说明ibatis自身有锁机制来进行等待。

3. maxSessions=90,数据库连接数100,同时做付款与查询:

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易85个并发。发现在启动查询交易一段时间后转账交易开始出现超时和失败,但查询交易无失败和超时,但交易时间有增加。

转账交易10个并发,在转账交易稳定后开始发送查询交易,查询交易75个并发。发现在启动查询交易一段时间后转账交易正常无失败和超时,查询交易亦无失败和超时。

这个测试我们为了看出对账务交易的影响对账务交易代码进行了修改,伪码如下:

Enquire agreementInfo and other info; (no transaction)
Start transaction #spring逻辑
	Get session  #ibatis处理逻辑
		Get Request #ibatis处理逻辑
			Do update account limit
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
	Sleep 1s #增加的代码,为了便于看出效果
	Get session #ibatis处理逻辑;在这里可能出现拿不到session而阻塞,但这时其实线程是有数据库连接的,因为transaction未断开,不应该出现阻塞
	    Get Request #ibatis处理逻辑
			Insert account transaction information  #预计账务流水
		Return Request #ibatis处理逻辑
	Return session #ibatis处理逻辑
End Transaction #spring逻辑

我们可以看出由于iBatis在每次数据库操作开始都会请求session,而在每次数据库执行完成都会释放session。如果拿不到锁就会阻塞等待。这在仅仅是查询时没问题,但是在更新等事务中就会有问题,会导致数据库行锁,而且会影响到这条记录相关的所有交易。

后来我们仔细阅读了iBatis的代码,包括spring对于iBatis处理封装的逻辑,发现其实:maxRequests、maxSessions、maxTransactions这三个参数只有maxSession是有用的,因为maxRequest都是在session中进行获取,所以其取值不会比session大,而transaction是spring负责管理的,所以此参数也无效。这三个参数在iBatis的2.3.1版本中已经去掉了,可能也是因为这个问题。

基于上述分析我们决定使用数据库连接作为限制条件(通过使maxSession值大于数据库连接数),放弃使用session来进行控制。这个事也让我明白了在事务中尽量不要使用锁(特别是无等待时间的锁),不管是显示的还是隐式的(框架的)。关于jdbc的超时设置可以见参考资料2 3 4

参考资料:

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Java测试那点事

最近公司在整顿大家的单元测试,会定期出coverage报告。我个人还是挺支持这事的,一方面能培养大家的开发习惯,也能提升代码质量(如果不考核就更好了,哈哈)。现在公司使用的是Spring框架,大家现在所说的单元测试其实更应该算是集成测试,因为很多人会测试交易报文的正确性、整个交易的执行正确性、数据库操作的正确性。

在测试过程中发现很多我以前没在意的问题,也让我对代码测试有了更深的理解。下面简要介绍一下:

1. Spring Bean中使用static变量:

使用Spring的初衷是使用它的IOC特性,能够装配出想要的bean,这些类只需要是普通的POJO对象就可以了,但是有些人却使用static类型来保存数据,更可气的是不支持多次加载(会判断如果static变量中有值时就报错),这样就把Spring的优点消失殆尽了。其实这样只是为了可以直接使用静态函数访问这个类。

这样做的缺点是显而易见的,在连续执行多个测试案例(suite)时,如果Spring需要重新加载(见我上一篇文章),则会报错,因为不支持多次加载。解决方法目前想到的只能使用ant通过fork出一个新的jvm的方式来进行测试。或者使用Powermock工具通过单独的classloader加载,但这样会导致Spring context不能复用、找不到某些类如log4j等、代码覆盖度报告无显示等很多其他问题。

2. 使用final、static等不可测试的方法:

普通的Mock方式一般是针对接口,或是通过继承来修改一个类的方法,如果遇到这些方法,想要进行单元测试,一般的Mock工具都不好使。但如果为了测试而修改代码风险又很大,好在现在发现一个好用的工具Jmockit,它可以解决上述不可测代码的问题。它是通过java.lang.instrument包结合ASM字节码工具来实现的,具体可以参考此博文。我原来是使用Powermock来做的,它是通过自己的Classloader在类加载时进行修改来实现的,相比之下Jmockit要更直接一些,功能也更加强大。

[注]: Jmockit官网的教程是最新的,如果要看历史版本的文档可以在这里下载,里面包括源码和文档。

3.测试覆盖度报告工具使用

目前有很多测试覆盖度工具,包括coberturajacocoJmockit code coverage。其中eclispe的代码覆盖度工具EclEmma使用的就是jacoco。每个代码覆盖度工具提供的覆盖度报告是不同的,包括行覆盖度、分支覆盖度等等,具体大家可以参考它们的文档。下面说下使用不同工具应该关注的点:

  1. 是否支持maven、ant工具,是否可以支持多个工程报告合并
  2. 你使用的mock方法和这些代码覆盖度工具是否兼容
  3. 能提供的报告内容是否满足你的需要,你是否认可它的指标
  4. 生成覆盖度报告的样式你是否满意

测试覆盖度工具、Mock工具建议大家了解一下它们的原理,可以在网上搜以下。这样大家在使用时可以更得心应手。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Java测试那点事

最近公司在整顿大家的单元测试,会定期出coverage报告。我个人还是挺支持这事的,一方面能培养大家的开发习惯,也能提升代码质量(如果不考核就更好了,哈哈)。现在公司使用的是Spring框架,大家现在所说的单元测试其实更应该算是集成测试,因为很多人会测试交易报文的正确性、整个交易的执行正确性、数据库操作的正确性。

在测试过程中发现很多我以前没在意的问题,也让我对代码测试有了更深的理解。下面简要介绍一下:

1. Spring Bean中使用static变量:

使用Spring的初衷是使用它的IOC特性,能够装配出想要的bean,这些类只需要是普通的POJO对象就可以了,但是有些人却使用static类型来保存数据,更可气的是不支持多次加载(会判断如果static变量中有值时就报错),这样就把Spring的优点消失殆尽了。其实这样只是为了可以直接使用静态函数访问这个类。

这样做的缺点是显而易见的,在连续执行多个测试案例(suite)时,如果Spring需要重新加载(见我上一篇文章),则会报错,因为不支持多次加载。解决方法目前想到的只能使用ant通过fork出一个新的jvm的方式来进行测试。或者使用Powermock工具通过单独的classloader加载,但这样会导致Spring context不能复用、找不到某些类如log4j等、代码覆盖度报告无显示等很多其他问题。

2. 使用final、static等不可测试的方法:

普通的Mock方式一般是针对接口,或是通过继承来修改一个类的方法,如果遇到这些方法,想要进行单元测试,一般的Mock工具都不好使。但如果为了测试而修改代码风险又很大,好在现在发现一个好用的工具Jmockit,它可以解决上述不可测代码的问题。它是通过java.lang.instrument包结合ASM字节码工具来实现的,具体可以参考此博文。我原来是使用Powermock来做的,它是通过自己的Classloader在类加载时进行修改来实现的,相比之下Jmockit要更直接一些,功能也更加强大。

[注]: Jmockit官网的教程是最新的,如果要看历史版本的文档可以在这里下载,里面包括源码和文档。

3.测试覆盖度报告工具使用

目前有很多测试覆盖度工具,包括coberturajacocoJmockit code coverage。其中eclispe的代码覆盖度工具EclEmma使用的就是jacoco。每个代码覆盖度工具提供的覆盖度报告是不同的,包括行覆盖度、分支覆盖度等等,具体大家可以参考它们的文档。下面说下使用不同工具应该关注的点:

  1. 是否支持maven、ant工具,是否可以支持多个工程报告合并
  2. 你使用的mock方法和这些代码覆盖度工具是否兼容
  3. 能提供的报告内容是否满足你的需要,你是否认可它的指标
  4. 生成覆盖度报告的样式你是否满意

测试覆盖度工具、Mock工具建议大家了解一下它们的原理,可以在网上搜以下。这样大家在使用时可以更得心应手。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Spring Test的一些小技巧

目前项目在使用Spring Test进行单元测试(其实算是集成测试了),有两个小知识想和大家分享下。

1.对于同时存在注解、xml配置的bean会以xml中的配置为准

例如spring配置文件如下:

    <!-- application.xml -->
    <context:component-scan base-package="com.itechlib.com.service" />
    
    <!-- 在测试目录下定义stub类 -->
    <bean id="userDao" class="com.itechlib.com.test.stub.UserDaoStubServiceImpl" />

Java代码如下:

@component("userService")
class UserServiceImpl implements IUserService {

@Resource(name="userDao")
private IUserDao userDao;

  public void doUpdate(){
    //...
    userDao.update();
    //...
  }
}

这样在测试时UserServiceImpl调用的就是stub包下定义的userDao的模拟类了。

2.同时运行多个测试类的时候,可以有不同的ContextConfiguration配置

例如使用Junit TestSuite来一次执行多个测试类,这时如果不同测试类的ContextConfiguration配置不一样会怎么样呢?其实这些测试类都能够正确运行,因为spring会针对不同的configuration创建不同的ApplicationContext,而且这些ApplicationContext都会进行缓存,只会加载一次。

下面这些key不同就会生成不同的ApplicationContext,具体见spring reference 文档 Context caching

  • locations (from @ContextConfiguration)
  • classes (from @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextLoader (from @ContextConfiguration)
  • activeProfiles (from @ActiveProfiles)
  • resourceBasePath (from @WebAppConfiguration)

如果测试中破坏了ApplicationContext,可以对这个测试类声明@DirtiesContext

例如下面的写法会生成两个独立的ApplicationContext。

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application.xml"})
class UserServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testUpdate(){
    //...    
    }
}

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application-custom.xml"})
class OrderServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testAdd(){
    //...    
    }
}

了解这些你可以更好的规划你的测试配置,更好地测试各种场景和分支。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

Spring Test的一些小技巧

目前项目在使用Spring Test进行单元测试(其实算是集成测试了),有两个小知识想和大家分享下。

1.对于同时存在注解、xml配置的bean会以xml中的配置为准

例如spring配置文件如下:

    <!-- application.xml -->
    <context:component-scan base-package="com.itechlib.com.service" />
    
    <!-- 在测试目录下定义stub类 -->
    <bean id="userDao" class="com.itechlib.com.test.stub.UserDaoStubServiceImpl" />

Java代码如下:

@component("userService")
class UserServiceImpl implements IUserService {

@Resource(name="userDao")
private IUserDao userDao;

  public void doUpdate(){
    //...
    userDao.update();
    //...
  }
}

这样在测试时UserServiceImpl调用的就是stub包下定义的userDao的模拟类了。

2.同时运行多个测试类的时候,可以有不同的ContextConfiguration配置

例如使用Junit TestSuite来一次执行多个测试类,这时如果不同测试类的ContextConfiguration配置不一样会怎么样呢?其实这些测试类都能够正确运行,因为spring会针对不同的configuration创建不同的ApplicationContext,而且这些ApplicationContext都会进行缓存,只会加载一次。

下面这些key不同就会生成不同的ApplicationContext,具体见spring reference 文档 Context caching

  • locations (from @ContextConfiguration)
  • classes (from @ContextConfiguration)
  • contextInitializerClasses (from @ContextConfiguration)
  • contextLoader (from @ContextConfiguration)
  • activeProfiles (from @ActiveProfiles)
  • resourceBasePath (from @WebAppConfiguration)

如果测试中破坏了ApplicationContext,可以对这个测试类声明@DirtiesContext

例如下面的写法会生成两个独立的ApplicationContext。

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application.xml"})
class UserServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testUpdate(){
    //...    
    }
}

@ContextConfiguration(loader = SpringBeanTestContextLoader.class, locations = {
    "classpath:test-resources/application/application-custom.xml"})
class OrderServiceTests extends AbstractJunit4SpringContextTests{
    @Test
    public void testAdd(){
    //...    
    }
}

了解这些你可以更好的规划你的测试配置,更好地测试各种场景和分支。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》