美文网首页服务端开发实战程序员微服务&架构
集群间如何实现session共享【面试+工作】

集群间如何实现session共享【面试+工作】

作者: Java帮帮 | 来源:发表于2018-04-19 08:58 被阅读33次

    一、引言

    针对企业,为了应对庞大的用户访问压力,目前大多数大型网站服务器都采用集群部署的方式;针对个人,仅一台服务器而言,也会安装多个tomcat进行错时更新,保证更新后台业务时服务不断开,即模拟了集群的运行方式。在此集群中,我们就不得不考虑一个用户鉴权的问题,即在不同服务上如何保证用户均已登录,并能获取相同的用户登录信息。

    二、Java Web推荐的(公认的)用户鉴权机制

    说此部分之前先了解几个概念:

    1.请求,即Request,指客户端向服务器发送的信息,通常是通信的发起方;

    2.响应,即Response,指服务器对请求的应答,通常是通信的回复方;

    3.会话,即Session,服务器可将请求<->响应这一个完整的过程称为一次会话,并为这次会话生成一个唯一的标识符,即sessionId,用来表示这次会话,Session储存在服务器端;

    4.Cookie,客户端保存在本地终端的数据,即Cookie储存在客户端。

    Java Web的共用的用户鉴权机制是采用Session-Cookie技术,实现原理是:用户登录时,请求到达服务器,服务器调用通过getSession()方法判断session是否存在,如果不存在,则新建session,并通过其算法为session生成一个随机数作为sessionId,开发者可在session中储存一些用户信息;第二次请求时,如获取用户信息,getSession()方法判断session存在,则取出session,而不是新建,从而从session中获取到用户的相关信息。

    客户端请求时,可以将cookie信息储存于request的head中发送给服务器;

    服务器响应时,可以将cookie信息置于response中回传给客户端。

    如下图代表,名称为test的cookie其值为aaa:

    那么getSession()里究竟做了什么?

    1.第一次用户请求,客户端本地没有任何数据,即其cookie为空,朝服务器发送request,getSession()中会解析request,发现其约定的cookie为null,则认为没有session,所以会重新创建一个session对象;

    2.创建session后会将此session的id放入response中,回传给客户端,客户端则保存response中的cookie;

    3.再次请求,服务器getSession()又会重新解析request获取cookie,发现了其中的sessionId,那么根据此sessionId去服务器的中去找,则得到了上次创建的session对象,那么则认为鉴权成功。

    如此,便完成了鉴权的整个流程,Java逻辑代码(伪代码)如下:

    如上,java中将sessionId在cookie中保存的名称叫做“JSESSIONID”,即“Java Session Id”之意,打开浏览器可以看到类型的信息,如图:

    三、集群间如何实现session共享

    按照前文所说的session-cookie机制,session是保存在每台服务器的,但在集群中,拥有多台服务器,每台各自为政,势必会造成在这台服务器中登录,获取session成功,但是到另一台服务器上,又会获取不到session,造成鉴权失败,这样对用户来说是极不友好的,那么怎么解决这个问题呢?

    通过我们以上的分析,即可得出几种处理方式:

    A.找一块公共的空间用来储存session,而不是将session储存在集群节点的某台服务器上,此时,每一台服务器都能访问这块空间,从而实现session共享;

    B.仍在每台服务器上保存session信息,不作修改,但采用另一种同步机制,实时同步没一台服务器的session信息;

    C.构建一种全新的鉴权机制,不采用session-cookie机制,但要去除此鉴权机制对单个服务器的依赖。

    综上所述,列举几种的具体实现方案:

    1.持久化session到数据库,即使用数据库来储存session。数据库正好是我们普遍使用的公共储存空间,一举两得,推荐使用mysql数据库,轻量并且性能良好。

    优点:就地取材,符合大多数人的思维,使用简单,不需要太多额外编码工作

    缺点:对mysql性能要求较高,访问mysql需要从连接池中获取连接,又因为大部分请求均需要进行登录鉴权,所以操作数据库非常频繁,当用户量达到一定程度之后,极易造成数据库瓶颈,不适用于处理高并发的情况。

    2.使用redis共享session。redis是一个key-value的储存系统。可以简单的将其理解为一个数据库,与传统数据库的区别是,它将数据储存于内存中,并自带有内存到硬盘的序列化策略,即按策略将内存中的数据同步到磁盘,避免数据丢失,是目前比较流行的解决方案。

    优点:无需增加数据库的压力,因为数据存储于内存中,所以读取非常快,高性能,并能处理多种类型的数据。

    缺点:额外增加一些编码,以便操作redis。

    3.使用memcache同步session,memcache可以实现分布式,可将服务器中的内存组合起来,形成一个“内存池”,以此充当公共空间,保存session信息。

    优点:数据储存在内存中,读取非常快,性能好;

    缺点:memcache把内存分成很多种规格的存储块,有大有小,不能完全利用内存,会产生内存碎片,浪费资源,如果储存块不足,还会产生内存溢出。

    4.通过脚本或守护进程在多台服务器之间同步session。

    优点:实现了session共享;

    缺点:对个人来说实现较为复杂,速度不稳定,有延时性,取决于现实中服务运行状态,偶然性较大,如果用于访问过快,可能出现session还没同步成功的情况。

    5.使用NFS共享session。NFS是Network File Server共享服务器的简称,最早由Sun公司为解决Unix网络主机间的目录共享而研发。选择一台公共的NFS做共享服务器,储存所有session数据,每台服务器所需的session均从此处获取。

    优点:较好的实现了session共享;

    缺点:成本较高,对于个人来说难以实现。NFS依托于复杂的安全机制和文件系统,因此并发效率不高。

    6.使用Cookie共享session。此方案可以说是独辟蹊径了,将分布式思想用到了极致。如上文分析所说,session-cookie机制中,session与cookie相互关联,以cookie做中转站,用来找到对应的session,其中session存放在服务器。那么如果将session中的内容存放在cookie中呢,那么则省略了服务器保存session的过程,后台只需要根据cookie中约定的标识进行鉴权校验即可。

    优点:完美的贯彻分布式的理念,将每个用户都利用起来,无需耗费额外的服务器资源;

    缺点:受http协议头长度限制,cookie中存储的信息不宜过多;为了保持cookie全局有效,所以其一般依赖在根域名下,所以基本上所有的http请求都需要传递cookie中的这些标记信息,所以会占用一些服务器的带宽;鉴权信息全存储于cookie中,cookie存在于客户端,服务器并没有储存相关信息,cookie存在着泄露的可能,或则其他人揣摩出规则后可以进行伪装,其安全性比其他方案差,故需要对cookie中信息进行加密解密,来增强其安全性。

    在此,我们将选择方案2使用redis来具体实现集群下的session共享。

    四、搭建测试环境

    1.为模拟集群环境,需要两台服务器或在一台服务器上安装两个tomcat;

    2.使用nginx做集群纷发;

    3.安装redis充当公共的空间存储session;

    4.框架中编写session储存业务,因为需要使用java操作redis,redis提供了驱动包jedis,故需要掌握jedis进行操作。

    五、详细部署

    5.1 安装多个tomcat

    怎么安装tomcat此处不作说明,只说明安装额外的tomcat,本人原安装的tomcat目录为apache-tomcat-7.0.77

    1.拷贝apache-tomcat-7.0.77为apache-tomcat-7.0.77_2

    2.修改apache-tomcat-7.0.77_2下conf中server.xml文件端口号

    ,共三处,将每处在原端口号port之上加1,确保两个tomcat不会共用端口,如下:

    5.2 更改nginx配置,模拟集群

    修改nginx配置文件nginx.conf文件,在server闭包外添加upstream,由上可知两个tomcat端口号分别为8080,8081

    5.2 redis安装与配置

    1.下载,官网:https://redis.io/download

    2.安装,以4.0.1版本为例

    3.启动

    4.关闭

    5.配置后台启动(redis默认是前台启动,启动成功后界面就持续停止在那个界面上,这对服务器操作很不方便)

    如下图:

    6.后台启动

    如图:

    7.关闭

    杀掉redis进程,如图:

    8.为redis配置系统服务,本人使用的系统是CentOS 7,需要配置使用systemctl进行管理。

    /lib/systemd/system目录下创建文件redis.service,并编辑:

    更多redis systemctl详细配置,看最后扩展

    配置成功,启动完成后,通过服务可知其运行状态,如图:

    至此,redis已全部安装部署完成。

    六、编写代码实现功能

    为了测试简便,后台web框架我选择的是JFinal,JFinal是中国开源社区中广受好评的后台轻量级极速web框架,因其操作简单,设计灵活而被大多数开发者所喜爱,有兴趣的朋友可以试试,用一次之后你就会喜欢它的。

    这里用JFianl的另一个好处就是JFinal核心库中自带Redis插件,集成了jedis的各种使用方法,这样就不用自己去编写了,省了很大的代码量。Jedis基本操作:看最后扩展

    为帮助理解代码,Jfinal中连接redis,只需要在主配置文件中编写:

    redis存取数据:

    正式代码如下,我们将会自定义session,每个sesison对象都是唯一的,需要给每个session分配一个唯一id,id生成算法,则可以借用UUID实现,UUID相关介绍:https://baike.baidu.com/item/UUID/5921266?fr=aladdin 

    自定义随机数工具类:

    自定义RedisSession类,将替代原来的HttpSession:

    仿造getSession()实现逻辑在控制器基类BaseController中自定义getSession()方法,获取RedisSession:

    说明:

    以上代码中,设想服务器给移动端和网页端同时提供服务,为了优化,但我希望移动端不需要频繁登录,就像微信一样,我将这个时间暂设一周;而网页端的话,session生存周期较短,只有半个小时,并且每次鉴权都刷新其可用时间,移动端只倒计时就可以了,一周登录一次就可以了。redis自带有过期策略,可以很好的实现这一点,同时为了保险起见,也手动验证了一下如过期,进行删除。为了避免初次请求时,多次调用getSession()生成多个session,故在创建session成功后记录其sessionId,再次调用getSession()时可对其进行验证。

    七、结果测试

    1.在Controller中编写两个接口,一为登录接口,登录成功,储存用户uid;二为验证登录接口,获取登录信息:

    2.配置nginx分别跳转到不同tomcat下的不同接口

    3.开启redis,nginx,两个tomcat下运行同样的项目,在浏览器中调用接口进行测试。

    调用tomcat1的登录接口

    日志:

    调用tomcat2的登录接口 

    日志: 

    可以看到,两个tomcat中的信息完全一样,很好的达到了我们预计的效果。

    到这里,本篇的内容也已经到了尾声,写的有点啰嗦,不过总算交代了来龙去脉,虽然有点累,但好歹写完了。未来还有很多工作要做,路漫漫其修远兮,吾将上下而求索。

    扩展一:Java操作redis

    在我们已安装Redis的基础之上(数据库测试环境Redis安装在/opt/redis中),将Redis添加到系统服务中去。

    配置过程:

    进入/usr/lib/systemd/system/目录中,创建redis.service文件:

    文件创建好保存之后,要执行systemctl daemon-reload命令,使配置生效。

    开机启动:systemctl enable mongodb.service

    查看mongodb服务是否设置开机启动:systemctl is-enabled mongodb.service

    停止mongodb服务开机启动:systemctl disable mongodb.service

    启动:systemctl start mongodb.service

    查看状态:systemctl status mongodb.service

    重启:systemctl restart mongodb.service

    停止:systemctl stop mongodb.service

    扩展二:Java操作redis

    一、server端安装

    1、下载

    https://github.com/MSOpenTech/redis

    可看到当前可下载版本:redis2.6

    下载windows平台文件:

    解压后,选择当前64位win7系统对应的版本:

    2、安装

    1)解压后将里面所有文件拷贝至redis安装目录:

    几个exe程序的功能:    

    redis-benchmark.exe:性能测试,用以模拟同时由N个客户端发送M个 SETs/GETs 查询 (类似于 Apache 的ab 工具).

    redis-check-aof.exe:更新日志检查

    redis-check-dump.exe:本地数据库检查

    redis-cli.exe:客户端

    redis-server.exe:服务端 

    2)将路径添加至系统环境变量:过程根据操作系统配置(略)

    3)cmd下启动redis-server

    注:由于此处未指定配置文件,系统采用默认参数

    3、下载对应的配置文件

    由上面cmd输出可知,当前版本为2.6.12

    原页面上打开所有发布版本,找到2.6.12:

    下载zip文件:

    拷贝出redis.conf配置文件至安装目录:

    配置文件redis.conf 各字段含义:

    daemonize yes  #---默认值no,该参数用于定制redis服务是否以守护模式运行。---

    pidfile /var/run/redis.pid  #默认值/var/run/redis.pid,指定redis服务的进程号文件路径,以守护模式运行时需要配置本参数;

    port 6379   #默认值6379,指定redis服务的端口

    # bind 127.0.0.1  #绑定ip,默认是本机所有网络设备;

    timeout 0   #客户端空闲n秒后断开连接;默认是 0 表示不断开。

    loglevel notice  ###设置服务端的日志级别,有下列几种选择:

    debug:记录详细信息,用于开发或调试;

    verbose:提供很多有用的信息,但是又不像debug那么详尽,默认就是这一选项;

    notice:适度提醒,多用于产品环境;

    warning:仅显示重要的警告信息;

    logfile ""   ##指定日志的输出路径,默认值stdout,表示输出到屏幕,守护模式时则输出到/dev/null;

    如果要输出日志到syslog中,可以启动syslog-enabled yes,默认该选项值为no。

    # syslog-enabled no

    databases 16   ###指定数据库的数量,默认为16个,默认使用的数据库是DB 0。

    ##########SNAPSHOTTING  ############

    ----以下为快照相关的设置:------

    #   save  ##指定多长时间刷新快照至磁盘,这个选项有两个属性值,只有当两个属性值均满足时才会触发;可以设置多种级别,例如默认的参数文件中就设置了:

    save 900 1:每900秒(15分钟)至少一次键值变更时被触发;

    save 300 10:每300秒(5分钟)至少10次键值变更时被触发;

    save 60 10000:每60秒至少10000次键值变更时被触发;

    save 900 1

    save 300 10

    save 60 10000

    rdbcompression yes  ##默认值yes,当dump数据库时使用LZF压缩字符串对象,如果CPU资源比较紧张,可以设置为no,选择不压缩;

    rdbchecksum yes

    # The filename where to dump the DB  数据库文件名

    dbfilename dump.rdb  ##默认值dump.rdb,dump到文件系统中的文件名

    dir /usr/local/redis/db  ##默认值./,即当前目录,dump出的数据文件的存储路径;

    ############ REPLICATION ################

    ----以下为复制相关的设置,复制默认是不启用的,因此在默认的参数文件下列表参数均被注释----

    # slaveof  ##指定主端ip和端口,用于创建一个镜像服务

    # masterauth  ##如果master配置了密码的话,此处也需做设置;

    slave-serve-stale-data yes  ##默认值yes。当slave丢失与master端的连接,或者复制仍在处理,那么slave会有下列两种表现:

    当本参数值为yes时,slave为继续响应客户端请求,尽管数据已不同步甚至没有数据(出现在初次同步的情况下);

    当本参数值为no时,slave会返回"SYNC with master in progreee"的错误信息;

    slave-read-only yes  ##默认从Redis是只读模式

    # repl-ping-slave-period 10  ###默认值10,指定slave定期ping master的周期;

    # repl-timeout 60  ##默认值60,指定超时时间。注意本参数包括批量传输数据和ping响应的时间。

    ############## SECURITY #############

    ------以下为安全相关的设置------

    # requirepass foobared  ###指定一个密码,客户端连接时也需要通过密码才能成功连接;

    # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52  ###重定义命令,例如将CONFIG命令更名为一个很复杂的名字:

    # rename-command CONFIG ""  取消这个命令;

    #################LIMITS #################

    -----以下为资源限制方面的设置------

    # maxclients 10000  ##指定客户端的最大并发连接数,默认是没有限制,直到redis无法创建新的进程为止,设置该参数值为0也表示不限制,如果该参数指定了值,当并发连接达到指定值时,redis会关闭所有新连接,并返回'max number of clients reached'的错误信息;

    # maxmemory  ###设置redis最大可使用内存。当达到最大内存后,redis会尝试按照设置的回收策略删除键值。如果无法删除键值,或者保留策略设置为不清除,那么redis就会向发出内存的请求返回错误信息。当把redis做为一级LRU的缓存时本参数较为有用。

    # maxmemory-policy volatile-lru  ###默认值volatile-lru,指定清除策略,有下列几种方法:

    volatile-lru -> remove the key with an expire set using an LRU algorithm

    allkeys-lru -> remove any key accordingly to the LRU algorithm

    volatile-random -> remove a random key with an expire set

    allkeys->random -> remove a random key, any key

    volatile-ttl -> remove the key with the nearest expire time (minor TTL)

    noeviction -> don't expire at all, just return an error on write operations

    # maxmemory-samples 3    ###默认值3,LRU和最小TTL策略并非严谨的策略,而是大约估算的方式,因此可以选择取样值以便检查。

    ################ APPEND ONLY MODE ##############

    -----以下为APPEND的配置----

    ONLY模式的设置,默认情况下redis采用异步方式dump数据到磁盘上,极端情况下这可能会导致丢失部分数据(比如服务器突然宕机),如果数据比较重要,不希望丢失,可以启用直写的模式,这种模式下redis会将所有接收到的写操作同步到appendonly.aof文件中,该文件会在redis服务启动时在内存中重建所有数据。注意这种模式对性能影响非常之大。

    appendonly no  ##默认值no,指定是否启用直写模式;

    # appendfilename appendonly.aof  ###直写模式的默认文件名appendonly.aof

    appendfsync:调用fsync()方式让操作系统写数据到磁盘上,数据同步方式,有下列几种模式:

    always:每次都调用,比如安全,但速度最慢;

    everysec:每秒同步,这也是默认方式;

    no:不调用fsync,由操作系统决定何时同步,比如快的模式;

    no-appendfsync-on-rewrite:默认值no。当AOF fsync策略设置为always或everysec,后台保存进程会执行大量的I/O操作。某些linux配置下redis可能会阻塞过多的fsync()调用。

    auto-aof-rewrite-percentage:默认值100

    auto-aof-rewrite-min-size:默认值64mb

    # appendfsync always

    appendfsync everysec

    # appendfsync no

    ################# ADVANCED CONFIG ##################

    -----以下为高级配置相关的设置----

    hash-max-zipmap-entries:默认值512,当某个map的元素个数达到最大值,但是其中最大元素的长度没有达到设定阀值时,其HASH的编码采用一种特殊的方式(更有效利用内存)。本参数与下面的参数组合使用来设置这两项阀值。设置元素个数;

    hash-max-zipmap-value:默认值64,设置map中元素的值的最大长度;这两个

    list-max-ziplist-entries:默认值512,与hash类似,满足条件的list数组也会采用特殊的方式以节省空间。

    list-max-ziplist-value:默认值64

    set-max-intset-entries:默认值512,当set类型中的数据都是数值类型,并且set中整型元素的数量不超过指定值时,使用特殊的编码方式。

    zset-max-ziplist-entries:默认值128,与hash和list类似。

    zset-max-ziplist-value:默认值64

    activerehashing:默认值yes,用来控制是否自动重建hash。Active rehashing每100微秒使用1微秒cpu时间排序,以重组Redis的hash表。重建是通过一种lazy方式,写入hash表的操作越多,需要执行rehashing的步骤也越多,如果服务器当前空闲,那么rehashing操作会一直执行。如果对实时性要求较高,难以接受redis时不时出现的2微秒的延迟,则可以设置activerehashing为no,否则建议设置为yes,以节省内存空间。

    ps:如果配置文件的版本和当前exe程序的版本不匹配,很容易

    由于参数不一致导致server端无法启动。

    4、通过指定的配置文件启动服务端

    5、客户端访问测试

    另起一个cmd窗口:

    二、基本功能测试

    1、程序基本结构

    2、主要类

    1)功能类

    2)测试类

    3、各个功能函数

    1)key功能

    运行结果:

    2)String功能

    运行结果:

    3)List功能

    运行结果:

    4)Set功能

    运行结果:

    5)SortedSet功能(有序集合)

    运行结果:

    6)Hash功能

    运行结果:

    三、常用命令

    1)连接操作命令

    quit:关闭连接(connection)

    auth:简单密码认证

    help cmd: 查看cmd帮助,例如:help quit

    2)持久化

    save:将数据同步保存到磁盘

    bgsave:将数据异步保存到磁盘

    lastsave:返回上次成功将数据保存到磁盘的Unix时戳

    shundown:将数据同步保存到磁盘,然后关闭服务

    3)远程服务控制

    info:提供服务器的信息和统计

    monitor:实时转储收到的请求

    slaveof:改变复制策略设置

    config:在运行时配置Redis服务器

    4)对value操作的命令

    exists(key):确认一个key是否存在

    del(key):删除一个key

    type(key):返回值的类型

    keys(pattern):返回满足给定pattern的所有key

    randomkey:随机返回key空间的一个

    keyrename(oldname, newname):重命名key

    dbsize:返回当前数据库中key的数目

    expire:设定一个key的活动时间(s)

    ttl:获得一个key的活动时间

    select(index):按索引查询

    move(key, dbindex):移动当前数据库中的key到dbindex数据库

    flushdb:删除当前选择数据库中的所有key

    flushall:删除所有数据库中的所有key

    5)String

    set(key, value):给数据库中名称为key的string赋予值value

    get(key):返回数据库中名称为key的string的value

    getset(key, value):给名称为key的string赋予上一次的value

    mget(key1, key2,…, key N):返回库中多个string的value

    setnx(key, value):添加string,名称为key,值为value

    setex(key, time, value):向库中添加string,设定过期时间time

    mset(key N, value N):批量设置多个string的值

    msetnx(key N, value N):如果所有名称为key i的string都不存在

    incr(key):名称为key的string增1操作

    incrby(key, integer):名称为key的string增加integer

    decr(key):名称为key的string减1操作

    decrby(key, integer):名称为key的string减少integer

    append(key, value):名称为key的string的值附加value

    substr(key, start, end):返回名称为key的string的value的子串

    6)List

    rpush(key, value):在名称为key的list尾添加一个值为value的元素

    lpush(key, value):在名称为key的list头添加一个值为value的 元素

    llen(key):返回名称为key的list的长度

    lrange(key, start, end):返回名称为key的list中start至end之间的元素

    ltrim(key, start, end):截取名称为key的list

    lindex(key, index):返回名称为key的list中index位置的元素

    lset(key, index, value):给名称为key的list中index位置的元素赋值

    lrem(key, count, value):删除count个key的list中值为value的元素

    lpop(key):返回并删除名称为key的list中的首元素

    rpop(key):返回并删除名称为key的list中的尾元素

    blpop(key1, key2,… key N, timeout):lpop命令的block版本。

    brpop(key1, key2,… key N, timeout):rpop的block版本。

    rpoplpush(srckey, dstkey):返回并删除名称为srckey的list的尾元素,

    并将该元素添加到名称为dstkey的list的头部

    7)Set

    sadd(key, member):向名称为key的set中添加元素member

    srem(key, member) :删除名称为key的set中的元素member

    spop(key) :随机返回并删除名称为key的set中一个元素

    smove(srckey, dstkey, member) :移到集合元素

    scard(key) :返回名称为key的set的基数

    sismember(key, member) :member是否是名称为key的set的元素

    sinter(key1, key2,…key N) :求交集

    sinterstore(dstkey, (keys)) :求交集并将交集保存到dstkey的集合

    sunion(key1, (keys)) :求并集

    sunionstore(dstkey, (keys)) :求并集并将并集保存到dstkey的集合

    sdiff(key1, (keys)) :求差集

    sdiffstore(dstkey, (keys)) :求差集并将差集保存到dstkey的集合

    smembers(key) :返回名称为key的set的所有元素

    srandmember(key) :随机返回名称为key的set的一个元素

    8)Hash

    hset(key, field, value):向名称为key的hash中添加元素field

    hget(key, field):返回名称为key的hash中field对应的value

    hmget(key, (fields)):返回名称为key的hash中field i对应的value

    hmset(key, (fields)):向名称为key的hash中添加元素field

    hincrby(key, field, integer):将名称为key的hash中field的value增加integer

    hexists(key, field):名称为key的hash中是否存在键为field的域

    hdel(key, field):删除名称为key的hash中键为field的域

    hlen(key):返回名称为key的hash中元素个数

    hkeys(key):返回名称为key的hash中所有键

    hvals(key):返回名称为key的hash中所有键对应的value

    hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value

    相关文章

      网友评论

      • b4ec20031329:在controller中sessionid 声明成员变量不会有线程安全问题吗

      本文标题:集群间如何实现session共享【面试+工作】

      本文链接:https://www.haomeiwen.com/subject/ezspottx.html