SpringMVC中Controller为什么能够处理并发访问
SpringMVC中用来处理http请求的Controller是基于Servlet实现的,Spring中绝大多数的类都是单例的,Servlet也是这样。
在学习时我曾经有一个问题就是在Spring中只有一个控制器,那么它是怎么能够在同时处理很多个请求的呢?
经过一些学习,我想我应该知道了它的答案,在这里记录一下。
首先要回答的一个问题是:计算机是如何处理一个请求的呢?
我们知道,计算机大部分的任务都是由CPU来完成的,Controller虽然叫做控制器,但是实际处理的是CPU。控制器提供了CPU处理请求的方法,所以实际上是CPU根据Controller中的代码来处理。
那么是谁来控制CPU来进行任务呢?当然是进程了,在我们面对的计算机中,进程是运行的基本单元。
所以第一个问题的答案就是:请求是由计算机中某个进程根据特定的指令来处理的。这个可能听起来有点像是废话,但是知道了这一点,接下来的分析就很简单了。
服务器收到一个请求后,会有一个进程来处理它,把这个请求经过拦截器等等不同的处理程序,终于来到了控制器了,控制器对它进行了一些处理,然后又把它交给下一步的程序处理(实际上的实施主体是进程,没搞清主体是我有这些疑惑的主要原因),经过一些处理,这时就可以叫处理过后的数据为响应了,进程把这些数据发送到某个接受的地方,一次Http请求就完成了。
在这个过程中,真正操作的是一个进程,代码是存放在内存中的一段一段数据,进程从中读取数据,也许会对其中的某些数据进行修改(这里就涉及到了多线程的安全问题)。在实际中,操作的是一个线程,它在主进程中创建,用于处理一个请求。
现在,有多个请求同时访问服务器,每个请求都有一个线程来处理,线程根据内存中的代码执行下去,每个线程都可以访问到Controller中的代码,如果Controller只有一个的话,那每个线程都访问这个Controller,根据它的代码来执行。这就是我曾经疑问的答案,代码就像是一份说明书,无论多少的请求,都按照同一份说明书来处理。
知道了每个请求都是由一个线程来处理,我们也就可以明白一个服务器同时能够处理的请求数与它的线程数有很大的关系。线程的创建是比较消耗资源的,所以容器一般维持一个线程池。像Tomcat的线程池 maxThreads
是200, minSpareThreads
是25。实际中单个Tomcat服务器的最大并发数只有几百,部分原因就是只能同时处理这么多线程上的任务。当然,并发的限制肯定不止在这里,还有很多需要考虑的地方。
如果一个类无论什么时候都不会改变,那么它就是线程安全的,无论多少线程同时访问,都会得到相同的结果,不会有任何影响,不用考虑多线程带来的影响。
控制器中如果没有维持可变的成员变量,也类似于不可变类,它在多线程情况下也不需要多考虑,和在单线程下区别不大,当然这一般不会发生。我们经常在其中定义许多Service,在容器启动的时候这些Service被注入进来,用户传入的请求大部分在这里和服务器进行交互,比如查看当前是否登录,请求查看用户信息等等,根据Controller中的代码,调用不同的Service对这些信息进行处理。这里就要考虑到线程安全的问题了。
@GetMapping(value = "/auth")
@ResponseBody
public Object auth() {
return authService.getCurrentUser()
.map(user -> LoginResult.success("用户已登录", user))
.orElse(LoginResult.success("用户未登录", false));
}
这是Controller中一个简单的验证是否登录的方法,可以看到,这是一个处理Get请求的方法,返回一个用户是否登录的Json数据。在这里调用了authService的getCurrentUser方法来获取当前回话中是否登录。假如同时有一个请求进行了登录操作,当获取到当前回话的的用户后登录才完成,这时会出现当前已登录但是返回了未登录的信息。这就是因为多线程出现的问题。当然,这个例子里的问题不大,刷新一下就好了。
下面这个例子问题就比较大了
@PostMapping("/auth/register")
@ResponseBody
public Result register(@RequestBody Map<String, String> usernameAndPassword) {
String username = usernameAndPassword.get("username");
String password = usernameAndPassword.get("password");
//输入格式合法性判断
........................
User user = userService.getUserByUsername(username);
if (user == null) {
userService.save(username, password);
User registeredUser = userService.getUserByUsername(username);
return LoginResult.success("注册成功", registeredUser);
} else {
return LoginResult.fail("用户已存在");
}
}
这是Controller中的一个注册方法,逻辑很简单,获取用户名是否已注册,没有则将用户名和密码保存到数据库中,有的话则返回用户已存在的信息。
这里就会出现多线程引起的问题了,当两个相同用户名的注册请求同时执行到 User user = userService.getUserByUsername(username)
时,得到的user都为空,这时他们都会向数据库中写数据,而数据库中用户名不能重复,这时就会产生异常。
这时我们就要处理多线程带来的问题了,这里我们捕获这个异常,处理后返回给用户对应的信息。
if (user == null) {
userService.save(username, password);
} catch (KeyAlreadyExistsException e) {
return LoginResult.fail("用户已存在");
}
.......
}