@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)也会导致死锁吗?

线上的服务突然就卡死了,整个服务不可用了,必须要重启才能解决,但重启过后,过一段时间就又出现了,后来通过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时间,也可以为该接口维护一个单独的数据库连接池,这样即使该接口出问题,也不会影响其余业务,总之一定不能造成服务不可用的状态,并尽量避免这种情况的发生。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值