手记

Tomcat集群后引发的...

2017-11-22 23:59:4220644浏览

Geely

3实战 · 15手记 · 32推荐
Tomcat集群
Tomcat单服务"毛坯版"

单服务很简单,一层Nginx,首先Nginx主要职责给Tomcat一层反向代理。

在这个架构图中,Nginx第二次职责是给FTPServer指定的目录再做一层目录转发,保证上传上去的图片实时可以通过http协议访问到。

单服务架构先不用考虑集群碰到的各种问题

Tomcat集群"体验版"

那在架构演进过程中,首先演进成这样的架构的也是有的。这种架构每个Session还都是每个Tomcat实例自己来维护的。

那这个架构图中的首先要解决Session共享的问题,具体如何解决以及各种优缺点,请参考 https://www.imooc.com/article/17545 《大型项目架构演进过程及思考的点》 这篇手记,里面写的非常之详细。

Tomcat集群"正式版"

如图,在此架构图中,nginx使用的是轮询的负载均衡策略。session不交给tomcat自己管理,已经交由左侧的redis分布式集群来管理。那紧接着就要说一下,在从Tomcat单服务演进到Tomcat集群环境下(使用)目前一期项目-Java从零到企业级电商项目实战 碰到的各种问题

简单理解为,在Tomcat集群下,各个Tomcat服务是分布式的。所以必须要解决业务逻辑碰到的各种分布式的问题。下

架构演进到代码演进及解决方案

下面列出典型的三点来过一遍。

解决session共享问题

原生Redis+Cookie+Filter解决

首先通过Response写入Cookie一个登陆的“SessionId”,这里是用引号的,它并不是真正意义上Tomcat Servlet原生的SessionId。

代码片段如下:

public static void writeLoginToken(HttpServletResponse response,String token){
        Cookie ck = new Cookie(COOKIE_NAME,token);
        ck.setDomain(COOKIE_DOMAIN);
        ck.setPath("/");//代表设置在根目录
        ck.setHttpOnly(true);
        //单位是秒。
        //如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存。只在当前页面有效。
        ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久
        log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
        response.addCookie(ck);
    }

其中COOKIE_NAME和COOKIE_DOMAIN是根据实际项目,线上的域名来配置的,如果扩展开来讲,对于里面每个属性,在二级/三级域名下的读写问题是必须要细化讲的,这里暂时先不过多深入。举个栗子

    //X:domain=".happymmall.com"
    //a:A.happymmall.com            cookie:domain=A.happymmall.com;path="/"
    //b:B.happymmall.com            cookie:domain=B.happymmall.com;path="/"
    //c:A.happymmall.com/test/cc    cookie:domain=A.happymmall.com;path="/ee/cc/"
    //d:A.happymmall.com/test/dd    cookie:domain=A.happymmall.com;path="/ee/dd/"
    //e:A.happymmall.com/test       cookie:domain=A.happymmall.com;path="/ee"

例如我们的线上网站是http://www.happymmall.com,例如a和b他们之间是无法读取到对方的cookie的。而c和d的访问URL下均可以读取到小a...更多的详细解读在此次进阶课程当中。我们继续回到主线来讲。

写完Cookie之后就要通过Redis把用户登录的Session存储到Redis当中。然后通过"SessionId"来在Redis中做一个映射。

所以整体流程是在登录的时候写Cookie,写Redis,使用的时候读Cookie,读Redis,登出的时候删除Cookie,删除Redis中的session信息。

解决时间重置问题

Session在和服务器交互的时候有效期会重置,当然我们自己写了一个,代码片段如下

@Override
    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)){
            //判断logintoken是否为空或者"";
            //如果不为空的话,符合条件,继续拿user信息

            String userJsonStr = RedisShardedPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsonStr,User.class);
            if(user != null){
                //如果user不为空,则重置session的时间,即调用expire命令
                RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

Spring Session框架

Spring Session框架可以零侵入解决session共享的问题,当然也是通过redis。

官方文档:https://docs.spring.io/spring-session/docs/current/reference/html5/

Spring Session官方文档写的非常好,例子也很多,小伙伴们仔细阅读即可,这里简单介绍一下几个关键类及代码

DelegatingFilterProxy

        <filter-name>springSessionRepositoryFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

通过配置这个filter来做代理,它获取了委托的过滤器。通过源码结构可知

它继承了GenericFilterBean其中GenericFilterBean的init方法调用了DelegatingFilterProxy的initFilterBean(),那其实这里面就是获取委托的过滤器啦。并调用委托过滤器的doFilter()。

源码片段

    protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
        Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
        if (isTargetFilterLifecycle()) {
            delegate.init(getFilterConfig());
        }
        return delegate;
    }

然后注意看init的实现类都有哪些?观察蓝色背景OncePerRequestFilter这个类,名字起的很好。见名知意,继续看。

如图,而主角SessionRepositoryFilter正是继承了这个抽象类。通过Attribute判断是否已经被过滤。而这个Attribute的key就是

SessionRepository.class.getName();

先跳出来继续说。

另外两个主角就是

SessionRepositoryRequestWrapper

SessionRepositoryResponseWrapper

例如SessionRepositoryRequestWrapper继承了HttpServletWrapper,所以它可以在目前的HttpRequest上进行包装,重写了getSession方法。

那Spring Session是如何和Redis如何交互的呢,session的有效期如何配置呢?那就要看看

org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration

还有

org.springframework.session.data.redis.RedisOperationsSessionRepository

这两个类啦,例如下面的代码是描述spring session存储在redis的session的namespace的一个常量


    /**
     * The default prefix for each key and channel in Redis used by Spring Session.
     */
    static final String DEFAULT_SPRING_SESSION_REDIS_PREFIX = "spring:session:";

还有下面这段,通过Spring Schedule来清理过期session

    @Scheduled(cron = "0 * * * * *")
    public void cleanupExpiredSessions() {
        this.expirationPolicy.cleanExpiredSessions();
    }

而Cookie的注入等默认设置(当然自己修改也是ok的,我们的项目就会通过注入的方式修改它,例如domain、path等等)

org.springframework.session.web.http.DefaultCookieSerializer

这个类有很多写的有意思的地方,例如servlet3才开始支持httponly,源码是这么判断的

/**
     * Returns true if the Servlet 3 APIs are detected.
     *
     * @return whether the Servlet 3 APIs are detected
     */
    private boolean isServlet3() {
        try {
            ServletRequest.class.getMethod("startAsync");
            return true;
        }
        catch (NoSuchMethodException e) {
        }
        return false;
    }

那其实spring session框架是真的非常有意思,里面牵扯的点非常多,redis、cookie、session,wrapper,proxy等等,那这些呢详细的在 Java企业级电商项目架构演进之路Tomcat集群和Redis分布式课程 中也会详细来讲解的。

我们先跳出来,以上两种方式都行,也各有优缺点,第一种更灵活,第二种对业务的侵入性更低。

解决定时任务分布式调度问题

问题的原因是我们有定时关单的Job,单个Tomcat OK,没有任何问题,但是在集群环境下,Spring Schedule定时执行的时候,会都一起执行,然而我们只希望执行一个就可以啦,避免数据错乱和资源浪费。所以在集群环境下,这种case也必须解决。

那分布式锁如果搞不好非常容易造成死锁,所以这种场景下要格外细致。

以Redisson实现分布式锁为例。

    @Scheduled(cron="0 */1 * * * ?")//每1分钟(每个1分钟的整数倍)
    public void closeOrderTaskV4() throws InterruptedException {
        RLock lock = redissonManager.getRedisson().getLock(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
        boolean getLock = false;
        try {
            if(getLock = lock.tryLock(2,50, TimeUnit.SECONDS)){//trylock增加锁
                log.info("===获取{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK,Thread.currentThread().getName());
                int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour","2"));
                iOrderService.closeOrder(hour);
            }else{
                log.info("===没有获得分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
        }finally {
            if(!getLock){
                return;
            }
            log.info("===释放分布式锁:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            lock.unlock();
        }
    }

当然这个方法名字是closeOrderTaskV4,就代表着我们还有closeOrderTaskV1、closeOrderTaskV2、closeOrderTaskV3。架构演进,代码也是要演进的,从复杂到简单拆解,从不完美到完美这么一个过程,从原生实现到框架解析,代码不断升级优化,并对比其中业务场景,缺陷,优点这个过程还是非常有意思的。

解决本地guava cache迁移问题

问题的原因是Tomcat之前使用的guava cache,它只存在于tomcat实例上,tomcat及tomcat之间并不共享,所以必须迁移。否则负载均衡就TomcatA存储了guava cache,TomcatB想拿并拿不到...就尴尬了。

继续解决...

那在Tomcat集群环境下根据实际的业务场景,会有很多地方深入到代码的演进,以上3个点仅仅是一小部分。从项目架构到系统架构的思维转变,然后通过系统架构再深入到项目架构,再深入到代码当中,这个过程还是非常有趣味的。

67人推荐
随时随地看视频
慕课网APP

热门评论

给老师点赞!思路非常清晰!

给老师点赞!思路非常清晰!

查看全部评论