第三章 分布式扩展(二)
Nginx 高性能原因
- epoll 多路复用
- master worker 进程模型
- 允许进程平滑重启以及平滑的加载配置保证不断开与客户端连接,可以依赖进程模型完成对应的操作
- 协程机制
- 基于协程的非阻塞式编程的一套机制来完成单线程、单进程的机制,却又支持并发的编程调用接口
epoll 多路复用
了解 epoll多路复用之前,我们先要知道 bio模型以及 select模型
- java bio 模型,阻塞进程式的模型
- bio模型建立在java C/S基础上,客户端和服务端通过socket进行连接,一对一建立,java client只有等
socket.write() 所有字节流输出到 TCP/IP 缓冲区之后才返回,如果网络传输很慢,TCP/IP缓冲区被塞满,java client也只能等信息传输过去,缓冲区有空间后,才能写入
- linux select模型,变更触发轮询查询,有1024数量限制
- select 模型监听客户端连接的socket,将所有监视的socket加入阻塞队列中,阻塞自己进程,如果有连接收到数据,就遍历整个阻塞队列,将该连接移除阻塞队列,加入工作队列中
- epoll 模型,变更触发回调直接读取,理论上无上限
- epoll 模型类似于select 模型,先将自己阻塞,监听客户端连接,但为了避免select遍历队列,设置回调函数,如果连接发生变化就唤醒自己并直接执行回调函数
详细可以看下这两篇文章:
Epoll原理解析_confirmwz的博客-CSDN博客_epoll
master worker进程模型
- master-worker 模型,有一个master父进程,管理多个worker子进程。master进程和worker进程是父子进程的方式,通过fork的方式创建,master进程可以管理worker内部的内存空间。
- nginx的master 进程负责启动、停止服务、重载配置文件、平缓升级等,而worker 进程负责Client端socket的
send(发送) 和receive(接收) 。 - nginx创建master进程后,会创建一个socket文件句柄,用于
listen() 在对应端口上,等待客户端发起Connect连接请求(这里用到了epoll多路复用技术),当Client发起连接请求时,epoll会执行对应的回调函数。
socket建立连接三次握手:
- 首先服务端socket先
bind(绑定) 在80端口,然后监听listen() 端口 - 客户端发起Connect请求连接到80端口,在端口上的进程执行
accecpt(接收) 方法,进行三次握手,连接建立起socket句柄 - socket 可以实现
socket.read() 和 socket.write() 的方法,这个过程就对应了request的read和response的write。
相关问题
问题1:当请求端口连接时,worker进程负责执行accept方法,那么有多个worker进程,哪一个会执行 ?
- 这里nginx采用的是锁竞争的原理,互斥体,哪个进程先获得锁,就优先执行。
问题2:执行 sbin/nginx -s reload 语句是无缝重启,不会断开连接,原理是什么 ?
sbin/nginx -s reload 执行前后master进程不变,worker进程改变。- 执行语句后,master进程发送信号给对应worker进程,要求把所有socket句柄交给master进程管理。
- master进程会重新加载配置文件,启动一个新的worker进程,并且master进程会将这些对应的socket句柄交给新的worker进程来管理(内存操作很快),这样就不用再进行客户端和nginx重连接
问题3:为什么worker进程是单线程,不采用多线程呢 ?
- 单线程不涉及线程间的上下文切换和通信,不会造成阻塞,更高效,而因为与client端有epoll多路复用,实现了并发管理。
- 通俗来讲,如果你的线程内部是没有任何的阻塞操作的,那对应的一个单线程的调用是会比多线程更快的!!!
协程机制
- 依附于线程的内存模型,切换开销小。
- 一个线程可以有多个协程,不需要cpu切换的开销,只要内存切换的开销。
- 遇阻塞及时归还执行权,代码同步。
- 如果协程遇到阻塞了,线程会立刻回收其执行权,然后立马调用一个不阻塞的协程去运行。
- 无需加锁。
分布式会话管理
什么是会话 ?
- 会话简单理解就是 用户打开浏览器,点击很多超链接,访问多个服务器资源,然后关闭浏览器,整个过程就称之为一个会话。
有哪些方式可以保存会话 ?
- 基于cookie的管理方式。实现会话的方式如下:
- 用户发起登录请求,服务端根据传入的用户名密码之类的信息,验证用户,然后创建一个登陆凭证(包含Id之类的信息,还有过期时间,创建时间)。
- 服务端对登陆凭证做数字签名,对称加密,加密后写入cookie。cookie名称必须固定(比如ticket),后面再获取时还得根据名字来获取cookie值。
- 用户下次登陆时,服务端根据上一次登陆凭证的cookie名字找到相关的cookie值,然后做解密处理再做数字签名的认证,验证是否过期等等。
- 基于token的管理方式。实现会话的方式如下:
- 基于token的方式和基于cookie类似,区别是token返回给客户端之后,后面每次请求,都会主动把token加到http header里或者url中,对token进行验证
两种目前流行的session实现方式:
但以上两种方式并不适合分布式会话,因为用于用户登录的sessionId相关的数据都是封装在项目内嵌的tomcat容器当中,因为分布式系统的原因会导致每次请求都不是在同一个服务器中,从而导致获取不到对应的sessionId
所以需要改变目前的状况:
- 基于cookie传输sessionid:java tomcat容器session实现迁移到 redis
- 基于token传输类似sessionid:java代码session实现迁移到 redis
分布式会话实现(一)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
redis 安装部署
- 首先下载好redis 安装包
- 赋予压缩包权限
chmod -R 777 redis-.tar.gz - 执行
tar -zxvf redis.tar.gz 解压缩
- 执行
make ?编译 redis,不成功的话执行 yum install gcc - 最后执行
make install 命令安装 redis - 启动 redis 服务器
redis-server /usr/local/redis-6.2.6/etc/redis.conf - 启动 redis 客户端
redis-cli -p 6379
分布式会话实现(二)
- 在配置
application.yml 中添加对redis的依赖:
spring:
redis:
host: 43.138.131.175
port: 6379
database: 10
jedis:
pool:
max-active: 50
min-idle: 20
var g_host = "localhost:80";
- 测试登录界面,将UserModel模型实现序列化接口,这样session的信息才能写入redis数据库中
- 也就是说,要存入redis的数据,都得实现
Serializable 序列化
- 使用 keys * 命令查看redis中的数据,发现session数据已经被存到里面
重新打包部署到服务器上
- 将打好的jar包上传到服务器的
//var/www/miaosha 目录上 - kill 掉正在运行的java程序
- 将重新打好的jar包替换掉原来的
miaosha.jar
- 给当前jar包赋予读写权限:
chmod -R 777 miaosha.jar - 后台启动java程序:
./deploy.sh &
分布式会话实现(三)
- 修改redis配置文件
redis.conf ,指定redis的访问:添加bind redis数据库 的内网IP地址
- 后台方式启动redis :
redis-server /usr/local/redis-6.2.6/etc/redis.conf - 查看6379端口情况 netstat -an | grep 6379 地址正常即成功
- 切换到应用服务器1,修改配置文件
application.properties
- 增加一句 spring.redis.host = redis服务器内网IP
- 重新部署jar包,成功
- 检查6379端口,出现下面的信息即为成功
- 测试打开登陆界面,登陆用户后下单,因为是同一个登陆用户的session,所以可重复下单
总结
- 应用服务器都要安装redis,然后修改应用程序的配置文件,将redis的ip地址设置为另一台redis服务器的内网ip地址
- redis服务器也需要安装redis,并将自己的可被访问ip地址设置为自己的内网地址
- 这样,应用服务器的应用程序只能通过redis服务器的内网ip地址才能访问到redis服务器上的redis服务端
基于token的分布式会话实现(一)
- 上面我们完成了基于cookie传输sessionid的实现,现在来做基于token的分布式会话的实现。
- token类似于服务端下发sessionid植入到cookie,下发令牌,交给前端,等登陆请求时,把token加入到http header或url中。
操作步骤
- 修改后端页面
UserController.java
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/login", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType login(@RequestParam(name = "telphone") String telphone, @RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
if (org.apache.commons.lang3.StringUtils.isEmpty(telphone) || org.apache.commons.lang3.StringUtils.isEmpty(password)) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
UserModel userModel = userService.validateLogin(telphone, this.EncodeByMd5(password));
String uuidToken = UUID.randomUUID().toString();
uuidToken = uuidToken.replace("-", "");
redisTemplate.opsForValue().set(uuidToken, userModel);
redisTemplate.expire(uuidToken, 1, TimeUnit.HOURS);
return CommonReturnType.create(uuidToken);
}
- 修改后端页面
OrderController.java
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId,
@RequestParam(name = "promoId", required = false) Integer promoId,
@RequestParam(name = "amount") Integer amount) throws BusinessException {
String token = httpServletRequest.getParameterMap().get("token")[0];
if (StringUtils.isEmpty(token)) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
}
UserModel userModel = (UserModel) redisTemplate.opsForValue().get(token);
if (userModel == null) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN, "用户还未登录,不能下单");
}
OrderModel orderModel = orderService.createOrder(userModel.getId(), itemId, promoId, amount);
return CommonReturnType.create(null);
}
- 点击下单,请求url中出现token,说明session加入到了token中
|