线上的服务突然就卡死了,整个服务不可用了,必须要重启才能解决,但重启过后,过一段时间就又出现了,后来通过jstack命令排查到是获取数据库连接对象时,tomcat的线程阻塞在那里导致线程被耗尽(Connection newCon = obtainDataSource().getConnection();),最终造成服务不可用。但究竟是什么原因造成获取连接一直阻塞呢?
后来通过压测发现只要并发数超过了连接池的最大连接数,这个问题就必现,下面的代码是模拟生产的代码写的demo
操作表A:
@Component
public class AService {
@Autowired
private AMapper mapperA;
private AtomicInteger atomicInteger = new AtomicInteger();
public void saveA(){
String value = "a" + atomicInteger.getAndIncrement();
mapperA.add(value);
}
操作表B:
@Component
public class BService {
@Autowired
private BMapper mapperB;
private AtomicInteger atomicInteger = new AtomicInteger();
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveB() {
String value = "b" + atomicInteger.getAndIncrement();
mapperB.add(value);
}
}
业务逻辑:先对表b进行入库操作,在对表a进行入库操作,不管a是否保存成功都不影响b的结果
所以在对b保存得时候用了REQUIRES_NEW
@Component
public class DataService {
@Autowired
private AService aService;
@Autowired
private BService bService;
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void save() {
bService.saveB();
aService.saveA();
}
}
配置文件:
server:
port: 8848
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
username: root
password: 123456
druid:
max-active: 128
max-wait: -1
min-idle: 28
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
这里使用的时Druid连接池,这里犯了一个错误无论什么情况下max-wait都不能设置为-1,否则将会无限等待。
spring的事务传播行为中,当前若存在事务,若是方法调用中方法被REQUIRES_NEW修改,那么会将当前线程中的事务挂起。并创建一个新的事务,创建新的事务时会从连接池中获取一个可用得连接对象
Connection newCon = obtainDataSource().getConnection();
当我的业务并发数远远大于连接数时,
在进入DataService的方法save()时会创建事务,此时获取一个连接对象,当执行BService的saveB()时会开启一个新的事务,也会获取一个连接对象,当大量的线程全部执行到save()此时连接数已经被耗光,当获取到连接对象的线程执行saveB()方法时,需将当前线程的事务挂起,但不会归还连接对象,然后再创建一个新的事务,获取连接对象时,连接池无对象,此时阻塞等待别的线程释放连接对象,但别的线程也是这样想的此时就陷入了死锁状态,又因为max-wait设置的是-1,不存在阻塞超时,所以全部线程都阻塞,当有新的请求进来时,在调用DataService的save()方法获取连接时也阻塞,最后造成tomact的线程池也耗尽,最终服务不可用。
Durid获取数据库连接对象的源码:
if (maxWait > 0) {
holder = pollLast(nanos);
} else {
holder = takeLast();
}
//当maxWait>0时执行pollLast()方法,此时有设置等待超时
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException {
long startEstimate = estimate;
estimate = notEmpty.awaitNanos(estimate);
}
//当maxWait<0时执行takeLast() ,此时未设置等待超时
private DruidConnectionHolder takeLast() throws InterruptedException, SQLException {
try {
notEmpty.await();
} finally {
notEmptyWaitThreadCount--;
}
}
所以可以将max-wait设置成4s~5s,那么线程就不会陷入一直等待的状态,当这不是解决问题得关键。
当我们使用REQUIRED_NEW时一定要考虑清楚,不用REQUIRED_NEW能不能满足业务需要,如这个示列代码 ,在执行save()方法时,完全可以将saveB()单独提出来,不放在save()方法里面调用,这样就会是调用saveB()完成后归还连接池对象,归还后调用save()方法在获取一个新的,这样就不会有死锁的隐患了。
当然如果非要用REQUIRED_NEW,那么一定要保证你设置的最大连接数一定要大于改接口并发数,并设置合理的max-wait时间,也可以为该接口维护一个单独的数据库连接池,这样即使该接口出问题,也不会影响其余业务,总之一定不能造成服务不可用的状态,并尽量避免这种情况的发生。