当macaron的session配了redis并且遇上了websocket——一个session“不”更新的bug

Posted -_-void

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了当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()

如果您有更好的想法,还望不吝赐教

以上是关于当macaron的session配了redis并且遇上了websocket——一个session“不”更新的bug的主要内容,如果未能解决你的问题,请参考以下文章

golang macaron解决跨域

golang macaron解决跨域

spring boot 中使用redis session

Spring Session Data Redis实现session共享

redis使用基础 ——Redis存储Session

如何在测试中使用在 beforeAll() 钩子中分配了值的变量