项目场景:
发生问题的是一个交易项目,使用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这个类,在这个拦截器中就有事务管理相关的代码