使用Spring Session做分布式会话管理

  在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,也就是一旦容器关闭,重启会导致会话失效。因此打造一个高可用性的系统,必须将session管理从容器中独立出来。而这实现方案有很多种,下面简单介绍下:

  第一种是使用容器扩展来实现,大家比较容易接受的是通过容器插件来实现,比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis等等。好处是对项目来说是透明的,无需改动代码。不过前者目前还不支持Tomcat 8,或者说不太完善。个人觉得由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。并且代码不在项目中,对开发者来说维护也是个问题。

  第二种是自己写一套会话管理的工具类,包括Session管理和Cookie管理,在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中。很显然这个方案灵活性最大,但开发需要一些额外的时间。并且系统中存在两套Session方案,很容易弄错而导致取不到数据。

  第三种是使用框架的会话管理工具,也就是本文要说的spring-session,可以理解是替换了Servlet那一套会话管理,既不依赖容器,又不需要改动代码,并且是用了spring-data-redis那一套连接池,可以说是最完美的解决方案。当然,前提是项目要使用Spring Framework才行。

  这里简单记录下整合的过程:

  如果项目之前没有整合过spring-data-redis的话,这一步需要先做,在maven中添加这两个依赖:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session</artifactId>
    <version>1.0.2.RELEASE</version>
</dependency>

  再在applicationContext.xml中添加以下bean,用于定义redis的连接池和初始化redis模版操作类,自行替换其中的相关变量。

<!-- redis -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
</bean>
 
<bean id="jedisConnectionFactory"
    class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    <property name="hostName" value="${redis.host}" />
    <property name="port" value="${redis.port}" />
    <property name="password" value="${redis.pass}" />
    <property name="timeout" value="${redis.timeout}" />
    <property name="poolConfig" ref="jedisPoolConfig" />
    <property name="usePool" value="true" />
</bean>
 
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
    <property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
 
<!-- 将session放入redis -->
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>

  这里前面几个bean都是操作redis时候使用的,最后一个bean才是spring-session需要用到的,其中的id可以不写或者保持不变,这也是一个约定优先配置的体现。这个bean中又会自动产生多个bean,用于相关操作,极大的简化了我们的配置项。其中有个比较重要的是springSessionRepositoryFilter,它将在下面的代理filter中被调用到。maxInactiveIntervalInSeconds表示超时时间,默认是1800秒。写上述配置的时候我个人习惯采用xml来定义,官方文档中有采用注解来声明一个配置类。

  然后是在web.xml中添加一个session代理filter,通过这个filter来包装Servlet的getSession()。需要注意的是这个filter需要放在所有filter链最前面

<!-- delegatingFilterProxy -->
<filter>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSessionRepositoryFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

  这样便配置完毕了,需要注意的是,spring-session要求Redis Server版本不低于2.8

  验证:使用redis-cli就可以查看到session key了,且浏览器Cookie中的jsessionid已经替换为session。

127.0.0.1:6379> KEYS *
1) "spring:session:expirations:1440922740000"
2) "spring:session:sessions:35b48cb4-62f8-440c-afac-9c7e3cfe98d3"

闪光灯那些事(二)

  自从走上玩灯的道路后,便一发不可收拾,俗话说人像玩灯,风景玩镜,而我更喜欢结合两者,烈日下依然可以压低天空亮度。同样喜欢用灯的朋友可以看看传说中的“一灯大师”Zack Arias的闪灯视频。

  由于刚入门的时候选择了一款小巧的Nissin i40,但该灯不支持主控,以及功率略低。仅作机顶使用到没啥问题,但要离机引闪就不行了,首先不支持手动,毕竟人家是需要转盘来控制的。其次闪光补偿幅度也很小,可能是本身功率就很小的缘故吧。于是打算换一支主控灯,首先当然是考虑国产灯了,便宜嘛。

  在闪客日记论坛中泡了几天后入了一支沃龙SP-600,此灯在国产货中口碑算是不错的,做工也还凑活,唯一不足的是在我永诺622c离机引闪下曝光过度,大概误差有2ev样子,但机顶使用是正常的,升级了622c-tx依旧如此,没办法只好退了。之后又购入了斯丹德DF-800,这货拿到手就感觉low到极致了,做工不是一般的粗糙,连扩散板都很难抠出来,打出来的光斑也是不规则,暗角超级大,感觉虚标了闪光指数,只好退货处理。两只灯都有一个共同的问题:连续全光输出几次后电池发热极其严重,取出来都烫手。灯头倒是不怎么热,我可是在空调房里面测试的,用的是爱乐普电池,真怕爆炸。。。至于永诺的灯,不敢再试了,网评烧的最多的就是它了,可人家销量高啊,大概都抱着坏了再换的打算而用的。

  最后还是选择了佳能自己的600ex-rt,做工啥的都不用说了,使用622c完美引闪,不愧为原厂最强灯,测试下来耗电也不算大,关键是电池也没见发烫,安全第一,太棒了。

  写下这篇文章纪录下折腾的一些心得,想在摄影这条路一直走下去的朋友,一步到位还是很有道理的,能省下不少时间,毕竟时间才是最宝贵的。

使用Java控制路由器获取公网IP

  不知道是公网IP不够用了,还是什么鬼原因,近期我这的联通ADSL拨号很大程度上获取的是一个10.开头的内网IP。虽说通常情况下无需关心,但跑PT,VPN等速度上大打折扣。投诉无果后只能自己写个脚本来自动更换IP。

  其原理很简单,模拟登录到路由器上检查WANIP是否是10.或0.开头,如是则断开重连,以此循环。代码是Java编写,无任何依赖,运行在树莓派上,24小时监视,在运营商完全分配内网IP之前还可以挣扎一阵子。有需要的朋友可以参考下。

  我这用的是TP-LINK WR720N路由器,设置了局域网IP为192.168.30.1 端口88,通过Chrome登录到路由器,可以在开发者工具中查看到Basic加密的Key,替换相应的位置即可。

查询IP的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm

断开拨号的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm?Disconnect=%B6%CF%20%CF%DF&wan=1

重新拨号的链接

http://192.168.30.1:88/userRpm/StatusRpm.htm?Connect=%C1%AC%20%BD%D3&wan=1

代码如下:

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
public class CheckIP {
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");
 
    public static void main(String[] args) throws Exception {
        do {
            String currentIP = getIP();
            if (currentIP.startsWith("10.") || currentIP.startsWith("0.")) {
                System.out.println(simpleDateFormat.format(new Date())
                        + " 检测到异常:" + currentIP);
                getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm?Disconnect=%B6%CF%20%CF%DF&wan=1");
                Thread.sleep(1000 * 1);
                getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm?Connect=%C1%AC%20%BD%D3&wan=1");
                Thread.sleep(1000 * 3);
            }
            Thread.sleep(1000 * 3);
        } while (true);
    }
 
    private static String getIP() throws Exception {
        String wanPara = getHtml("http://192.168.30.1:88/userRpm/StatusRpm.htm");
        if (null != wanPara) {
            wanPara = wanPara.substring(wanPara.indexOf("var wanPara"),
                    wanPara.length());
            wanPara = wanPara.substring(0, wanPara.indexOf(");") + 2);
        }
        return getFirstIp(wanPara);
    }
 
    private static String getHtml(String address) throws Exception {
        URL url = new URL(address);
        URLConnection connection = url.openConnection();
        connection.setRequestProperty("Authorization", "Basic YWRtaW46d3Npa3Nr");
        connection.connect();
        InputStream inputStream = null;
        StringBuffer stringBuffer = new StringBuffer();
        inputStream = connection.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(inputStream));
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            stringBuffer.append(line);
        }
        bufferedReader.close();
        inputStream.close();
        return stringBuffer.toString();
    }
 
    private static String getFirstIp(String packet) {
        Pattern p = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+");
        Matcher m = p.matcher(packet);
        if (m.find()) {
            return m.group();
        } else {
            return null;
        }
    }
}

编译

pi@raspberrypi ~ $ javac CheckIP.java

后台运行

pi@raspberrypi ~ $ nohup java CheckIP &

日志

pi@raspberrypi ~ $ tail -f nohup.out

StarTech 3.5寸移动硬盘盒

  随着拍摄的照片越来越多,一个500G的移动硬盘和Macbook的256G空间已经快塞满了,于是再购置了一块3.5寸台式机硬盘。由于是连接到Macbook使用,必须要再加装移动硬盘盒。

  要是没有特别要求,那这事就简单了,市面上硬盘盒一大把,十几上百的都有。可偏偏符合我要求的就少之又少,主要要求如下:

  1. 支持Mac USB 3.0 (5 Gbit/s)
  2. 支持硬盘4T以上
  3. 支持硬盘SATA III (6 Gbps)
  4. 支持UASP协议
  5. 支持智能休眠,磁盘卸载,自动停转
  6. 稳定,可长时间运行
  7. 仅需要1个盘位

  好吧,这样一来,没多少可选的了。本来早在今年4月份就在JD一起下单买了希捷2T和数据巴士S320I,结果S320I唯独不支持Mac系列的USB 3.0,表现为读取缓慢,并且掉盘,极其严重的问题,导致丢失了好几百张照片。后来看说明书上的系统支持里面Mac那一栏居然写着一串小字,不支持USB 3.0,但购买的时候网上介绍都没特别说明,只是概括的说支持Mac电脑,看来是信息没有及时更新。后来在一台Windows主机上测试,一切正常。数据巴士的做工真是无力吐槽了,明明设计是免工具插拔的,结果硬盘塞进去,非得用蛮力才能拔出,检查一看是一个塑料卡脚太粗了,死死的卡在硬盘螺丝孔内,导致难以取出,无奈只好退货。后来官方客服说是芯片太老了,新款有几个是可以的,不过不打算再试了。

  最后在美亚上找了一款,也就是现在测试的StarTech.com HDD Enclosure with UASP (S3510BMU33B),支持USB 3.0,SATA III硬盘,有散热风扇。虽说也不确定是不是知名度很高的品牌,不过这小众需求压根也没大厂看得上。从startech.com官网看了下,还挺正规的,产品种类齐全,各种参数一目了然,价格也不贵,最重要的是明确支持Macbook USB3.0和UAPS协议,邮件客服响应速度也挺快的。直邮过来,正好今天收到了,迫不及待的开箱测试了一番。通过Blackmagic旗下的Disk Speed Test软件测速,顺序读写均能稳定在200MB/S,结果大大出乎意料,基本上是目前机械硬盘的极限速度了。而之前在Windows上测试S320I也才100多的水平,看来UASP协议还是有点用的,据说这个是为SSD硬盘准备的。

  接下来就是迁移照片了,我习惯在Lightroom中导入并管理照片,一般情况下都会保留相机原始的RAW文件,所以体积比较大。很简单直接将源路径的照片剪切到移动盘,再通过Lightroom的查找丢失照片选择新的目录,就可以完成迁移了。粗略算了下,从10年到今天共计16451张照片,167.54GB,也算是个不小的战绩了。

IMG_0170IMG_0174speed

时代的车轮

  再一次看到百度空间即将关闭的提示,不得不表示遗憾。

  2007年至2010年间,正好是整个大学生涯,在百度空间上纪录了不少的东西。大约150篇的文章,虽说都是些折腾的纪录,在现在看来也没多少技术含量。不过恰逢和朋友吹水聊天的时候,偶尔触及一些话题也能让人发出“当年如何如何”的感概时候,这时候如果能找到当时的文章就显得有理有据了,吹牛逼格瞬间高了几个档次。当然更大的价值还是在于自己。现在的问题是要关闭了,各种原因就不用说了,总归是人去楼空,时代变了。百度也算做了个好事,都备份到网盘了,只不过只能自己浏览。也罢,免的我写脚本导入到这个博客。

  作为混迹在互联网上近十多年的“老江湖”也遇到过不少的服务曾经红极一时,而后随着大势起起伏伏,转型的,倒闭的不计其数。最初用过pjblog程序搭建过一个博客,也写过不少心的。而后微软的asp渐渐淡出,接班人php一路红火起来,于是我也转到wordpress阵营。在那个还不需要备案的年代,虚拟主机满天飞,质量也良莠不齐。用现在角度来看就是野蛮生长状态。放纵下的自由竞争,同时也富裕了一大批“站长”们。

  我想这是为免费付出的代价,倒不是说如果一开始收费就能一直存在下去,或许用户早就跑了。而是即便有付费用户,但整体用户使用率下降,产生的信息交换越来越少,不足以维持高额的运营成本,势必要转型或关闭了。互联网用户都是实实在在的人,不断成长,从一个圈子转移到另一个圈子。人们只会对新事物才有好奇心,时代的车轮总是不断的向前滚动,谁能说得定呢。任何看似大众的事物都有可能变的小众,这也是我一直对“云”的看法。在时间的长河中,产品如何求的生存,但我想一定不能只靠“大众”。

  ”It was the best of times, it was the worst of times.”