简介
本文介绍SpringBoot为什么Controller是串行的?在什么场景下才能并行执行?
大家都知道,SpringBoot的Controller按理是并行执行的,也就是说:如果一个请求没执行完,另一个请求就进入了,那么第二个请求也会立刻执行,与第一个请求并行。但事实真的是这样吗?
问题描述
对于下边的Controller,如果一个请求没执行完,另一个请求就进入了,会输出什么结果?
package com.knife.example.controller; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "测试") @RestController public class TestController { @ApiOperation("测试1") @GetMapping("test1") public String test1(String username) { System.out.println("方法进入。线程名:" + Thread.currentThread().getName()); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("方法退出。线程名:" + Thread.currentThread().getName()); return "success"; } }
为了方便,我这里用的是knife4j(swagger的升级版)。
请求方式:打开两个页面,传参一样。先在第一个页面请求,第一个页面还没返回时,快速在第二个页面请求。
后端的打印结果:
方法进入。线程名:http-nio-8080-exec-7 方法退出。线程名:http-nio-8080-exec-7 方法进入。线程名:http-nio-8080-exec-8 方法退出。线程名:http-nio-8080-exec-8
什么?竟然是串行的!第一个请求完全退出之后,第二个请求才进来,这是为什么!
原因排查
1.打开F12(是并行的)
打开F12,看结果是否有不同:
后端结果(竟然是并行的!)
方法进入。线程名:http-nio-8080-exec-7 方法进入。线程名:http-nio-8080-exec-8 方法退出。线程名:http-nio-8080-exec-7 方法退出。线程名:http-nio-8080-exec-8
2.Get使用不同的参数(是并行的)
再尝试一下不同的参数,这次不打开F12。
后端结果:(是并行的)
方法进入。线程名:http-nio-8080-exec-3 方法进入。线程名:http-nio-8080-exec-4 方法退出。线程名:http-nio-8080-exec-3 方法退出。线程名:http-nio-8080-exec-4
3.Post(是并行的)
了解RestFul的都知道,Get一般对应读操作,Post一般对应写操作,难道SpringBoot针对Get请求有特殊处理?那我就试一下Post:
package com.knife.example.controller; import com.knife.example.entity.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.Mapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @Api(tags = "测试") @RestController public class TestController { @ApiOperation("测试1") @PostMapping("test1") public String test1() { System.out.println("方法进入。线程名:" + Thread.currentThread().getName()); try { Thread.sleep(5000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("方法退出。线程名:" + Thread.currentThread().getName()); return "success"; } }
同样的方法测试(没打开F12)。结果是(并行的!!!):
方法进入。线程名:http-nio-8080-exec-7 方法进入。线程名:http-nio-8080-exec-8 方法退出。线程名:http-nio-8080-exec-7 方法退出。线程名:http-nio-8080-exec-8
原因定位
从上边应该能感觉出来,参数、是否开启F12、请求方法,都会有影响。怀疑点是:是不是与浏览器有关系?
查一下浏览器的相关资料:
谷歌浏览器同时只能对同一个URL发起一个请求,如果有更多的请求的话,则会串行执行。如果请求阻塞,后续相同请求也会阻塞。
Chrome的限制规则是:浏览器同时只能对同一个URL提出一个请求,如果有更多的请求的话,对不起,请排队。这个所谓“限制”到底好不好?可能不错,想想对同一URL的请求,如果前请求阻塞,那么后请求想必也会被阻塞,这无端增加了开销,并没多大意义,Chrome这么做应该有它的合理性。
有篇文章是这么写的(链接:https://codereview.chromium.org/345643003):
Http cache: Implement a timeout for thecache lock. The cache has a single writer / multiple reader lock to avoiddownloading the same resource n times. However, it is possible to block manytabs on the same resource, for instance behind an auth dialog. This CLimplements a 20 seconds timeout so that the scenario described in the bugresults in multiple authentication dialogs (one per blocked tab) so the usercan know what to do. It will also help with other cases when the single writerblocks for a long time. The timeout is somewhat arbitrary but it should allowmedium size resources to be downloaded before starting another request for thesame item. The general solution of detecting progress and allow readers tostart before the writer finishes should be implemented on another CL.
找到谷歌的源码
有一段代码:
void HttpCache::Transaction::AddCacheLockTimeoutHandler(ActiveEntry* entry) { DCHECK(next_state_ == STATE_ADD_TO_ENTRY_COMPLETE || next_state_ == STATE_FINISH_HEADERS_COMPLETE); if ((bypass_lock_for_test_ && next_state_ == STATE_ADD_TO_ENTRY_COMPLETE) || (bypass_lock_after_headers_for_test_ && next_state_ == STATE_FINISH_HEADERS_COMPLETE)) { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout, weak_factory_.GetWeakPtr(), entry_lock_waiting_since_)); } else { int timeout_milliseconds = 20 * 1000; if (partial_ && entry->writers && !entry->writers->IsEmpty() && entry->writers->IsExclusive()) { // Even though entry_->writers takes care of allowing multiple writers to // simultaneously govern reading from the network and writing to the cache // for full requests, partial requests are still blocked by the // reader/writer lock. // Bypassing the cache after 25 ms of waiting for the cache lock // eliminates a long running issue, http://crbug.com/31014, where // two of the same media resources could not be played back simultaneously // due to one locking the cache entry until the entire video was // downloaded. // Bypassing the cache is not ideal, as we are now ignoring the cache // entirely for all range requests to a resource beyond the first. This // is however a much more succinct solution than the alternatives, which // would require somewhat significant changes to the http caching logic. // // Allow some timeout slack for the entry addition to complete in case // the writer lock is imminently released; we want to avoid skipping // the cache if at all possible. See http://crbug.com/408765 timeout_milliseconds = 25; } base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( FROM_HERE, base::BindOnce(&HttpCache::Transaction::OnCacheLockTimeout, weak_factory_.GetWeakPtr(), entry_lock_waiting_since_), TimeDelta::FromMilliseconds(timeout_milliseconds)); } }
解决方案
上边“原因排查”那里已经有方案了:
方案1:打开F12
方案2:使用Post请求
请先
!