微服务之会话管理

Session 会话

通常我们所说的会话是两个或更多个通信设备之间或计算机和用户之间的半永久性交互式信息交换, 会话在某个时间点建立,然后在稍后的时间点拆除。

建立的通信会话可以在每个方向上涉及多于一个消息, 这些消息只存在这个会话中, 而与其他会话隔离.

会话通常是有状态的,这意味着至少一个通信部分需要保存关于会话历史的信息以便能够进行通信,这与无状态通信相反,其中通信由具有响应的独立请求组成。

而状态保存在什么地方, 有很多选择, 内存中, 磁盘上, 共享缓存中, 数据库里, 总有一款适合你.

常见的会话就有 TCP Session, SIP Session , RTP Session , HTTP Session 等等, 分别工作在传输层, 会话层和应用层

TCP Session

这个自不必说, 用三次握手建立会话, 四次挥手终止会话

  • 三次握手

  • 四次挥手

这样在连接的两端就建立了一个 TCP Session, 并且维护着会话状态

SIP/RTP Session

SIP是一种应用层控制协议,可以建立,修改和终止多媒体会话(会议),例如互联网电话呼叫,多媒体分发播放和多媒体会议。它在TCP 或 UDP 之上通过 INVITE 消息来搭建用户代理之间的信令(控制 - SIP Session) 和媒体会话 (RTP Session)

这里不做赘述, 请见微服务协议之 SIP

更多细节见 RFC

Http Session

这里重点讲讲 HTTP Session, 传统 Web 应用里都有一个 session 的概念,相比用 Cookie 在客户端记录信息确定用户身份, Session 一般是在服务器端记录信息确定用户身份和状态, 这里的状态不仅指用户登录和在线的状态, 也包括应用层中的一些业务相关的信息, 比如很多网站的购物车就是放在 Http Session 里的.

在分布式系统中, 通常不建议将会话状态放在一台服务器的内存或磁盘中, 因为这样的话, 系统会有单点失败而导致的服务不可用, 如下图所示:

如果客户端与服务器的 session 只存在于 server 1 中 负载均衡器做流量派发时, 必须要把流量派发到server 1, 这叫 session sticky , 一旦 server 1 挂掉了, 这个对话状态就丢失了, 如果你在网站购物, 突然购物车里选好的宝贝都没了, 这多让人恼火

对于session 状态的管理我们一般有三种策略

  1. session sticky 会话粘滞 如上所述, 会话在一台服务器上持续, 直到会话终止, 问题在于单点失败

  2. session replicate 会话复制 将会话信息复制到各台服务器上, 例如利用多播技术及组通信技术把状态同步到组中的每一台server, 我曾经用过 Jgroups, 在服务器数量不多的情况下工作得不错, 可是如果服务器距离较远并不在一个网段, 服务器数量较多, 这种方案就不适合了, 同步消息过多且有性能问题.

  3. session Repository 会话仓库 会话状态存储在共享的数据仓库中, 这样每台server 都可以轻松存取, 会话仓库可以是传统关系型数据库或NOSQL产品, 不过单点失败转移到了会话仓库, 如果访问量比较大且存取频繁, 对会话仓库的要求也比较高, 鉴于会话并不需要存储很长时间, 相比 Oracle/MySQL, Cassandra 或 Redis 更加合适

就以现在比较流行的 Spring Session 的 Redis 方案为例

购物车示例 Spring Session + Redis

Redis 的安装和配置不说了, 非常简单, 参见Redis 入门, 我是在自己的 macbook 中启了一个 redis docker image , 侦听端口是 6379

建立一个 Spring Boot 项目, 在 start.spring.io 上选择

  • Session
  • Lombok
  • Web
  • Redis

将生成的压缩包解开, 这是一个 spring boot 项目的框架

让我们先看看所需要的依赖库

  • pom.xml
xml 复制代码
		<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

搞定配置

  • application.yml
yaml 复制代码
# refer to https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/html/common-application-properties.html
spring:
  profiles:
  #use dev environment by default
    active: dev
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
---
# dev environment
spring:
  profiles: dev
  redis:
    host: localhost
    port: 6379
server:
  port: 8000

---
# production environment
spring:
  profiles: pro
  redis:
    host: 127.0.0.1
    port: 6379
server:
  port: 8080
  • RedisSessionConfig
kotlin 复制代码
package com.github.walterfan.hellosession;

import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession
public class RedisSessionConfig {
}

新建购物车类和控制器

  • ShoppingCart
java 复制代码
package com.github.walterfan.hellosession;

import lombok.Data;

import java.io.Serializable;
import java.util.List;


@Data
public class ShoppingCart implements Serializable {
    private String cartId;
    private String userId;
    private List<String> shoppingList;
}
  • ShoppingCartController
java 复制代码
package com.github.walterfan.hellosession;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping(value = "/api/v1")
public class ShoppingCartController {
    @RequestMapping(value = "/carts/{cartId}", method = RequestMethod.GET)
    public ShoppingCart getShoppingCart (HttpServletRequest request, @PathVariable String cartId){
        HttpSession httpSession = request.getSession();

        ShoppingCart cart = (ShoppingCart) httpSession.getAttribute(cartId);

        log.info("getShoppingCart sessionId={}, cartId={}", httpSession.getId(), cartId);
        if(null != cart)
            log.info("cart={}", cart);
        return cart;
    }

    @RequestMapping(value = "/carts/{cartId}" , method = RequestMethod.PUT)
    public ShoppingCart setShoppingCart (HttpServletRequest request, @PathVariable String cartId, @RequestBody ShoppingCart cart){
        HttpSession httpSession = request.getSession();
        httpSession.setAttribute(cartId, cart);
        log.info("setShoppingCart sessionId={}, cart={}", httpSession.getId(), cart);
        return cart;
    }


    @RequestMapping(value = "/session" , method = RequestMethod.GET)
    public Map<String, String> getVersionInfo (HttpServletRequest request){
        HttpSession httpSession = request.getSession();
        Enumeration<String> names = httpSession.getAttributeNames();
        Map<String, String> map = new HashMap<>();

        map.put("sessionId", httpSession.getId());

        while (names.hasMoreElements()) {
            String key = names.nextElement();
            String value = String.valueOf(httpSession.getAttribute(key));
            map.put(key, value);
        }

        return map;
    }

}

例子代码参见 github.com/walterfan/h...

用 postman 尝试一下, 先保存购物车

bash 复制代码
PUT http://localhost:8000/api/v1/carts/100
# request:
{
  "cartId": "1001",
  "userId": "200",
  "shoppingList": [
    "iphone",
    "ipad"
  ]
}

# response
{
    "cartId": "1001",
    "userId": "200",
    "shoppingList": [
        "iphone",
        "ipad"
    ]
}

再读取购物车

GET http://localhost:8000/api/v1/carts/100
# response
{
    "cartId": "1001",
    "userId": "200",
    "shoppingList": [
        "iphone",
        "ipad"
    ]
}


GET http://localhost:8000/api/v1/session
# response
{
    "100": "ShoppingCart(cartId=1001, userId=200, shoppingList=[iphone, ipad])",
    "sessionId": "e1c33d09-1e7c-47d4-83d3-9932a836ce18"
}

打开 redis 命令行工具 redis-cli> keys spring:session:*

ruby 复制代码
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys spring:session:*
(empty list or set)
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> keys *
1) "spring:session:sessions:e1c33d09-1e7c-47d4-83d3-9932a836ce18"
2) "spring:session:expirations:1533389160000"
3) "spring:session:sessions:expires:e1c33d09-1e7c-47d4-83d3-9932a836ce18"

127.0.0.1:6379> hgetall "spring:session:sessions:e1c33d09-1e7c-47d4-83d3-9932a836ce18"
1) "maxInactiveInterval"
2) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
3) "lastAccessedTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01e\x05\x02\a~"
5) "sessionAttr:100"
6) "\xac\xed\x00\x05sr\x00.com.github.walterfan.hellosession.ShoppingCart(\bQ\xcd\xb2\x05O\xdb\x02\x00\x03L\x00\x06cartIdt\x00\x12Ljava/lang/String;L\x00\x0cshoppingListt\x00\x10Ljava/util/List;L\x00\x06userIdq\x00~\x00\x01xpt\x00\x02a1sr\x00\x13java.util.ArrayListx\x81\xd2\x1d\x99\xc7a\x9d\x03\x00\x01I\x00\x04sizexp\x00\x00\x00\x02w\x04\x00\x00\x00\x02t\x00\x02pct\x00\x04ipadxt\x00\x02a2"
7) "creationTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01e\x05\x02\a~"

推荐一款 Redis 的 Web GUI 工具, 好用简单, 主页是 www.npmjs.com/package/red..., 安装启动超简单:

css 复制代码
npm install -g redis-commander
redis-commander -p 9090

打开 http://localhost:9090

如果这时你用 curl 再来试一下

我们用 curl 也来试一下, 这是不同的session 了

json 复制代码
curl -c cookies.txt -X PUT -H "Content-Type: application/json" -d '{"cartId":"101","userId":"200", "shoppingList":["pc", "ipad"]}' http://localhost:8000/api/v1/carts/100
# 响应输出
{"cartId":"101","userId":"200","shoppingList":["pc","ipad"]}


curl -L -b cookies.txt http://localhost:8000/api/v1/carts/100
# 响应输出
{"cartId":"101","userId":"200","shoppingList":["pc","ipad"]}

试试把 cookies.txt 中的 session 改成之前的sessonID, 就可以取回之前存储的sessionID 了 注意这里的sessionID要base64 编码

ruby 复制代码
YAFAN-M-N0CV:hellosession yafan$ more cookies.txt
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost     FALSE   /       FALSE   0       SESSION ZjY3Y2MxMDEtYWNkNC00MGE4LThmNDAtOWZlZDljMjRiY2My

所以 session ID 是不能重复的, 在生成 sessionID 时于算法上就要保证唯一性,tomcat的算法参见tomcat.apache.org/tomcat-8.0-..., 其实我觉得就用uuid 好了

把 Redis 实例改成 Redis cluster 的地址, 这个 sesssion 就会复制到其他 redis 实例中, 从而保证了高可用性, Redis 的高并发量也保证了性能

参考资料

相关推荐
阿龟在奔跑1 小时前
引用类型的局部变量线程安全问题分析——以多线程对方法局部变量List类型对象实例的add、remove操作为例
java·jvm·安全·list
飞滕人生TYF1 小时前
m个数 生成n个数的所有组合 详解
java·递归
代码小鑫1 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖1 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
激流丶2 小时前
【Kafka 实战】Kafka 如何保证消息的顺序性?
java·后端·kafka
周全全2 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
uzong2 小时前
一个 IDEA 老鸟的 DEBUG 私货之多线程调试
java·后端
AiFlutter3 小时前
Java实现简单的搜索引擎
java·搜索引擎·mybatis
飞升不如收破烂~3 小时前
Spring boot常用注解和作用
java·spring boot·后端
秦老师Q3 小时前
Java基础第九章-Java集合框架(超详细)!!!
java·开发语言