对ddlaw项目后台的理解
业务流程图
后台架构图
使用Nginx作为http服务器,使用加权随机的策略进行负载均衡,将前端的请求分发到三台Tomcat服务器上。
使用MySQL做数据持久化数据库,保存数据。
使用Redis将访问的一些热点数据临时存储在内存中,以减少访问MySQL数据库的次数,减少磁盘IO,提高服务器性能。
项目的开发过程
业务部分
业务的开发并没有遇到太大的麻烦,主要是设计数据库,编写业务代码,前后端联调,功能测试等等。
优化过程
1.需求
该项目所要求的是可以供20万律师同时在线使用。
2. 加入Redis
业务代码开发完之后,项目是SSM框架+Tomcat服务器+MySQL数据库的架构,没有做任何优化。
在这时,后台获取数据的方式均是从MySQL中进行读取,这导致了大量的磁盘IO出现,因此这是一个可以优化的点。
进行讨论后,决定使用Redis来减少项目中的磁盘IO量。
将经常需要访问的Material对象临时保存在Redis内存中,当用户需要获取Material时,先查看Redis内存中有无该Material对象,如果有直接从内存中获取,返给用户,如果没有,再从MySQL中进行查询,然后把查询结果放入Redis中,并且将结果返回给用户。
3. 使用负载均衡
在加入Redis后,减少了大量的磁盘IO,服务器性能得到一定的提升,若还想提高性能,可引入多台服务器,做负载均衡。
由于实验室三台服务器性能并不平均,因此使用了加权策略进行负载均衡。
4. 使用Token代替Session
采用Token代替Session。
亮点
Redis
Redis和Memcach区别
Redis Memecach 单核 多核 可以持久化 纯内存操作 支持string,list,set,zset,hash等数据结构 只能存储简单的key-value 支持虚拟内存(VM),当物理内存用完,可以将很久没用的value映射到磁盘上 不支持 数据可以定期持久化到磁盘上,如果宕机,可以进行数据恢复 宕机数据就全没了 默认使用定期删除+惰性删除的过期策略 只使用惰性删除策略
Redis过期策略
redis使用了定期删除+惰性删除的过期策略。
定期删除:redis在定时删除时并不会遍历所有的key,而是会进行一个随机抽样,检测抽样出来的key是否过期,如果过期就将其删除。
惰性删除:当对某个key进行get操作时,会检测一次该key是否过期,如果过期,就将其删除。
如果定期删除中遍历所有的key,那么当redis中保存的key很多时,会出现大量的CPU开销。
如果只使用惰性删除,那么可能会导致很多已经过期了的key依然保存在内存中,造成很大的内存开销。
缓存淘汰策略的选择
redis缓存淘汰策略有如下几种:
- noeviction:内存使用到达阈值时,所有引起申请内存的命令都会报错。(默认使用该策略)
- allkeys-lru:在主键空间中,优先移除最近未使用的key。
- volatile-lru:在设置了过期实践的键空间中,优先移除最近未使用的key。
- allkeys-random:在主键空间中,随机移除某几个key。
- volatile-random:在设置了过期时间的键空间中,随机移除某几个key。
- volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。
在该项目中,选择使用allkeys-lru策略。
每次对某个key进行操作,都会更新它的时间戳。lru策略就会找出时间戳最远的那几个进行删除。
注意:在redis中,对lru淘汰策略进行了优化。
Redis的近似LRU算法:
与redis的定期删除类似,redis从所有的key中随机抽取出几个key,然后根据这几个key的时间戳进行比较、淘汰。
抽样的数量可以用maxmemory-samples进行设置,默认时5。
这样做可以节省开销,可以说maxmemory-samples设置得越大,就越接近真是的lru算法,但是开销会更大。
Redis与MySQL数据一致性问题
首先了解一下读取数据的流程。
当某线程要读取数据时,先访问redis,如果在redis中寻找到了需要的数据,那么直接返回。
如果redis中没有需要的数据,那么到MySQL中获取数据,然后将数据更新在redis上。
当用户要对某些数据进行修改,如何保证redis和mysql数据的一致性呢?
假设现在A线程要读取数据,B线程要修改数据。
- 先修改MySQL,后修改Redis:
会出现如下情况
a. Redis中还未存放数据;
b. A线程进行Redis数据读取,没有读取到数据;
c. A线程从MySQL中读取数据;
d. A线程失去了CPU资源,B线程修改MySQL中的数据;
e. B线程修改Redis中的数据;
f. A线程再次获得CPU资源,将原来从MySQL中获取的数据更新到Redis中。
此时Redis中的数据就是脏数据。
- 先修改Redis,后修改MySQL:
同样会出现如上情况。不多叙述了。
- 先删除Redis中的该数据,再修改MySQL数据
会出现如下情况
a. B线程先删除了Redis中的数据;
b. B线程失去CPU资源,A线程开始读取;
c. A线程发现Redis中无该数据,到MySQL中读取数据;
d. A线程将该数据存放到Redis中,A线程结束;
e. B线程更新数据库,结束。
此时,Redis中存放的时脏数据。
如何解决这个问题:
可以让线程B更新完数据库后,sleep一段时间,然后对redis进行二次删除。
- 先修改MySQL,后删除Redis
会出现如下情况
a. redis首先没有该数据;
b. A线程读取redis,没有数据,于是读取MySQL,然后A线程失去CPU资源;
c. B线程更新数据库;
d. B线程删除redis,B线程结束;
e. A线程将其获取到的数据更新到redis中。
此时,redis中的数据是脏数据。
看似该方法也会出现脏数据,但是,这种情况很少出现,原因如下:MySQL的读操作所花费的时间远远少于写操作所需要的时间。
因此,步骤b中,A线程读取MySQL后,接下来的步骤c,B线程更改数据库会花费很多的时间。
在绝大部分情况下,执行步骤c的这段时间里,A线程已经将数据更新到redis中了。
也就是说,绝大部分情况下,步骤e应该发生在步骤d之前。A线程放入redis中的脏数据会被B线程删除掉。在该项目中,我们选用的保证一致性的策略是方法4,先修改MySQL,后删除Redis。
总结一下:到目前为止,还没有找到能够100%保证内存和数据库数据一致的方法,如果需要保证数据的强一致性,就不可以使用内存数据库。
负载均衡
当一台服务器性能有限时,可以使用多服务器集群来提高网站性能。
集群:多台服务器一起做同一件事情。
分布式:多台服务器干着不同的事情,举个例子,一个网购业务,服务器1负责验证用户的登录,服务器2负责为用户提供搜索商品的服务,服务器3负责完成支付业务,多个服务器协同工作,各自负责整个业务中的某一个部分,最后一起完成一个完整的业务。
所以,ddlaw这个项目,属于集群,并不是分布式。
在服务器集群中,需要一台http服务器用于请求调度,所有请求首先请求这台服务器,然后该服务器根据特定的负载均衡策略将请求转发给相应的应用服务器。
nginx就是http服务器,而三台tomcat就是应用服务器,http服务器用于转发请求,而应用服务器用于运行逻辑代码处理请求。
负载均衡一定能提高网站性能吗
并不是。
经过实践检测,当并发量不高时,单服务器会比多服务器进行负载均衡效果要好,而当并发量升高时,负载均衡的效果才显现出来。
据我猜测,当并发量小时,单台服务器的性能足以保证程序的正常运行,而如果此时使用多服务器负载均衡,还要增加一个nginx对转发请求的步骤,导致效果不如单服务器。
负载均衡策略的选择
现在nginx的upstream支持5种负载均衡策略
- 轮询法(默认):
将请求按顺序轮流分配到后台服务器上,均衡的对待每一台服务器。- 权重法:
根据所指定的权重将请求分发到后端服务器上,适用于后端服务器性能不均的情况。- ip_hash法:
将请求按访问ip的hash值分配服务器,可以让访客固定的访问一个服务器,能解决Session问题。- 最小连接数法:
将请求发送给连接数最少的后端服务器。- fair法(第三方):
按后端服务器的响应时间来分配请求,响应时间短的优先分配。- url_hash法(第三方):
按照访问的url的hash结果来分配请求,使每个url定向到同一个后端服务器。第三方的方法需要下载第三方插件才可使用。
在该项目中,选择的是权重法的负载均衡策略,原因是实验室有三台服务器,而三台服务器的性能并不均衡,根据它们性能的高低,给它们分配了各自的权重。
Nginx负载均衡实践
由于现在不在实验室,现在使用单机多端口开tomcat服务器的方法来模拟一下负载均衡。
首先,说一下如何在同一台电脑上开三个tomcat的。很简单。
将tomcat复制三份,分别命名为tomcat1、2、3。
分别进入各自的conf文件价下,打开server.xml
将三个tomcat的如下端口改成不同的端口
<Server port=“8005” shutdown=“SHUTDOWN”>
<Connector port=“8009” protocol=“AJP/1.3” redirectPort=“8443” />
<Connector port=“8080” protocol="HTTP/1.1 connectionTimeout=“20000” redirectPort=“8443” />
比如我的tomcat2将tomcat1的8005端口改为了8006,tomcat3将tomcat1中的8009端口改为了8011。
改好以后,分别进入到各自的bin目录下,运行start.sh脚本即可开启tomcat。
此时,访问localhost:8080 localhost:8081 localhost:8082都可以看到tomcat的首页。
ps:更改tomcat首页的方法很简单,去百度一下就好了~
接下来我们先开启nginx服务器。
如图,我配置中nginx监听的端口号是80,而http默认访问的端口号就是80,因此直接在浏览器中输入localhost后,就能访问到nginx首页。
- 轮询法
原本的server块中原配置如下:server { listen 80; #监听的端口是80 server_name localhost; #服务器名成时localhost location / { #如果有请求访问,则会返回/usr/share/nginx/html/index.html这个页面给浏览器 root /usr/share/nginx/html; index index.html; } }
在nginx的配置文件的http块中加入如下代码:
upstream test { #轮询是nginx做负载均衡的默认策略 #不进行策略指定,会默认是用轮询的方法做负载均衡 server localhost:8080; server localhost:8081; server localhost:8082; }
将server块代码进行如下修改:
server { listen 80; server_name localhost; location / { proxy_pass http://test #设置反向代理,test是upstream块的名字 # 简单点理解这个配置,当客户端访问localhost时, # 将localhost替换为upstream test中的某一个server。 # 也就是,在浏览器输入localhost, # 你的请求会被发送到localhost:808x上。 } }
这里提一句反向代理
正向代理:指客户端委托某个代理服务器,代替它向目的服务器发送请求。
反向代理:指服务端委托某个代理服务器,代替它们就收请求。
这样配置以后,相当于后端的三台tomcat服务器委托nginx服务器代替它们就收请求,然后nginx服务器会将请求分发给tomcat服务器。配置修改完毕后,重启nginx服务器。
然后我们三次访问localhost,结果如下图
可见配置已经成功。接下来看看日志文件:
可见,一共发起了4次访问,upstream_addr是按照8080->8081->8082->8080循环访问的。
如果其中某一台tomcat发生了down机的情况,nginx不会向其发起请求。也就是说假设此时tomcat1 down机了,nginx会在tomcat2和tomcat3上轮询。
更改配置,可以设置某台服务器down机。upstream test { server localhost:8080 down; #在后面加上down就设置这台服务器down机 server localhost:8081; server localhost:8082; #还有一个backup可以这样使用 #server localhost:8080 backup; #这句的意思是将该服务器设置成为备用机 #当主服务器挂掉后,备用机自动接管服务。 }
这样配置后,再进行测试,日志如下图:
可见,仅仅在8081和8082端口上进行轮询。
- 权重法
更改nginx中的upstream模块如下:upstream test { server localhost:8080 weight=3; server localhost:8081 weight=1; server localhost:8082 weight=1; }
重启nginx,然后开始测试,日志如下:
可见,在五次请求中,有三次是访问8080端口,一次访问8081端口,一次访问8082端口,与所配置的权重对应。
- ip_hash法
更改nginx配置如下:upstream test { ip_hash; server localhost:8080; server localhost:8081; server localhost:8082; }
重启nginx,进行测试。日志如下:
三次访问,请求都被转发到8082端口上。
Token代替Session
Session的使用会带来一些问题,如下:
- 用户每次发起请求,服务器都需要创建一个对象保存Session,当用户数量越来越多,对服务器内存的消耗也越来越大。
- 出现跨站请求伪造(CSRF)问题(图片来源)
- 在集群时(如该项目),多台服务器上的Session并不能共享,而用户的多次请求可能会被分发到不同服务器上去,出现了Session共享问题。
当然,该问题也有一些解决办法:
- 做负载均衡时,使用ip_hash策略,让某IP的用户固定访问某一台服务器。
- Session复制,每新建一个Session,将该Session复制到各台服务器上,这回大量浪费资源。
- 使用内存数据库(如Redis)保存Session信息,每台服务器都到缓存上读取Session信息。
在该项目中,由于负载均衡策略选用的权重法,而且项目后期还需要引入安卓客户端(安卓没有Session),因此,对用户认证的方法由Session改为Token,这样性能更高,也更加安全。
使用Token机制后用户的请求流程:
- 用户进行登录操作,后台验证帐号密码后,生成一个token,返回给前端,并且将token信息保存到redis中;
- 该用户每次发送请求,都会把token信息放置到Authorization请求头中;
- 后台收到请求,先经过一个filter,解析Authorization头中的token信息,获取user_id;
- 将收到的token与redis中该user_id对应的token进行对比,如果对比一样,则成功,执行请求响应的逻辑代码
否则,返给前端验证失败。