你的接口,真的能承受高并发吗?

点击上方"蓝字", 右上角选择“设为星标”

 周一至周五早8点半!精品技术文章准时送上!


本文来自肥朝同学的投稿


目录

一、写在前面

二、问题复现

三、真相大白

四、对症下药

五、敲黑板、划重点



一、写在前面

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

640?wx_fmt=png

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

  • XML中根据service及方法名配置切面,来开启事务(前几年用的频率较高,现在基本很少用)

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

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

我们先不纠结为什么使用第三种,后面在讲事务传播机制的时候我会专门介绍。你现在只要知道,那个是开启事务的意思就行了.

我特意用红色和蓝色把日志代码圈起来,意思就是,进入方法的时候打印日志,然后开启事务后,再打印一个日志.

一波压测之后,发现接口频繁超时,数据一直压不上去.我们查看日志如下:

640?wx_fmt=jpeg

我们发现.这两个日志输出时间间隔,竟然用了接近5秒!开个事务为何用了5秒?事出反常必有因!


二、问题复现

首先我们可以准备一个并发工具类,通过这个工具类,可以在本地环境模拟并发场景.


至于这个工具类为什么能模拟并发场景,由于这个工具类的代码全是JDK中的代码,核心就是CountDownLatch类,这个原理你根据我提供的关键字对着你喜欢的搜索引擎搜索即可.


CountDownLatchUtil.java

 1public class CountDownLatchUtil {
2
3    private CountDownLatch start;
4    private CountDownLatch end;
5    private int pollSize = 10;
6
7    public CountDownLatchUtil() {
8        this(10);
9    }
10
11    public CountDownLatchUtil(int pollSize) {
12        this.pollSize = pollSize;
13        start = new CountDownLatch(1);
14        end = new CountDownLatch(pollSize);
15    }
16
17    public void latch(MyFunctionalInterface functionalInterface) throws InterruptedException {
18        ExecutorService executorService = Executors.newFixedThreadPool(pollSize);
19        for (int i = 0; i < pollSize; i++) {
20            Runnable run = new Runnable() {
21                @Override
22                public void run() {
23                    try {
24                        start.await();
25                        functionalInterface.run();
26                    } catch (InterruptedException e) {
27                        e.printStackTrace();
28                    } finally {
29                        end.countDown();
30                    }
31                }
32            };
33            executorService.submit(run);
34        }
35
36        start.countDown();
37        end.await();
38        executorService.shutdown();
39    }
40
41    @FunctionalInterface
42    public interface MyFunctionalInterface {
43        void run();
44    }
45}


HelloService.java

1public interface HelloService {
2
3    void sayHello(long timeMillis);
4
5}


HelloServiceImpl.java

 1@Service
2public class HelloServiceImpl implements HelloService {
3
4    private final Logger log = LoggerFactory.getLogger(HelloServiceImpl.class);
5
6    @Transactional
7    @Override
8    public void sayHello(long timeMillis) {
9        long time = System.currentTimeMillis() - timeMillis;
10        if (time > 5000) {
11            //超过5秒的打印日志输出
12            log.warn("time : {}", time);
13        }
14        try {
15            //模拟业务执行时间为1s
16            Thread.sleep(1000);
17        } catch (Exception e) {
18            e.printStackTrace();
19        }
20    }
21}


HelloServiceTest.java

 1@RunWith(SpringRunner.class)
2@SpringBootTest
3public class HelloServiceTest {
4
5    @Autowired
6    private HelloService helloService;
7
8    @Test
9    public void testSayHello() throws Exception {
10        long currentTimeMillis = System.currentTimeMillis();
11        //模拟1000个线程并发
12        CountDownLatchUtil countDownLatchUtil = new CountDownLatchUtil(1000);
13        countDownLatchUtil.latch(() -> {
14            helloService.sayHello(currentTimeMillis);
15        });
16    }
17
18}


我们从本地调试的日志中,发现了大量超过5s的接口,并且还有一些规律,这里特地用不同颜色的框框给大家框起来

640?wx_fmt=png

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


三、真相大白

@Transactional的核心代码如下:

简单来说,就是:

retVal = 

invocation.proceedWithInvocation()方法会去获取数据库连接。

 1if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
2    // Standard transaction demarcation with getTransaction and commit/rollback calls.
3    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
4    Object retVal = null;
5    try {
6        // This is an around advice: Invoke the next interceptor in the chain.
7        // This will normally result in a target object being invoked.
8        retVal = invocation.proceedWithInvocation();
9    }
10    catch (Throwable ex) {
11        // target invocation exception
12        completeTransactionAfterThrowing(txInfo, ex);
13        throw ex;
14    }
15    finally {
16        cleanupTransactionInfo(txInfo);
17    }
18    commitTransactionAfterReturning(txInfo);
19    return retVal;
20}

为了更好的演示这个问题,将数据库连接池(本篇用的是Druid)的参数做了以下设置:

1//初始连接数
2spring.datasource.initialSize=1
3//最大连接数
4spring.datasource.maxActive=5

由于最大连接数是5.所以当1000个线程并发进来的时候,你可以想象是一个队伍有1000个人排队,最前面的5个,拿到了连接,并且执行业务时间为1秒.

那么队伍中剩下的995个人,就在门外等候.等这5个执行完的时候.释放了5个连接,依次向后的5个人又进来,又执行1秒的业务操作.

简单的计算,就可以得出最后5个执行完,需要多长时间.通过这里分析你就知道,为什么上面的日志输出,是5秒为一组了,并且每组间隔为1s了.


四、对症下药


看过笔者源码实战的粉丝都知道,凡是抛出问题,都会相应给出其中一种解决方案.当然方案没有最优只有更优!

比如看到这里,有的朋友可能会说,你最大连接数设置得那么小,如果设置大一点,自然就不会有问题了.

当然这里为了方便向大家演示问题,设置了最大连接数是5.正常生产的连接数是要根据业务特点和不断压测才能得出合理的值。

当然,笔者也了解到,部分同学公司机器的配置,竟然比不过市面上的千元手机!!!

但是其实当时压测的时候,数据库的最大连接数设置的是200,并且当时的压测压力并不大.

那既然这样,为什么还会有这个问题呢?

请大家仔细看前面的代码:

640?wx_fmt=png


其中这个校验的代码是RPC调用,该接口耗时较长,导致后续线程获取数据库连接等待的时间过长.

大家可以根据前面所述的方法,再来计算一下,就很容易明白该压测问题出现的原因.


敲黑板、划重点


之前笔者反复提过,遇到问题,要经过深度思考.比如这个问题,我们能得到什么拓展性的思考呢?


来看一下之前一位粉丝的面试经历


640?wx_fmt=jpeg

其实他面试遇到的这个问题,和我们这个压测问题基本是同一个问题,只不过面试官的结论其实并不够准确.


我们来一起看一下阿里巴巴的开发手册

640?wx_fmt=png

那么什么样叫做滥用呢?即这个方法经常调用,但都是单表insert、update操作,执行时间非常短,那么承受较大并发问题也不大.

关键是这个事务中的所有方法调用是否有意义,或者说事务中的方法是否是真的要事务保证,这个才是关键.

因为部分同学在一些比较传统的公司,做的多是能用就行的CRUD工作,很容易一个service方法就直接打上事务注解开始事务。

然后在一个事务中,进行大量和事务一毛钱关系都没有的无关耗时操作,比如文件IO操作,比如查询校验操作等。例如本文中的业务校验就完全没必要放在事务中.

通过这个经历我们又有什么拓展性的思考呢?因为问题是永远解决不完的,但是我们可以通过不断的思考,把这个问题压榨出更多的价值!

我们再来看一下阿里规范手册

640?wx_fmt=png

用大白话概括就是:尽量减少锁的粒度,并且尽量避免在锁中调用RPC方法。

因为RPC方法涉及网络因素,他的调用时间存在很大的不可控,很容易就造成了占用锁的时间过长.

其实这个和我们这个压测问题是一样的.首先你本地事务中调用RPC既不能起到事务作用(RPC需要分布式事务保证),但是又会因为RPC不可控因素导致数据库连接占用时间过长,从而引起接口超时。当然我们也可以通过APM工具来梳理接口的耗时拓扑,将此类问题在压测前就暴露.


End

640?wx_fmt=gif

公众号后台回复“学习”,获取作者独家秘制学习资料


还没读够?更多原创系列文章,请移步至



  • 2018年原创汇总


  • 2019年原创汇总(持续更新 


  • 面试专栏


  • 爆款推荐                                       

640?wx_fmt=gif

持续关注,一大波原创系列文章正在路上

欢迎扫描下方二维码

640?wx_fmt=jpeg

石杉的架构笔记(id:shishan100)

BAT架构经验倾囊相授



640?wx_fmt=gif

你可能感兴趣的:(你的接口,真的能承受高并发吗?)