本文的环境:
1.CentOs6.4 minimal
2.jdk1.7.0_71
3.Tomcat7.0.65
4.Nginx1.8
5.Redis3.0.5
6.vm11
环境配置不多说了,假设环境都已经装好。我虚拟机的ip地址是192.168.233.129 后面就不再解释了,直接用。
修改nginx配置信息 cd /usr/local/nginx/conf
vim nginx.conf
配置负载均衡 ,增加下面的配置
upstream localhost {
server localhost:8080;
server localhost:8081;
}
server的location 中增加
proxy_pass http://localhost;
ok 重启下nginx cd /usr/local/nginx/sbin
./nginx -s reload
接下来访问http://192.168.233.129
ok 能够正常访问,显示了tomcat默认页,表示配置成功。
nginx 负载均衡默认是用轮询的方式,为了能够看到每次请求都被转发到了不同的tomcat上的效果,我们分别在webapps下ROOT下增加test.jsp 简单的一句话来表示这是tomcat1还是tomcat2。
ok 增加好之后再访问http://192.168.233.129/test.jsp 就显示tomcat1或者tomcat2,刷新页面,就会交替显示tomcat1 tomcat2.
ok 如果能够交替显示表示负载均衡配置成功了。
ok 接下来就要解决session的问题了,打开F12看cookie,发现每次刷新都是新的sessionid,这样肯定是不行的,解决集群环境下的session共享有很多实现的方式,可以通过在upstream下配置 ip_hash;来根据ip指定到固定一台tomcat服务器来解决session的问题。配置完后之后再重启nginx 再刷新页面发现sessionid不变了,无论怎么刷新sessionid都不会变了,而且页面只显示tomcat1,说明现在是固定的访问tomcat1服务器了,而不是轮询的方式。
ok 到这里为止,我都非常的顺利,几乎没什么问题,但在配置redis来管理session的时候,问题就来了,而且还是相当的蛋疼,花了一天的时间终于解决了,也是我写这篇的主要原因。
ok 接下来我们要实现的是用轮询的方式实现session共享,本文用的方式是用redis来集中管理session,当一个请求过来,tomcat是去redis里找这个sessionid是否存在。
首先先把tomcat的依赖包分别拷到tomcat/lib目录下,我一开始用的包是
1.tomcat-redis-session-manager-1.2-tomcat-7.jar
2.jedis-2.1.0.jar
3.commons-pool-1.6.jar
然后分别修改tomcat下的context.xml配置文件,增加如下配置
<Valve className="com.radiadesign.catalina.session.RedisSessionHandlerValve" />
<Manager className="com.radiadesign.catalina.session.RedisSessionManager"
host="localhost"
port="6379"
database="0"
maxInactiveInterval="60" />
ok,配置好了,但是在启动tomcat的时候报错了,错误信息如下:
caused by: org.apache.catalina.LifecycleException: Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly.
借助有道词典看的懂错误的大概意思,但是不知道怎么解决,不用怕,把错误信息复制一下到网上查一下,肯定能解决,因为毕竟我又不是第一个人配置这玩意,别人肯定也会碰到这问题。一点都不用慌,嗯,抱着这样的心态上网查了半天,结果居然没有一个人碰到这问题,我简直日了狗了。
于是看看别人的博客看别人是怎么做的,
几乎所有的博客都说要去
tomcat-redis-session-manager库的主页
https://github.com/jcoleman/tomcat-redis-session-manager
下载源码,修改build.gradle的依赖包版本,改成你自己的版本,然后重新构建项目,用构建生成的jar包。
不就是要编译打包嘛,为什么要弄的这么麻烦,而且也没用过gradle来构建项目,所以我是直接把源码拷到eclipse,注意这个项目有很多分支和tags,有些比较早的版本是依赖common-pool的jar包,我是下的master的分支,所以加的是common-pool2的jar包,要不然会报找不到类的错误,把依赖包加进来,通过export jar文件,来生成jar包。到这里,我的jar包变成了下面的样子
1.common-pool2-2.4.2.jar
2.jedis-2.5.1.jar
3.tomcat-redis-session-manager-1.2-mine.jar(自己通过源码编译生成的)
ok,把这3个jar包去替换原来的3个jar,然后还要修改一下context.xml配置文件,否则会提示找不到类,因为master分支的包名变了,修改后的context.xml文件如下
<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"
host="localhost"
port="6379"
database="0"
maxInactiveInterval="60" />
重启tomcat,嗯,感觉这次肯定没问题了。
结果,还是报同样的错,我简直日了狗了,难道真的要用gradle来构建项目才行?这不科学啊,不就是编译几个java类打成jar包吗,改不改build.gradle依赖包版本应该没有半毛钱关系,又不会影响源代码。但是呢,实在没有办法了,还是决定试一下吧。我想gradle应该类似maven应该很简单吧,于是下了个gradle-2.8。通过修改build.gradle的依赖版本,通过 gradle bulid的命令来构建项目,这里也有一些坑,只改依赖版本,根本构建不成功,不过构建过程碰到的问题,网上都找到了
一个是需要把maven私服给注释了,因为没帐号密码,直接让它去maven中央仓库找,或者你可以配开源中国的,速度快点,不过也就才几个包,无所谓拉,懒的搞,另外一个是需要把 signing那一段注释掉。修改后build.gradle文件如下
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
group = 'com.orangefunction'
version = '2.0.0'
repositories {
mavenCentral()
}
compileJava {
sourceCompatibility = 1.7
targetCompatibility = 1.7
}
dependencies {
compile group: 'org.apache.tomcat', name: 'tomcat-catalina', version: '7.0.65'
compile group: 'redis.clients', name: 'jedis', version: '2.5.1'
compile group: 'org.apache.commons', name: 'commons-pool2', version: '2.4.2'
//compile group: 'commons-codec', name: 'commons-codec', version: '1.9'
testCompile group: 'junit', name: 'junit', version: '4.+'
testCompile 'org.hamcrest:hamcrest-core:1.3'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-all:1.9.5'
testCompile group: 'org.apache.tomcat', name: 'tomcat-coyote', version: '7.0.65'
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from 'build/docs/javadoc'
}
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
artifacts {
archives jar
archives javadocJar
archives sourcesJar
}
//signing {
// sign configurations.archives
//}
task copyJars(type: Copy) {
from configurations.runtime
into 'dist'
}
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
//repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
// authentication(userName: sonatypeUsername, password: sonatypePassword)
//}
//repository(url: "https://oss.sonatype.org/content/repositories/snapshots") {
// authentication(userName: sonatypeUsername, password: sonatypePassword)
//}
pom.project {
name 'tomcat-redis-session-manager'
packaging 'jar'
description 'Tomcat Redis Session Manager is a Tomcat extension to store sessions in Redis'
url 'https://github.com/jcoleman/tomcat-redis-session-manager'
issueManagement {
url 'https://github.com:jcoleman/tomcat-redis-session-manager/issues'
system 'GitHub Issues'
}
scm {
url 'https://github.com:jcoleman/tomcat-redis-session-manager'
connection 'scm:git:git://github.com/jcoleman/tomcat-redis-session-manager.git'
developerConnection 'scm:git:[email protected]:jcoleman/tomcat-redis-session-manager.git'
}
licenses {
license {
name 'MIT'
url 'http://opensource.org/licenses/MIT'
distribution 'repo'
}
}
developers {
developer {
id 'jcoleman'
name 'James Coleman'
email '[email protected]'
url 'https://github.com/jcoleman'
}
}
}
}
}
}
其实根本不需要改依赖版本,直接用默认的就可以了,原因我上面说过了。
ok,我们再用gradle build 来构建项目,这次终于成功了,看构建日志会发现,他会去下载依赖包,所以如果用gradle来构建的话,就不需要再另外去下载jar包了,,gradle已经帮你下载了,跟maven类似,也有个本地仓库,在用户目录下\.gradle\caches\modules-2\files-2.1,只要把这jar包拷过去就可以了。
ok 这下总可以了吧,把构建生成的jar包再替换tomcat中的。然后再重启
结果你猜怎么着,没错,还是报同样的错误,我简直又日了狗了,简直奔溃,因为网上根本搜不到这个问题的解决方案
Stack Overflow上也去查了。
试了各种的方法,还去专门深入了解了一下tomcat的原理,都没有什么卵用,好吧,只能根据错误提示,去源码里查原因了,找到出错的那一行,
/**
* Start this component and implement the requirements
* of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
@Override
protected synchronized void startInternal() throws LifecycleException {
super.startInternal();
setState(LifecycleState.STARTING);
Boolean attachedToValve = false;
for (Valve valve : getContainer().getPipeline().getValves()) {
if (valve instanceof RedisSessionHandlerValve) {
this.handlerValve = (RedisSessionHandlerValve) valve;
this.handlerValve.setRedisSessionManager(this);
log.info("Attached to RedisSessionHandlerValve");
attachedToValve = true;
break;
}
}
if (!attachedToValve) {
String error = "Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly.";
log.fatal(error);
throw new LifecycleException(error);
}
try {
initializeSerializer();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
log.fatal("Unable to load serializer", e);
throw new LifecycleException(e);
}
log.info("Will expire sessions after " + getMaxInactiveInterval() + " seconds");
initializeDatabaseConnection();
setDistributable(true);
}
看抛出的异常说明attachedToValve是false,说明if (valve instanceof RedisSessionHandlerValve) 这句是false。
ok,为了能够看清这个valve到底是什么东西,于是在这句之前加个日志打印一下看是什么,这里写日志的时候一开始把valve写成value了,我靠,我一直以为是value,长的太他妈像了。
for (Valve valve : getContainer().getPipeline().getValves()) {
log.info("valve======================>" + valve.getClass().getName());
if (valve instanceof RedisSessionHandlerValve) {
this.handlerValve = (RedisSessionHandlerValve) valve;
this.handlerValve.setRedisSessionManager(this);
log.info("Attached to RedisSessionHandlerValve");
attachedToValve = true;
break;
}
}
ok,改完之后,直接用eclipse的export 重新打成jar包来替换tomcat中的,这时候可以看到刚刚添加的日志
INFO: valve======================>org.apache.catalina.core.StandardContextValve
ok,valve 果然不是RedisSessionHandlerValve,看来valve没取到值,是不是在context.xml中配置有问题?
于是又去检查了一遍,发现RedisSessionHandlerValve类名没有写错,于是又去看了一下打印的日志。
这一次看,有惊天大突破,平时waring的日志啊什么的肯定都是不看的,但是这次发现这个waring有点特别
WARNING: No rules found matching 'Context/Value'.
总感觉报错跟这个有关,警告说没有匹配的规则,那应该还是配置文件的问题了,到这里的时候突然想到写日志的时候把valve写成value了,于是去官网确认了一下,到底是value还是valve。
好吧,果然是写错了,简直崩溃,看来下次配置文件应该用复制的,而不是自己手打。把value改成valve之后,再重启tomcat,ok,终于不报错了。把2台tomcat跑起来,测试下redis管理session的效果。
可以看到刷新页面,tomcat1和tomcat2交替显示,并且sessionid不变。
然后去redis 用命令看一下,keys * ,发现存了一个session,ok,到此Nginx负载均衡+Tomcat集群+Redis管理session终于配置成功。
既然是配置文件写错了,那我用之前的3个jar可以吗,于是又把最开始的3个jar包来替换tomcat中的,记得还要改下context.xml配置,因为包名改了,之前说过的。测试了之后发现一点问题也没有,完全可以用最开始的那3个jar包。
最后来个非常非常重要的总结,你前面可以都不看,但是总结你应该看看,因为很重要,所以字体要大点。
总结:用Tomcat通过redis来管理session所需要用到的依赖包tomcat-redis-session-manager.jar,完全没有必要自己去下载源码,然后修改build.gradle的依赖包的版本,再重新构建。真的,我不知道为什么博客几乎所有有关redis管理session的文章都说要自己重新构建,真的没必要。还有的人说如果用jedis-2.1.jar则要用commons-pool jar包,如果要jedis-2.5以上则要用commons-pool2的jar包,真的完全错误。我不知道他们自己有没有测试过。这里说下什么时候用commons-pool,什么时候用commons-pool2,因为commos-pool2的包名发生了变化,所以版本导不对会报找不到类异常。
你可以直接去github里下载已经编译好的jar包,比如tomcat-redis-session-manager-1.2-tomcat-7.jar,他里面依赖的是commons-pool版本的jar包,如果你想用commons-pool2,你可以把master分支源码拉下来自己打jar包,master里依赖的commons-pool2,看RedisSessionManager这个类也知道,
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.commons.pool2.impl.BaseObjectPoolConfig;
他导的是commons-pool2。
所以,最终你可以用我一开始用的3个jar包,也可以用我后来的3个jar包,我都测试过,都没问题。
ok,现在我把这两组jar包都上传,你们可以直接下载我的。
commons-pool2-2.4.2版本之所以比上面那个大那么多是因为,tomcat-redis-session-manager-1.2-mine.jar这个包把依赖包也打进来了,所以,应该只要拷这个到tomcat下就可以了,我没试过,有兴趣的可以试一下。