单服务很简单,一层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的实现类都有哪些?
如图,而主角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个点仅仅是一小部分。从项目架构到系统架构的思维转变,然后通过系统架构再深入到项目架构,再深入到代码当中,这个过程还是非常有趣味的。
热门评论
给老师点赞!思路非常清晰!
给老师点赞!思路非常清晰!