1. 程式人生 > >當macaron的session配了redis並且遇上了websocket——一個session“不”更新的bug

當macaron的session配了redis並且遇上了websocket——一個session“不”更新的bug

文章目錄

上個月剛好是go語言9週年,忽然發現入坑go語言也兩年了,把最近一次遇到的bug分享一下,後面有時間再把這兩年的積累慢慢倒出來。

著急解決問題的直接點上面“解決方案”

排錯過程

功能描述:點選專案名稱切換專案。
實現邏輯:前端呼叫後端切換專案介面,後端更新session中的專案ID,前端收到返回後重新整理頁面。
問題描述:點選專案名稱,等待重新整理後出現原專案頁面。

我在這首先是開F12看下前端傳的引數有沒有問題,當然如果真是引數就不會有這篇文章了。但是這裡有點問題的是,瀏覽器看不到我返回的資料,而postman可以。不過雖然看不到響應體還有響應頭可以搞事情,於是在header裡面加log給回前端,又是一切正常……

但是頁面重新整理的第一個介面所帶的session中,確實是切換前的專案。那麼問題來了,切換專案的handler已經把session中的專案ID改了,這個不管是斷點還是F12都已經驗證;而重新整理後的第一個介面調到後端,又從session裡拿到原專案的ID,而F12中在這兩者之間又只有靜態資源請求,這是怎麼回事。

這裡說明一下,為了高可用部署,session是放在redis上叢集共享的。於是就可以在redis上看看session到底啥樣。

於是redis-cli用sessionID來get一下,看到專案ID確實是舊專案的,那麼問題又來了,切換專案的handler明明更新了session且沒有error返回,redis裡的session到底發生了什麼?

這裡提一下,redis沒有history之類的操作,但是有monitor可以提供類似log的作用。於是開著monitor操作了一波後發現,session經歷了兩次修改:原專案ID->新專案ID->原專案ID。在排除了有人同時操作的可能後,session還是經歷了兩次修改。

於是進到macaron原始碼中,發現給session的set操作是這樣的:

// Set sets value to given key in session.
func (s *RedisStore) Set(key, val interface{}) error {
	s.lock.Lock()
	defer
s.lock.Unlock() s.data[key] = val return nil }

也就是說這一步只是存到記憶體,而沒有發給redis,不過在它附近發現了這個:

// Release releases resource and save data to provider.
func (s *RedisStore) Release() error {
	data, err := session.EncodeGob(s.data)
	if err != nil {
		return err
	}

	return s.c.SetEx(s.prefix+s.sid, s.duration, string(data)).Err()
}

然後在return前加了斷點,操作一下後發現果然執行了兩次,難怪在redis的monitor中看到兩次修改。分別在呼叫棧裡找請求URL,發現除了switch正常更新session裡的專案ID外,還有一個state請求。

這裡插一句說明一下,state介面是一個websocket介面,頁面重新整理時重建連線。

但是這個F12上看,頁面重新整理後第一個請求是info啊,哪來的state呢?其實這是因為重新整理操作導致原頁面的websocket斷開而走到這裡的。

到這就有必要先捋一下macaron裡session這部分的程式碼了,在pkg/go-macaron/session/session.go:Sessioner方法會返回一個macaron.Handler型別的func,這個func其實是所有前端請求進來的第一站,也是最後一站,開發者通過macaron.Macaron.Get()等方法註冊的handler,是在這個macaron.Handler型別的func中的ctx.Next()去呼叫的(上面提到的在呼叫棧中找請求URL就是在這裡找的)。

這個macaron.Handler型別的func裡操作session的大致邏輯是,最開始先從context中搞到(獲取或建立)session,最後再呼叫session.Release()儲存session(比如儲存到redis)。

那麼我是怎麼發現 “其實這是因為重新整理操作導致原頁面的websocket斷開而走到這裡的” 的呢?因為ctx裡URL為state的那次斷點,沒經過獲取session,而直接到session.Release()。所以說,在F12的network清一下再做操作可以幹掉一些干擾項,但同時也可能把有用的幹掉了,比如還沒斷開的websocket

到這裡問題就找到了。大致流程如圖(因為那個型別為macaron.Handler的返回值是匿名方法,所以圖上就以macaron.Handler來表示了):
流程示意圖
說明一下圖中的虛線部分,前端頁面收到switch介面的返回後重新整理頁面,重新整理頁面導致websocket斷開,進而導致stateHandler返回,而此時此macaron.Handler持有的是websocket建立時的session,將此session存到redis當然會覆蓋switchHandler儲存在redis的session。

解決方案

問題找到後,解決起來也就簡單了,只需在sess.Release()之前加個判斷,如果是websocket就不執行即可,而websocket的判斷方法不止一種,這裡是根據請求頭中的Upgrade欄位來實現的,程式碼如下:

...
ctx.Next()

if ctx.Req.Header.Get("Upgrade") == "websocket" {
	return
}

if err = sess.Release(); err != nil {
...

程式碼位置:

pkg/go-macaron/session/session.go:Sessioner()

如果您有更好的想法,還望不吝賜教