Demo_mmall v2.0 (四) Tomcat集群演进及使用Redis进行session重构实现单点登录

小谈mmall架构演进

上回书和上上回书说到redis的用法还有在代码里怎么操作Redis数据库,学完了得用啊。怎么用啊?这得从项目架构说起了。
mmall是一个简单的用SSM搭建起来的基本只能本地玩耍的电商DEMO,最简单的架构版本V1.0是这样婶的:Demo_mmall v2.0 (四) Tomcat集群演进及使用Redis进行session重构实现单点登录_第1张图片
user123访问网址发送请求,nginx把请求发送到Tomcat,Tomcat再去访问数据库或者ftpserver;session保存在Tomcat里;
如果是访问人少,一台服务器当然可以顶得住,请求服务器的多了,我们可以给这个服务器升级,提高它的纵向扩展能力:升级机器的内存,CPU,硬盘机械改固态但带来的还有成本指数级升高。
升级一台服务器成本高,那用几个普通的服务器做成集群不就可以了吗?于是,就有了下面的机构:Demo_mmall v2.0 (四) Tomcat集群演进及使用Redis进行session重构实现单点登录_第2张图片
这个架构版本,称为V1.1版本吧。
这个版本看上去没毛病,实际是不能使用的。想象下这个业务场景:userA登录请求,nginx发送给TomcatA;用户再进行下单请求,Nginx发送给TomcatB;B里没有session,A里有session,这里就会校验到用户未登录,实际上用户登录了,session保存在A里,请求的却是B。
既然这个版本不能满足业务需求,就继续升级吧,于是就有了下面的版本V2.0:
Demo_mmall v2.0 (四) Tomcat集群演进及使用Redis进行session重构实现单点登录_第3张图片
user无论请求到哪台服务器,我们都会把session信息放到redis session server上。tomcat服务器都会从Redis session server上读取session信息。
这样一来,Session登录信息存储及读取的问题就解决了。但是还有一个问题:服务器定时任务并发的问题。这个问题怎么产生的?怎么解决?请看下回分解。

  • todo 服务器定时任务并发问题的解决
    左下角的Token是个小彩蛋,下面会说到。
    Tomcat集群能提高服务的性能,并发能力,以及高可用性;
    实际中一台服务器只部署一个Tomcat,因为机器硬件有瓶颈(内存,硬盘IO等);
    一台Tomcat的HTTP线程池是有限的多个Tomcat,多个线程池,并发能力提高;
    活多了一个人做不完怎么办?摇人就是了。
    请求多了服务器要挂掉了怎么办?做成集群就是了。
空谈误国,代码兴邦。

代码解析

util,util还是Util。util是什么?是你写完永远不会去看源码拿来就用的东西。
cookie的path,domain属性没有深入研究,其实要是不做这个DEMO,cookie都碰不到。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class CookieUtil {

    private final static String COOKIE_DOMAIN = ".happymmall.com";
    private final static String COOKIE_NAME = "mmall_login_token";

    // 把request对象里的所有cookie保存到cookie数组里,遍历数组拿到指定的cookie后返回其值
    public static String readLoginToken (HttpServletRequest request) {
        Cookie[] cks = request.getCookies();
        if (cks != null) {
            for (Cookie ck : cks) {
                log.info("read cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    log.info("return cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }

    // 根据传入的tooken新建cookie对象,将其保存到response对象里
    public static void writeLoginToken (HttpServletResponse response, String token) {
        Cookie ck = new Cookie(COOKIE_NAME, token);
        ck.setDomain(COOKIE_DOMAIN);
        ck.setPath("/"); // 代表设置在根目录
        ck.setHttpOnly(true); // 不能通过脚本访问cookie
        // 如果maxage不设置,cookie就不会写入硬盘而是写在内存里,只在当前页面有效
        ck.setMaxAge(60 * 60 * 24 * 265);// -1 永久 单位是秒
        log.info("write cookieName:{}, cookieValue: {}", ck.getName(), ck.getValue());
        response.addCookie(ck);
    }

    // 从request里拿到cookie数组,找到指定的cookie,设置删除后添加到response里
    public static void delLoginToken (HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cks = request.getCookies();
        if (cks != null) {
            for (Cookie ck : cks) {
                if (StringUtils.equals(ck.getName(), COOKIE_NAME)) {
                    ck.setDomain(COOKIE_DOMAIN);
                    ck.setPath("/");
                    ck.setMaxAge(0);// 0为删除cookie
                    log.info("del cookieName:{}, cookieValue:{}", ck.getName(), ck.getValue());
                    response.addCookie(ck);
                    return;
                }
            }
        }
    }
}

接下来看个登录接口,看下是怎么实现单点登录的。
小白千万不要怕,心理千万不要这么想:(唉,单点登录啊,这是什么流弊的技术啊这个该怎么用啊等等心中一万个曹尼玛。)我只想说,娃儿,莫慌,莫慌。莫慌。
知道什么是服务器吧?服务器是什么?说白了就是一个电脑,不过性能高些而已。
知道什么是部署吧?部署是什么?说白了就是复制粘贴,把自己的本地代码放到服务器上运行跑起来。
有些词看起来高大上,其实并没有。有些听上去高大上的东西是骗投资人和消费者的,IT行业的高大上的东西,是老鸟们装逼用的。装逼是人类的刚需。
userA在登录操作的时候,nginx把请求分发到了tomcatA,在A里把cookie保存到客户端,用户信息保存到redis里。A服务器的cookie只能在A里使用,为了让集群服务器都能使用,cookie的域名(domain属性)在util里写死掉,这样所有以写死的域名结尾的域名都能访问该域名。多个服务器共享一个cookie,DEMO里单点登录用的还是共享cookie。

todo 研究下真正的单点登录:
https://blog.csdn.net/qq_40241957/article/details/88371061

对照着V1.0架构图和V2.0架构图,我们可以清楚的看到,原来的session是保存在tomcat服务器里的,现在放在了redis里面,redis成了"session server"。
V1.0的登录接口:

    @RequestMapping(value = "login.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse login (String username, String password, HttpSession session){
        ServerResponse response = iUserService.login(username,password);
        if (response.isSuccess()){
            session.setAttribute(Const.CURRENT_USER,response.getData());
        }
        return response;
    }

V2.0的登录接口:

    @RequestMapping(value = "login.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse login (String username, String password, HttpSession session,
                                       HttpServletResponse httpServletResponse){
                                       //HttpServletResponse httpServletResponse, HttpServletRequest httpServletRequest){
        //service-->mybatis-->dao
        ServerResponse response = iUserService.login(username,password);
        if (response.isSuccess()){
            // 重构后的对业务代码仍然有侵入
            // todo 使用spring session 进行解耦
            CookieUtil.writeLoginToken(httpServletResponse, session.getId());
            // 本来保存在session里的用户信息 放到数据库里 有效期 30min
            RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
        }
        return response;
    }

用户登录后,在页面上点点点,买买买,和后台交互的不亦乐乎,像这样:

    /**
     * 获取用户信息
     * @return
     */
    @RequestMapping(value = "get_user_info.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse getUserInfo(HttpServletRequest httpServletRequest){
        //User user = (User) session.getAttribute(Const.CURRENT_USER);
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if (StringUtils.isEmpty(loginToken)) {
            return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
        }
        String userJsonStr = RedisPoolUtil.get(loginToken);
        User user = JsonUtil.string2Obj(userJsonStr, User.class);
        if (user != null){
            return ServerResponse.createBySuccess(user);
        }
        return ServerResponse.createByErrorMessage("用户未登录,无法获取当前用户的信息");
    }

但是别忘了在登录的时候,我们redis保存的用户信息是有有效期的:

RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);

时间半小时。半小时啊半小时,也就是用户最多只能玩耍半小时过了半小时就得重新登录。怎么办?能不能改下代码,加个东西让用户每请求一次后台就重置下用户的有效期?
当然可以。那个东西叫拦截器:

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class SessionExporeFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    // 拦截器,用户登录后每次请求后台都会重置token有效期为30min
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if (StringUtils.isNotEmpty(loginToken)) {
            String userJsoinStr = RedisPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsoinStr, User.class);
            if (user != null) {
                RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

web.xml里也要配置下:

    
        sessionExpireFilter
        com.mmall.controller.common.SessionExporeFilter
    
    
        sessionExpireFilter
        *.do
    

好了,功能实现了,接下来该做什么呢?
奥,还有一个彩蛋。
这个彩蛋涉及到用户重置密码这个业务。
最简单的前台页面是,用户点击忘记密码,输入用户名,问题答案,点击提交后输入新的密码就能重置密码;在后台涉及到两个接口:忘记密码&重置密码。简单地回顾下逻辑:(详细的请移步mallV1.0)
mmallV1.0
1 忘记密码checkAnswer
传入用户名 问题 答案 校验
设置有有效期的 forgetToken token放在服务器的 GuavaCache 里
返回forgetToken
token的作用是 防止其他人拿到这个forgetToken去恶意请求接口修改他人的密码
2 重置密码 forgetResetPassword
传入用户名 新密码 forgetToken 校验
forgetToken超过了有效期就会被清除 以此校验前端传来的forgetToken
校验成功后 修改密码
现在集群来了,原来的不能再放到服务器上了:
1 用户点击忘记密码,请求服务器1,它将token保存在本服务器的 GuacaCache 里
2 用户提交新密码 请求服务器2,这个服务器里的 GuavaCache 里没有保存 第一步的 token 提交就会报错 token无效或者过期
怎么办?当然还是把将生成的token放到redis里:

   		   String forgetToken = UUID.randomUUID().toString();
           // 原来的 token 保存在服务器上
           //TokenCache.setKey(TokenCache.TOKEN_PREFIX+username,forgetToken);
           // 现在放到redis里面
           RedisPoolUtil.setEx(Const.TOKEN_PREFIX+username, forgetToken, 60*30);

用的时候也是从redis里读:

		//String token = TokenCache.getKey(TokenCache.TOKEN_PREFIX+username);
        String token = RedisPoolUtil.get(Const.TOKEN_PREFIX+username);

项目地址如下,分支用dev:

https://github.com/SilentJhin/mmall/tree/dev

收工。
自己写的东西一定是有不完善的地方(内容,技术,代码等等),如果有看不惯的,来评论。我一定回。

你可能感兴趣的:(项目DEMO)