微服務(wù)之會(huì)話管理

Session 會(huì)話

通常我們所說(shuō)的會(huì)話是兩個(gè)或更多個(gè)通信設(shè)備之間或計(jì)算機(jī)和用戶之間的半永久性交互式信息交換, 會(huì)話在某個(gè)時(shí)間點(diǎn)建立,然后在稍后的時(shí)間點(diǎn)拆除。

建立的通信會(huì)話可以在每個(gè)方向上涉及多于一個(gè)消息, 這些消息只存在這個(gè)會(huì)話中, 而與其他會(huì)話隔離.

會(huì)話通常是有狀態(tài)的,這意味著至少一個(gè)通信部分需要保存關(guān)于會(huì)話歷史的信息以便能夠進(jìn)行通信泛源,這與無(wú)狀態(tài)通信相反,其中通信由具有響應(yīng)的獨(dú)立請(qǐng)求組成。

而狀態(tài)保存在什么地方, 有很多選擇, 內(nèi)存中, 磁盤(pán)上, 共享緩存中, 數(shù)據(jù)庫(kù)里, 總有一款適合你.

常見(jiàn)的會(huì)話就有 TCP Session, SIP Session , RTP Session , HTTP Session 等等, 分別工作在傳輸層, 會(huì)話層和應(yīng)用層

TCP Session

這個(gè)自不必說(shuō), 用三次握手建立會(huì)話, 四次揮手終止會(huì)話

  • 三次握手


    TCP Session Estalish
  • 四次揮手

TCP Session Close

這樣在連接的兩端就建立了一個(gè) TCP Session, 并且維護(hù)著會(huì)話狀態(tài)

SIP/RTP Session

SIP是一種應(yīng)用層控制協(xié)議斯棒,可以建立,修改和終止多媒體會(huì)話(會(huì)議)主经,例如互聯(lián)網(wǎng)電話呼叫,多媒體分發(fā)播放和多媒體會(huì)議荣暮。它在TCP 或 UDP 之上通過(guò) INVITE 消息來(lái)搭建用戶代理之間的信令(控制 - SIP Session) 和媒體會(huì)話 (RTP Session)

這里不做贅述, 請(qǐng)見(jiàn)微服務(wù)協(xié)議之 SIP

更多細(xì)節(jié)見(jiàn) RFC

Http Session

這里重點(diǎn)講講 HTTP Session, 傳統(tǒng) Web 應(yīng)用里都有一個(gè) session 的概念,相比用 Cookie 在客戶端記錄信息確定用戶身份, Session 一般是在服務(wù)器端記錄信息確定用戶身份和狀態(tài), 這里的狀態(tài)不僅指用戶登錄和在線的狀態(tài), 也包括應(yīng)用層中的一些業(yè)務(wù)相關(guān)的信息, 比如很多網(wǎng)站的購(gòu)物車(chē)就是放在 Http Session 里的.

在分布式系統(tǒng)中, 通常不建議將會(huì)話狀態(tài)放在一臺(tái)服務(wù)器的內(nèi)存或磁盤(pán)中, 因?yàn)檫@樣的話, 系統(tǒng)會(huì)有單點(diǎn)失敗而導(dǎo)致的服務(wù)不可用, 如下圖所示:

如果客戶端與服務(wù)器的 session 只存在于 server 1 中 負(fù)載均衡器做流量派發(fā)時(shí), 必須要把流量派發(fā)到server 1, 這叫 session sticky , 一旦 server 1 掛掉了, 這個(gè)對(duì)話狀態(tài)就丟失了, 如果你在網(wǎng)站購(gòu)物, 突然購(gòu)物車(chē)?yán)镞x好的寶貝都沒(méi)了, 這多讓人惱火

對(duì)于session 狀態(tài)的管理我們一般有三種策略

  1. session sticky 會(huì)話粘滯
    如上所述, 會(huì)話在一臺(tái)服務(wù)器上持續(xù), 直到會(huì)話終止, 問(wèn)題在于單點(diǎn)失敗

  2. session replicate 會(huì)話復(fù)制
    將會(huì)話信息復(fù)制到各臺(tái)服務(wù)器上, 例如利用多播技術(shù)及組通信技術(shù)把狀態(tài)同步到組中的每一臺(tái)server, 我曾經(jīng)用過(guò) Jgroups, 在服務(wù)器數(shù)量不多的情況下工作得不錯(cuò), 可是如果服務(wù)器距離較遠(yuǎn)并不在一個(gè)網(wǎng)段, 服務(wù)器數(shù)量較多, 這種方案就不適合了, 同步消息過(guò)多且有性能問(wèn)題.

  3. session Repository 會(huì)話倉(cāng)庫(kù)
    會(huì)話狀態(tài)存儲(chǔ)在共享的數(shù)據(jù)倉(cāng)庫(kù)中, 這樣每臺(tái)server 都可以輕松存取, 會(huì)話倉(cāng)庫(kù)可以是傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)或NOSQL產(chǎn)品, 不過(guò)單點(diǎn)失敗轉(zhuǎn)移到了會(huì)話倉(cāng)庫(kù), 如果訪問(wèn)量比較大且存取頻繁, 對(duì)會(huì)話倉(cāng)庫(kù)的要求也比較高, 鑒于會(huì)話并不需要存儲(chǔ)很長(zhǎng)時(shí)間, 相比 Oracle/MySQL, Cassandra 或 Redis 更加合適

就以現(xiàn)在比較流行的 Spring Session 的 Redis 方案為例

購(gòu)物車(chē)示例 Spring Session + Redis

Redis 的安裝和配置不說(shuō)了, 非常簡(jiǎn)單, 參見(jiàn)Redis 入門(mén),
我是在自己的 macbook 中啟了一個(gè) redis docker image , 偵聽(tīng)端口是 6379

建立一個(gè) Spring Boot 項(xiàng)目, 在 https://start.spring.io 上選擇

  • Session
  • Lombok
  • Web
  • Redis

將生成的壓縮包解開(kāi), 這是一個(gè) spring boot 項(xiàng)目的框架

讓我們先看看所需要的依賴庫(kù)

  • pom.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
# 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
package com.github.walterfan.hellosession;

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

@EnableRedisHttpSession
public class RedisSessionConfig {
}

新建購(gòu)物車(chē)類和控制器

  • ShoppingCart
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
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;
    }

}

例子代碼參見(jiàn) https://github.com/walterfan/helloworld/tree/master/hellosession

用 postman 嘗試一下, 先保存購(gòu)物車(chē)

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

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

再讀取購(gòu)物車(chē)

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"
}

打開(kāi) redis 命令行工具
redis-cli> keys spring:session:*

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 工具, 好用簡(jiǎn)單, 主頁(yè)是 https://www.npmjs.com/package/redis-commander, 安裝啟動(dòng)超簡(jiǎn)單:

npm install -g redis-commander
redis-commander -p 9090

打開(kāi) http://localhost:9090

如果這時(shí)你用 curl 再來(lái)試一下

我們用 curl 也來(lái)試一下, 這是不同的session 了

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
# 響應(yīng)輸出
{"cartId":"101","userId":"200","shoppingList":["pc","ipad"]}


curl -L -b cookies.txt http://localhost:8000/api/v1/carts/100
# 響應(yīng)輸出
{"cartId":"101","userId":"200","shoppingList":["pc","ipad"]}

試試把 cookies.txt 中的 session 改成之前的sessonID, 就可以取回之前存儲(chǔ)的sessionID 了
注意這里的sessionID要base64 編碼

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 是不能重復(fù)的, 在生成 sessionID 時(shí)于算法上就要保證唯一性,tomcat的算法參見(jiàn)https://tomcat.apache.org/tomcat-8.0-doc/config/sessionidgenerator.html, 其實(shí)我覺(jué)得就用uuid 好了

把 Redis 實(shí)例改成 Redis cluster 的地址, 這個(gè) sesssion 就會(huì)復(fù)制到其他 redis 實(shí)例中, 從而保證了高可用性, Redis 的高并發(fā)量也保證了性能

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末罩驻,一起剝皮案震驚了整個(gè)濱河市穗酥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惠遏,老刑警劉巖砾跃,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異爽哎,居然都是意外死亡蜓席,警方通過(guò)查閱死者的電腦和手機(jī)器一,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)课锌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人祈秕,你說(shuō)我怎么就攤上這事渺贤。” “怎么了请毛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵志鞍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我方仿,道長(zhǎng)固棚,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任仙蚜,我火速辦了婚禮此洲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘委粉。我一直安慰自己呜师,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布贾节。 她就那樣靜靜地躺著汁汗,像睡著了一般衷畦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上知牌,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天祈争,我揣著相機(jī)與錄音,去河邊找鬼送爸。 笑死铛嘱,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的袭厂。 我是一名探鬼主播墨吓,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纹磺!你這毒婦竟也來(lái)了帖烘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤橄杨,失蹤者是張志新(化名)和其女友劉穎秘症,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體式矫,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乡摹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了采转。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片聪廉。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖故慈,靈堂內(nèi)的尸體忽然破棺而出板熊,到底是詐尸還是另有隱情,我是刑警寧澤察绷,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布干签,位于F島的核電站,受9級(jí)特大地震影響拆撼,放射性物質(zhì)發(fā)生泄漏容劳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一闸度、第九天 我趴在偏房一處隱蔽的房頂上張望竭贩。 院中可真熱鬧,春花似錦筋岛、人聲如沸娶视。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肪获。三九已至寝凌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間孝赫,已是汗流浹背较木。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留青柄,地道東北人伐债。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像致开,于是被迫代替她去往敵國(guó)和親峰锁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容