模型如下

nginx基于七层模型走的事HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量。
我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发。
经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。
在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右。
所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。
首先用户点发送验证码->验证码存入session
用户点登录或注册->检查+处理用户信息->用户信息存入session
用户请求(携带cookie)->cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息->检查用户是否存在来拦截



- //1.获取session
- HttpSession session = request.getSession();
- //2.获取session中的用户
- Object user = session.getAttribute("user");
- //3.判断用户是否存在
- if(user == null){
- //4.不存在,拦截,返回401状态码
- response.setStatus(401);
- return false;
- }
- //5.存在,保存用户信息到Threadlocal
- UserHolder.saveUser((User)user);
- //6.放行
- return true;
- //拦截器
- registry.addInterceptor(new LoginInterceptor())
- .excludePathPatterns(
- "/shop/**",
- "/voucher/**",
- "/shop-type/**",
- "/upload/**",
- "/blog/hot",
- "/user/code",
- "/user/login"
- ).order(1);
- // token刷新的拦截器
- registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);

当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外。当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket互相传递数据。
当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求。
我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB。
在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应。
核心思路就是写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象
- public static void saveUser(UserDTO user){
- tl.set(user);
- }
-
- public static UserDTO getUser(){
- return tl.get();
- }
-
- public static void removeUser(){
- tl.remove();
- }
-
-
- //保存用户信息到session中
- session.setAttribute("user", BeanUtils.copyProperties(user,UserDTO.class));
- //存在,保存用户信息到Threadlocal
- UserHolder.saveUser((UserDTO) user);
由于在第二台服务器上,没有第一台服务器存放的session
早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session
这样会造成每台服务器中都有完整的一份session数据,服务器压力过大,下面用redis优化
session是每个用户都有自己的session,但是redis的key是共享的,因此可以解决session共享问题
在设计这个key的时候,需要满足key具有唯一性且方便携带,选择在后台生成一个随机串token

- @Override
- public Result login(LoginFormDTO loginForm, HttpSession session) {
- // 1.校验手机号
- String phone = loginForm.getPhone();
- if (RegexUtils.isPhoneInvalid(phone)) {
- // 2.如果不符合,返回错误信息
- return Result.fail("手机号格式错误!");
- }
- // 3.从redis获取验证码并校验
- String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
- String code = loginForm.getCode();
- if (cacheCode == null || !cacheCode.equals(code)) {
- // 不一致,报错
- return Result.fail("验证码错误");
- }
-
- // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
- User user = query().eq("phone", phone).one();
-
- // 5.判断用户是否存在
- if (user == null) {
- // 6.不存在,创建新用户并保存
- user = createUserWithPhone(phone);
- }
-
- // 7.保存用户信息到 redis中
- // 7.1.随机生成token,作为登录令牌
- String token = UUID.randomUUID().toString(true);
- // 7.2.将User对象转为HashMap存储
- UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
- Map
userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), - CopyOptions.create()
- .setIgnoreNullValue(true)
- .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
- // 7.3.存储
- String tokenKey = LOGIN_USER_KEY + token;
- stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
- // 7.4.设置token有效期
- stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
-
- // 8.返回token
- return Result.ok(token);
- }
由于拦截器绑定了刷新登录token令牌的存活时间,如果当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行。
当前方案如下:

解决方案:添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可。

- // 第一个拦截器
- public class RefreshTokenInterceptor implements HandlerInterceptor {
-
- private StringRedisTemplate stringRedisTemplate;
-
- public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
- this.stringRedisTemplate = stringRedisTemplate;
- }
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 1.获取请求头中的token
- String token = request.getHeader("authorization");
- if (StrUtil.isBlank(token)) {
- return true;
- }
- // 2.基于TOKEN获取redis中的用户
- String key = LOGIN_USER_KEY + token;
- Map
- // 3.判断用户是否存在
- if (userMap.isEmpty()) {
- return true;
- }
- // 5.将查询到的hash数据转为UserDTO
- UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
- // 6.存在,保存用户信息到 ThreadLocal
- UserHolder.saveUser(userDTO);
- // 7.刷新token有效期
- stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
- // 8.放行
- return true;
- }
-
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- // 移除用户
- UserHolder.removeUser();
- }
- }
-
- // 第二个拦截器
- public class LoginInterceptor implements HandlerInterceptor {
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- // 1.判断是否需要拦截(ThreadLocal中是否有用户)
- if (UserHolder.getUser() == null) {
- // 没有,需要拦截,设置状态码
- response.setStatus(401);
- // 拦截
- return false;
- }
- // 有用户,则放行
- return true;
- }
- }
这节主要分析了项目的整体架构,redis部分处理了短信验证码和登录状态验证(代替session)