spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

       目前公司网站只有一个tomcat和mysql服务器,全部托管在阿里云服务平台,稳定运行三年,基本上没出现大的问题。所以,不要总觉得单点显的很低级,优化做好,一样没问题。还是那句话,适合自己的才是最重要的,我也相信业务驱动技术,不过由于公司的发展比较快,网站的查询效率就显得尤为重要。所以,预计搭建一个小的集群环境来承载大量用户同一时间来访问的问题,以前我这块使用ehcache来当作二级缓存,其实也很好,速度也非常的快,但是毕竟是插件类的,分布式的支持也没有nosql的多,于是考虑一下,还是用redis来做吧。

      先准备环境,项目采用spring+jpa+struct2的结构,前端采用nginx来做反向代理(nginx做反向代理,完全能够避免我们的应用程序直接暴露在用户的面前,同时能够做到动静分离,也就是说静态文件类似于图片,完全不用走程序,直接通过I/O留读取,效率可想而知,而且可以写一些shell脚本,阻断恶意用户的访问,使之进入不到程序入口),中间搭建一个redis缓存服务器,下设两个应用服务器tomcat,然后redis缓存服务器在阿里云的价格也不贵,一年一万多块钱足够,完全适合小型企业。思路是就是这样。

      准备开发工具,开发环境都在windows下进行,开发eclipse,下载nginx的window版本和reids的window版本,以及reids需要的jar包,本项目没有采用maven,所以jar包都在网上下载的,稍后我把需要的东西附件中上传,省着大家下载了。

       第一步,启动nginx,把下载的nginx放在c盘,然后进入命令行,找到对应的盘符,输入start nginx,启动完毕,时间太长,忘记了是否需要配置环境变量了,这个默认端口是80的,不过可以修改,修改文件在conf下的nginx.conf里面。

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

启动成功的页面效果,然后输入localhost,出现welcome nginx页面,说明启动成功

     第二步,修改nginx的配置文件,让它适合集群环境。打开nginx下的nginx.conf文件,修改地方在下图,第一个红线指的地方为两台tomcat服务器的地址,权重一样,第二个红线是监听的nginx的地址,也就是说,我只要访问192.168.1.118,那么nginx就会随机的派发请求到上面的两台服务器里面,修改完毕后进行保存。然后重启,记住,nginx的重启要nginx -s reload一下,否则重启不生效。至此,nigix的环境搭建完毕。

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

   第三步,搭建redis缓存服务器,启动reids服务器,点击如下图

     出现如下图

输入1,回车,启动成功,如果发现第一次能启动成功,但是用着用着就报内存不足的话,需要修改redis的配置文件,打开redis.conf文件,找到如下图的地方,按照红色箭头的格式进行添加

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

至此,reids缓存服务器也开启成功,那么选择一个可视化的工具吧,否则没办法查看redis到底存了什么东西。采用redis-desktop-manager这个工具,挺好用的,打开的效果如下图,非常简单,一看就知道怎么用

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

第四歩:分布式最先要解决的就是session问题,tomcat本身提供了广播机制来进行session的共享,但是这种方案,我并没有采取,本身就用redis作为缓存,那么就不用tomcat本身的广播机制了,再有,广播机制毕竟占有带宽,而且tomcat服务器一多,出现问题都不好定位。spring已经集成了redis来解决session共享问题。

需要的jar包如下,记住,版本一定要对,否则,启动会报各种找不到或者实例化的错误,jar包如下图添加即可。

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

commons-pool2-2.4.2

jedis-2.4.1.jar

spring-data-redis-1.4.1.RELEASE

spring-session-1.2.0.RELEASE

jar添加完毕后,需要在web.xml里面和spring的配置文件里面分别添加

记住,要加在所有过滤器的最前面,spring的配置文件里面加上的也要靠前,至于poolConfig可以先不加,后续会介绍。至此,spring +redis整合完毕,两个tomcat一起启动,启动之后,通过redis可视化工具可以发现session已经存入到redis里面,如下图。

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

第五步:改写Captcha的存储方式,本项目的验证码采用的是jcaptcha-1.6的验证码,这个验证码在单个tomcat里面没问题,因为它的生成规则是这样的,生成的验证码会存在jvm里面,这样,如果登录的时候是tomcat1,那么这个验证码就存在了tomcat1里面,而验证的时候正好是tomcat2,因为tomcat2里面根本没有存验证码,那么就会报验证码输入错误的问题了。为了解决这个问题,我百度了所有的方式,发现都不适合我的项目,于是,只能从源码下手。先说一下本项目生成验证码的方式比较简单,代码如下,就三个方法而已

/**
    
 * 生产校验码
    
 * @throws IOException
      */
     protected voidgenernateCaptchaImage() throws IOException {  
          response.setHeader("Cache-Control","no-store");  
        response.setHeader("Pragma","no-cache");  
        response.setDateHeader("Expires",0);  
        response.setContentType("image/jpeg");  
        ServletOutputStream out =response.getOutputStream();  
        try {  
            String captchaId =request.getSession(true).getId();  
            BufferedImage challenge =(BufferedImage)  CaptchaServiceSingleton.getInstance().getChallengeForID(captchaId,request.getLocale());  
            ImageIO.write(challenge,"jpg", out);  
            out.flush();  
        } catch (CaptchaServiceException e) {  
        } finally {  
            out.close();  
        }  

   } 

publicclass CaptchaServiceSingleton {  
     private static ImageCaptchaService instance = newDefaultManageableImageCaptchaService(  
             new FastHashMapCaptchaStore(),new RdImageEngine(), 180,  
             100000 , 75000);
   /*  private static ImageCaptchaService instance = newDefaultManageableImageCaptchaService(  
             new FastHashMapCaptchaStore(),new GMailEngine(), 180,  
             100000 , 75000); */ 
    public static ImageCaptchaService getInstance(){  
        return instance;  
    }  

}  

RdImageEngine类是继承了ListImageCaptchaEngine

验证验证码是否正确的代码

protectedboolean checkValidImg(String valid){
    
     if(isOpenValidCode()){
              boolean b=false;
              try {
                   b=CaptchaServiceSingleton.getInstance().validateResponseForID(request.getSession().getId(),valid.toLowerCase());
              } catch (CaptchaServiceException e) {
                   logger.debug(e.getMessage());
                   b=false;
              }
              return b;
          }else{
              return true;
          }
         
     }

是不是特别简单,可以仔细看代码发现,生成验证码和校验验证码的代码全部封装好了,无法修改。而网上的解决办法我都试了,不是没看明白就是根本不行,于是打开jcaptcha的源码进行查看,找到如下图的地方

\jcaptcha-src-1.0-RC6\service\src\java\com\octo\captcha\service\image就是这个路径

spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

找到DefaultManageableImageCaptchaService这个文件,至于原因,可以往上看,因为

privatestatic ImageCaptchaService instance = new DefaultManageableImageCaptchaService这个,所以要找到实现类,打开发现里面的代码是这样的

发现这里面还是没有getChallengeForID这个方法,为什么要找这个方法,因为要重写它,改变验证码的存储方式。

根据extends一层一层找,最终会找到原始方法的,这里就不找了,因为不管继承了多少层,DefaultManageableImageCaptchaService这个类里面一定继承了getChallengeForID这个方式,否则CaptchaServiceSingleton.getInstance().getChallengeForID(captchaId,request.getLocale()); 这个就不成立了,于是,重写这个类,让存储验证码的地方放在redis里面而不是jvm里面。重写的类如下



public class DefaultManageableImageCaptchaServiceChild extendsDefaultManageableImageCaptchaService {
    

    
      /*private RedisCacheUtilredisCacheUtil;
      public RedisCacheUtilgetRedisCacheUtil() {
          return redisCacheUtil;
     }
     public voidsetRedisCacheUtil(RedisCacheUtil redisCacheUtil) {
          this.redisCacheUtil = redisCacheUtil;
     }*/
     publicDefaultManageableImageCaptchaServiceChild() {}
     publicDefaultManageableImageCaptchaServiceChild(
              FastHashMapCaptchaStorefastHashMapCaptchaStore,
              RdImageEngine rdImageEngine, int i,int j, int k) {
           super(fastHashMapCaptchaStore,rdImageEngine, i,
                    j, k);
     }
     public ObjectgetChallengeForID(String ID, Locale locale)
                throws CaptchaServiceException {
             Captchacaptcha;
             Objectchallenge;
             captcha =(Captcha) RedisCacheUtil.get("captcha"+ID);
                //else get it
               // captcha = this.store.getCaptcha(ID);
//              captcha = storeRedis.getCaptcha(ID);
                if (captcha == null) {
                    captcha = generateAndStoreCaptcha(locale, ID);
                } else {
                    //if dirty
                    if (captcha.hasGetChalengeBeenCalled().booleanValue()) {
                        //get a new one and store it
                        captcha = generateAndStoreCaptcha(locale,ID);
                    } else {
                        //else nothing
                    }
                }
             
             //checkif has capthca
            /* if(!this.store.hasCaptcha(ID)) {
                //if not generate and store
                captcha = generateAndStoreCaptcha(locale, ID);
             } else {
                //else get it
                captcha = this.store.getCaptcha(ID);
                if (captcha == null) {
                    captcha = generateAndStoreCaptcha(locale, ID);
                } else {
                    //if dirty
                    if (captcha.hasGetChalengeBeenCalled().booleanValue()) {
                        //get a new one and store it
                        captcha = generateAndStoreCaptcha(locale,ID);
                    } else {
                        //else nothing
                    }
                }
             }*/
             challenge= getChallengeClone(captcha);
            captcha.disposeChallenge();


             returnchallenge;
         }
     public BooleanvalidateResponseForID(String ID, Object response)
            throws CaptchaServiceException {
            Captcha captcha;
            captcha = (Captcha)RedisCacheUtil.get("captcha"+ID);
        if (captcha==null) {
            throw newCaptchaServiceException("captcha is null ,please check baseAction or DefaultManageable...");
        } else {
            Boolean valid =captcha.validateResponse(response);
           RedisCacheUtil.del("captcha"+ID);
            return valid;
        }
    }
     public CaptchagenerateAndStoreCaptcha(Locale locale, String ID) {
             Captchacaptcha = engine.getNextCaptcha(locale);
            this.store.storeCaptcha(ID, captcha, locale);
            RedisCacheUtil.del("captcha"+ID);
            RedisCacheUtil.set("captcha"+ID, captcha, 600);
             returncaptcha;
         }

}

重新完之后再上面new对象的时候要写成子类

ImageCaptchaServiceinstance = new DefaultManageableImageCaptchaServiceChild(  
             new FastHashMapCaptchaStore(),new RdImageEngine(), 180,  

            100000 , 75000);

这样的话,生成验证码的地方就会调用子类了,而且会发现,上述地方只要是生成验证码和校验验证码的地方全部改成程redis来实现,至此验证码的存储地点改写完成

第六部  编写redisUtil的工具类,这块也就是在第四歩为什么要写poolConfig的原因了

编写RedisCacheUtil

publicclass RedisCacheUtil
{


    
@Autowired
     private staticRedisTemplate<String, Object> redisTemplate; 




    


     public RedisTemplate<String,Object> getRedisTemplate() {
          return redisTemplate;
     }








     public voidsetRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
          this.redisTemplate = redisTemplate;
     }
    
    
    /** 
     * 删除缓存 
     * @param key
可以传一个值 或多个 
     */  
    @SuppressWarnings("unchecked")  
    public static void del(String ... key){  
        if(key!=null&&key.length>0){  
            if(key.length==1){  
               redisTemplate.delete(key[0]);  
            }else{  
               redisTemplate.delete(CollectionUtils.arrayToList(key));  
            }  
        }  
    }  
      
    /** 
     * 普通缓存获取 
     * @param key 键 
     * @return 值 
     */  
    public static Object get(String key){  
        returnkey==null?null:redisTemplate.opsForValue().get(key);  

   }  

/** 
     *
普通缓存放入 
     * @param key
键 
     * @param value 值 
     * @return true成功 false失败 
     */  
    public static boolean set(String key,Object value) {  
         try {  
            redisTemplate.opsForValue().set(key,value);  
            return true;  
        } catch (Exception e) {  
            e.printStackTrace();  
            return false;  
        }  
          

   }  

}

这个类写完的时候,重启程序发现redisTemplate为null,没有注入进来。找到spring的配置文件,写下

<beanid="redisUtil"class="com.rongdu.tool.RedisCacheUtil">  
        <property name="redisTemplate"ref="redisTemplate" />   

   </bean>  

<beanid="redisTemplate"class="org.springframework.data.redis.core.RedisTemplate">
         <propertyname="connectionFactory" ref="connectionFactory" />
        
 <propertyname="keySerializer">
                <beanclass="org.springframework.data.redis.serializer.StringRedisSerializer"/>
         </property>
         <property name="valueSerializer">
             <beanclass="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
         </property>
         <propertyname="hashKeySerializer">
            <beanclass="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <beanclass="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
        </property>
        <propertyname="enableTransactionSupport"value="true"></property>  

    </bean> 

<beanid="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
           <property name="maxIdle"value="${redis.maxIdle}" />    
        <!--
连接池的最大数据库连接数  -->  
        <property name="maxTotal"value="${redis.maxTotal}" />  
        <!--
最大建立连接等待时间-->   
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>    
        <!--逐出连接的最小空闲时间 默认1800000毫秒(30分钟)-->  
        <propertyname="minEvictableIdleTimeMillis"value="${redis.minEvictableIdleTimeMillis}" />   
        <!--每次逐出检查时 逐出的最大数目 如果为负数就是 : 1/abs(n), 默认3-->  
        <property name="numTestsPerEvictionRun"value="${redis.numTestsPerEvictionRun}" />   
        <!--逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1-->  
        <propertyname="timeBetweenEvictionRunsMillis"value="${redis.timeBetweenEvictionRunsMillis}" />   
        <!--是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个-->    
        <property name="testOnBorrow"value="${redis.testOnBorrow}" />    
        <!--在空闲时检查有效性, 默认false  -->  
        <property name="testWhileIdle"value="${redis.testWhileIdle}" />   
      </bean>

因为要把这个类当作bean来进行管理,spring才会进行注入,而redisTemplate是java提供的操作redis的模板,模板里面写的都是一些配置参数,但是要区分StringRedisTemplate和redisTemplate,这两个是完全不一样的,我这块是这么弄的,如果存字符串对象,采用StringRedisTemplate,存储java对象采用redisTemplate,也就是说,需要把redisTemplate的配置文件全部复制一份,变成StringRedisTemplate。虽然两个模板不通用,但是保证了不侵入式的编码。而且加好备注,我相信没啥问题。然后重启项目,输入验证码发现如下图,验证码的key已经存入进来了,并且验证也通过。

 spring+redis实现集群,并且解决Captcha存储在一个jvm里面的问题

 

 至此,spring+redis并解决验证码存储问题全部完事,在实现的过程中,网上有些资料的确给人很大的帮助,但是要结合项目本身的情况,多看看实现的源码,所有的问题就都不复杂了。把所有的东西都上传了,如果没有分的话也自己自己去网上搜索资源,jar包的版本已经写了,然后redis和nginx以及可视化工具,jcaptcha的源码,需要的东西就这么多。

https://download.csdn.net/download/tianya0138/10303534