一、引言
针对企业,为了应对庞大的用户访问压力,目前大多数大型网站服务器都采用集群部署的方式;针对个人,仅一台服务器而言,也会安装多个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
网友评论