【肥朝】你的接口,真的能承受高并发吗?

前言

本篇主要讲解的是前阵子的一个压测问题.那么就直接开门见山java

可能有的朋友不并不知道forceTransactionTemplate这个是干吗的,首先这里先普及一下,在Java中,咱们通常开启事务就有三种方式面试

  • XML中根据service及方法名配置切面,来开启事务(前几年用的频率较高,如今基本不多用)spring

  • @Transactional注解开启事务(使用频率最高)数据库

  • 采用spring的事务模板(截图中的方式,几乎没什么人用)bash

咱们先不纠结为何使用第三种,后面在讲事务传播机制的时候我会专门介绍,咱们聚焦一下主题,你如今只要知道,那个是开启事务的意思就好了.我特地用红色和蓝色把日志代码圈起来,意思就是,进入方法的时候打印日志,而后开启事务后,再打印一个日志.一波压测以后,发现接口频繁超时,数据一致压不上去.咱们查看日志以下:网络

咱们发现.这两个日志输出的时间间隔,居然用了接近5秒!开个事务为什么用了5秒?事出反常必有妖!并发

如何切入解决问题

线上遇到高并发的问题,因为通常高并发问题重现难度比较大,因此通常肥朝都是采用眼神编译,九浅一深静态看源码的方式来分析.具体能够参考本地可跑,上线就崩?慌了!.可是考虑到肥朝公众号仍然有小部分新关注的粉丝还没有掌握分析问题的技巧,本篇就再讲一些遇到此类问题的一些常见分析方式,不至于遇到问题时,慌得一比!分布式

好在这个并发问题的难度并不大,本篇案例排查很是适合小白入门,咱们能够经过本地模拟场景重现,将问题范围缩小,从而逐步定位问题.ide

本地重现

首先咱们能够准备一个并发工具类,经过这个工具类,能够在本地环境模拟并发场景.手机查看代码并不友好,可是不要紧,如下代码均是给你复制粘贴进项目重现问题用的,并非给你手机上看的.至于这个工具类为何能模拟并发场景,因为这个工具类的代码**全是JDK中的代码**,核心就是CountDownLatch类,这个原理你根据我提供的关键字对着你喜欢的搜索引擎搜索便可.高并发

CountDownLatchUtil.java

public class CountDownLatchUtil {

    private CountDownLatch start;
    private CountDownLatch end;
    private int pollSize = 10;

    public CountDownLatchUtil() {
        this(10);
    }

    public CountDownLatchUtil(int pollSize) {
        this.pollSize = pollSize;
        start = new CountDownLatch(1);
        end = new CountDownLatch(pollSize);
    }

    public void latch(MyFunctionalInterface functionalInterface) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(pollSize);
        for (int i = 0; i < pollSize; i++) {
            Runnable run = new Runnable() {
                @Override
                public void run() {
                    try {
                        start.await();
                        functionalInterface.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        end.countDown();
                    }
                }
            };
            executorService.submit(run);
        }

        start.countDown();
        end.await();
        executorService.shutdown();
    }

    @FunctionalInterface
    public interface MyFunctionalInterface {
        void run();
    }
}
复制代码

HelloService.java

public interface HelloService {

    void sayHello(long timeMillis);

}
复制代码

HelloServiceImpl.java

@Service
public class HelloServiceImpl implements HelloService {

    private final Logger log = LoggerFactory.getLogger(HelloServiceImpl.class);

    @Transactional
    @Override
    public void sayHello(long timeMillis) {
        long time = System.currentTimeMillis() - timeMillis;
        if (time > 5000) {
            //超过5秒的打印日志输出
            log.warn("time : {}", time);
        }
        try {
            //模拟业务执行时间为1s
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

HelloServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class HelloServiceTest {

    @Autowired
    private HelloService helloService;

    @Test
    public void testSayHello() throws Exception {
        long currentTimeMillis = System.currentTimeMillis();
        //模拟1000个线程并发
        CountDownLatchUtil countDownLatchUtil = new CountDownLatchUtil(1000);
        countDownLatchUtil.latch(() -> {
            helloService.sayHello(currentTimeMillis);
        });
    }

}
复制代码

咱们从本地调试的日志中,发现了大量超过5s的接口,而且还有一些规律,肥朝特意用不一样颜色的框框给你们框起来

为何这些时间,都是5个为一组,且每组数据相差是1s左右呢?

真相大白

@Transactional的核心代码以下(后续我会专门一个系列分析这部分源码,关注肥朝以避免错过核心内容).这里简单说就是TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);方法会去获取数据库链接.

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
	// Standard transaction demarcation with getTransaction and commit/rollback calls.
	TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
	Object retVal = null;
	try {
		// This is an around advice: Invoke the next interceptor in the chain.
		// This will normally result in a target object being invoked.
		retVal = invocation.proceedWithInvocation();
	}
	catch (Throwable ex) {
		// target invocation exception
		completeTransactionAfterThrowing(txInfo, ex);
		throw ex;
	}
	finally {
		cleanupTransactionInfo(txInfo);
	}
	commitTransactionAfterReturning(txInfo);
	return retVal;
}
复制代码

而后肥朝为了更好的演示这个问题,将数据库链接池(本篇用的是Druid)的参数作了如下设置

//初始链接数
spring.datasource.initialSize=1
//最大链接数
spring.datasource.maxActive=5
复制代码

因为最大链接数是5.因此当1000个线程并发进来的时候,你能够想象是一个队伍有1000我的排队,最前面的5个,拿到了链接,而且执行业务时间为1秒.那么队伍中剩下的995我的,就在门外等候.等这5个执行完的时候.释放了5个链接,依次向后的5我的又进来,又执行1秒的业务操做.经过简单的小学数学,均可以计算出最后5个执行完,须要多长时间.经过这里分析,你就知道,为何上面的日志输出,是5秒为一组了,而且每组间隔为1s了.

怎么解决

看过肥朝源码实战的粉丝都知道,肥朝历来不耍流氓,凡是抛出问题,都会相应给出其中一种解决方案.固然方案没有最优只有更优!

好比看到这里有的朋友可能会说,你最大链接数设置得**就像平时赞扬肥朝的金额同样小**,若是设置大一点,天然就不会有问题了.固然这里为了方便向你们演示问题,设置了最大链接数是5.正常生产的链接数是要根据业务特色和不断压测才能得出合理的值,固然肥朝也了解到,部分同窗公司机器的配置,居然比不过市面上的千元手机!!!

可是其实当时压测的时候,数据库的最大链接数设置的是200,而且当时的压测压力并不大.那为何还会有这个问题呢?那么仔细看前面的代码

其中这个校验的代码是RPC调用,该接口的同事并无像肥朝同样值得托付终身般的高度可靠,致使耗时时间较长,从而致使后续线程获取数据库链接等待的时间过长.你再根据前面说的小学数学来算一下就很容易明白该压测问题出现的缘由.

敲黑板划重点

以前肥朝就反复说过,遇到问题,要通过深度思考.好比这个问题,咱们能获得什么拓展性的思考呢?咱们来看一下以前一位粉丝的面试经历

其实他面试遇到的这个问题,和咱们这个压测问题基本是同一个问题,只不过面试官的结论其实并不够准确.咱们来一块儿看一下阿里巴巴的开发手册

那么什么样叫作滥用呢?其实肥朝认为,即便这个方法常常调用,可是都是单表insert、update操做,执行时间很是短,那么承受较大并发问题也不大.关键是,这个事务中的全部方法调用,是不是有意义的,或者说,事务中的方法是不是真的要事务保证,才是关键.由于部分同窗,在一些比较传统的公司,作的可能是能用就行的CRUD工做,很容易一个service方法,就直接打上事务注解开始事务,而后在一个事务中,进行大量和事务一毛钱关系都没有的无关耗时操做,好比文件IO操做,好比查询校验操做等.例如本文中的业务校验就彻底不必放在事务中.平时工做中没有相应的实战场景,加上并无关注肥朝的公众号,对原理源码真实实战场景一无所知.面试稍微一问原理就喊痛,面试官也只好换个方向再继续深刻!

经过这个经历咱们又有什么拓展性的思考呢?由于问题是永远解决不完的,可是咱们能够经过不断的思考,把这个问题压榨出更多的价值!咱们再来看一下阿里规范手册

用大白话归纳就是,尽可能减小锁的粒度.而且尽可能避免在锁中调用RPC方法,由于RPC方法涉及网络因素,他的调用时间存在很大的不可控,很容易就形成了占用锁的时间过长.

其实这个和咱们这个压测问题是同样的.首先你本地事务中调用RPC既不能起到事务做用(RPC须要分布式事务保证),可是又会由于RPC不可控因素致使数据库链接占用时间过长.从而引发接口超时.固然咱们也能够经过APM工具来梳理接口的耗时拓扑,将此类问题在压测前就暴露.

写在最后

相关文章
相关标签/搜索