项目场景:

发生问题的是一个交易项目,使用SpringBoot+SpringDataJpa框架,上边连接银行系统发起交易


问题描述:

我们的项目平时并发量并不算大,但是有一天客户联系我们,说会出现交易卡死,所有交易无法进行,大约等待一分钟后会恢复的现象。后来检查日志,发现日志中有大量的等待数据库连接超时的问题


原因分析:

先写一下大致的代码结构:

├─src
   └──main
       └──java
   			└──com.example.test
   	   			└──controller
   	   				└──TranController	
   	   			└──business
   	   				└──PaymentBusiness
   	   			└──service
   	   				├──MerchantService
   	   				└──TransactionService
   	   			└──dao
   	   				├──BaseDao
   	   				├──MerchantDao
   	   				└──TransactionDao
   	   			└──entity
   	   				├──Merchant
   	   				└──Transaction
   	   			└──util
   	   				└──SequenceGenerator
   	   			└──TestApplication

主要代码:


PaymentBusiness.java

package com.example.test.business;

import com.example.test.service.MerchantService;
import com.example.test.service.TransactionService;
import com.example.test.util.SequenceGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author quhan
 */
@Service
public class PaymentBusiness {
    @Autowired
    MerchantService merchantService;
    @Autowired
    TransactionService transactionService;

    @Autowired
    SequenceGenerator sequenceGenerator;

    public String payment(String merchantNo, String merchantOrderNo, String amt) {
        boolean checkMerchantResult = merchantService.checkMerchant(merchantNo);
        boolean checkTransactionResult = transactionService.checkTransaction(merchantOrderNo);

        if (!checkMerchantResult || !checkTransactionResult) {
            return "FAIL";
        }

        int sequence = sequenceGenerator.getSequence(merchantNo);

        String paymentResult = null;

        //TODO 发起交易,此处需要使用 sequence 的值, 处理交易返回值,设置 paymentResult 的值

        paymentResult = "SUCCESS";

        return paymentResult;
    }

}


MerchantService.java

package com.example.test.service;

import com.example.test.dao.MerchantDao;
import com.example.test.entity.Merchant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author quhan
 */
@Service
public class MerchantService {

    @Autowired
    MerchantDao merchantDao;

    /**
     * 检查商户
     *
     * @return 如果商户检查正常则返回true, 否则返回false
     */
    @Transactional(readOnly = true)
    public boolean checkMerchant(String merchantNo) {
        System.out.println("准备检查商户是否存在");
        Merchant merchant = merchantDao.findById(merchantNo).orElse(null);
        System.out.println("商户检查数据库操作完成");
        return merchant != null;
    }
}


TransactionService.java

package com.example.test.service;

import com.example.test.dao.TransactionDao;
import com.example.test.entity.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author quhan
 */
@Service
public class TransactionService {

    @Autowired
    TransactionDao transactionDao;

    @Transactional(readOnly = true)
    public boolean checkTransaction(String merchantOrderNo) {
        System.out.println("准备检查商户订单号是否存在");
        Transaction transaction = transactionDao.findById(merchantOrderNo).orElse(null);
        System.out.println("商户订单号检查数据库操作完成");
        return transaction == null;
    }
}


SequenceGenerator.java

package com.example.test.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 序列生成
 *
 * @author quhan
 */
@Component
public class SequenceGenerator {

    private static Logger LOGGER = LoggerFactory.getLogger(SequenceGenerator.class);

    @Autowired
    private DataSource dataSource;

    public synchronized int getSequence(String key) {
        int statement = 0;
        String querySql = "SELECT " + key + ".nextval from dual";
        try (Connection connection = dataSource.getConnection()) {
            ResultSet resultSet = connection.createStatement().executeQuery(querySql);
            resultSet.next();
            statement = resultSet.getInt(1);
        } catch (SQLException e) {
            LOGGER.error("从数据库获取下一个Sequence数据错误,Key:" + key, e);
        }
        return statement;
    }
}

开始调试:

调试的时候,我们在SpringBoot的application.properties配置文件中加入了:

logging.level.com.zaxxer.hikari=trace

这个配置,这个配置会把数据库连接池的状态打印出来,会显示连接池的使用情况

预想的正常情况

因为 PaymentBusiness 中在 没有 @Transactional 注释,但是在MerchantService 和 TransactionService 中是有 @Transactional 注释的

所以我们认为,在PaymentBusiness 调用 MerchantService 类的 checkMerchant 方法完成之后,在进入TransactionService 的 checkTransaction 方法之前,数据库连接池的活动连接数应该是0(因为 MerchantService 的事务应该已经提交了)

之后我们使用Debug调用 PaymentBusiness 测试,结果跟我们预想是一致的,出了Service之后,事务关闭,连接就释放了;再进Service,再开连接,出Service 连接释放,测试结果符合预期。

实际上显现的问题

但这个时候更好奇为什么生产情况下会出现连接占用太多的情况,于是分析了与生产环境不同的地方,首先就是生产环境是通过web访问Controller调用的 PaymentBusiness ,这跟测试不同

于是就补了Controller,通过 Postman 调用Controller进行测试。这次在PaymentBusiness 中调用MerchantService之后,就算是代码执行出了 Service , 连接池的活动连接数还是1,也就是说连接还是没有释放掉。直到在调用 SequenceGenerator 的 getSequence 方法中,因为手动从DataSource中获取了连接,所以连接池的占用连接数变成了2,出了 getSequence 之后,由于手动释放了数据库的 Connection 所以活动的连接恢复为1,这个连接一直到出了 Business 之后,响应结果完全返回给客户端之后才会释放。

也就是说,这个数据库连接,会从这个请求调用起来第一个数据库请求之后,一直持有,直到整个请求处理结束才会释放

这样一来,问题就显现出来了,因为在连接池的其中一个连接没有释放的时候,手动从 DataSource 中获取另一个连接,如果我们现在配置文件中配置最大打开60个连接,这时正好有60个请求进来 Business ,都调用完了Service,其中有一个请求走到了执行 SequenceGenerator 的 getSequence 方法中,这时就获取不到数据库连接,剩下的59个请求也会很快走到这个方法,但是在现在 Service 中获取的60个连接都没有释放,无法获取新的数据库连接,所以导致程序会一致等待获取数据库连接,所有交易全部卡死,一分钟以后,到达我们配置的最大等待时间,程序报错,连接全部释放,交易又可以正常进行

另外我们的 PaymentBusiness 中有需要连接银行服务器发起交易的耗时IO操作,这样一来会导致单个连接占用时间更长


解决方案:

到现在看来,问题已经很明显了,解决方法就是在执行完一个Service的时候,把事务关掉就能解决这个问题:
application.properties 中加入

spring.jpa.open-in-view=false

配置,就可以解决这个问题

另有一个配置文件不知道是干啥的,我们也没用,也给加上了
后来百度到是个什么图形数据库什么的,但是我们没有涉及到这方面的技术,就没管它

spring.data.neo4j.open-in-view=false

感觉这个配置文件是打开或者关闭在事务以外,也可以访问懒加载属性的数据的功能,但是如果你的程序逻辑中在操作数据库之后有耗时IO操作,这个功能会非常影响你的程序性能

到此,问题解决

相关源码:

Spring Data Jpa 有个 OpenEntityManagerInViewInterceptor 的拦截器,实现了SpringMvc 的 AsyncWebRequestInterceptor 接口,同时继承了 EntityManagerFactoryAccessor这个类,在这个拦截器中就有事务管理相关的代码