
文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载本篇文章围绕开源电子书《build-web-application-with-golang》第六章「session 和数据存储」中的 6.3 小节展开以 Go 语言完整实现一个基于内存的 session 存储引擎Provider并串联 session 管理器Manager的接口设计、注册机制与 GC 回收原理。读完本文你将掌握如何为session.Manager编写并注册自定义存储引擎、理解内存 session 的 LRU 式过期回收与并发安全细节并能据此扩展到文件、数据库如 Redis/Memcache等持久化存储方案。从接口到实现session 存储引擎的定位在 6.2 小节 中我们已经实现了一个全局的 session 管理器Manager并将底层的存储抽象为Provider接口。它的设计思路借鉴了 Go 标准库database/sql/driver先定义好接口再由具体的存储实现注册进来。核心接口定义如下type Provider interface { SessionInit(sid string) (Session, error) SessionRead(sid string) (Session, error) SessionDestroy(sid string) error SessionGC(maxLifeTime int64) }四个方法的职责分别是SessionInit初始化一个 session成功则返回新的Session变量SessionRead返回sid对应的Session变量若不存在则以sid为参数调用SessionInit创建并返回SessionDestroy销毁sid对应的Session变量SessionGC根据maxLifeTime删除过期的数据。而Session接口则对应开发中最常用的四个操作——设置值、读取值、删除值以及获取当前 sessionIDtype Session interface { Set(key, value interface{}) error // set session value Get(key interface{}) interface{} // get session value Delete(key interface{}) error // delete session value SessionID() string // back current sessionID }配套的Register函数用于将具体的存储引擎按名字注册进全局注册表重复注册同名 provider 或注册 nil 会触发 panicvar provides make(map[string]Provider) // Register makes a session provide available by the provided name. // If Register is called twice with the same name or if driver is nil, // it panics. func Register(name string, provider Provider) { if provider nil { panic(session: Register provider is nil) } if _, dup : provides[name]; dup { panic(session: Register called twice for provider name) } provides[name] provider }6.3 小节正是上述接口的一份具体落地在memory包中实现一个纯内存的Provider再通过init()注册为memory引擎。这也是读者编写文件、数据库等其它存储引擎的样板。内存版 SessionStore会话数据的最小载体内存实现的核心数据结构是两个类型SessionStore与Provider。先看SessionStore它对应Session接口保存单个会话的全部状态type SessionStore struct { sid string //session id唯一标示 timeAccessed time.Time //最后访问时间 value map[interface{}]interface{} //session里面存储的值 }三个字段的分工非常清晰sidsession id 唯一标识由管理器生成后传入timeAccessed最后访问时间是 GC 判断是否过期的唯一依据value以map[interface{}]interface{}保存会话键值数据Go 的任意类型都可以作为 key 或 value。它实现的四个方法中值得注意的细节是Set、Get、Delete在读写数据之后都会调用全局 provider 的SessionUpdate(st.sid)刷新该 session 的最后访问时间让 GC 不会误删仍在使用的会话func (st *SessionStore) Set(key, value interface{}) error { st.value[key] value pder.SessionUpdate(st.sid) return nil } func (st *SessionStore) Get(key interface{}) interface{} { pder.SessionUpdate(st.sid) if v, ok : st.value[key]; ok { return v } else { return nil } } func (st *SessionStore) Delete(key interface{}) error { delete(st.value, key) pder.SessionUpdate(st.sid) return nil } func (st *SessionStore) SessionID() string { return st.sid }这正是 6.2 小节 中反复强调的机制「当我们进行了任意一个 session 操作都会对 Session 实体进行更新触发对最后访问时间的修改这样当 GC 的时候就不会误删除还在使用的 Session 实体。」内存版 Providermap list 互斥锁的存储引擎Provider负责管理所有会话它的数据结构组合是本实现最巧妙的部分type Provider struct { lock sync.Mutex //用来锁 sessions map[string]*list.Element //用来存储在内存 list *list.List //用来做gc }lock sync.Mutex保证多 goroutine 并发访问时的数据安全sessions map[string]*list.Element以 sid 为 key 的索引表value 指向双向链表中的元素实现 O(1) 的按 sid 查找list *list.List标准库container/list双向链表按访问时间维护全部 session 的先后顺序供 GC 高效回收。包级变量pder是唯一的 provider 实例var pder Provider{list: list.New()}SessionInit创建新会话func (pder *Provider) SessionInit(sid string) (session.Session, error) { pder.lock.Lock() defer pder.lock.Unlock() v : make(map[interface{}]interface{}, 0) newsess : SessionStore{sid: sid, timeAccessed: time.Now(), value: v} element : pder.list.PushBack(newsess) pder.sessions[sid] element return newsess, nil }流程分四步加锁 → 构造SessionStore记录创建时间、初始化空 map→ 追加到链表尾部 → 把链表元素指针登记进sessions索引表。这样新会话既能在 map 中被快速找到又处于链表末尾即最旧的一端。SessionRead读不到就自动创建func (pder *Provider) SessionRead(sid string) (session.Session, error) { if element, ok : pder.sessions[sid]; ok { return element.Value.(*SessionStore), nil } else { sess, err : pder.SessionInit(sid) return sess, err } return nil, nil }SessionRead与接口语义严格一致命中则从链表元素中取出*SessionStore返回未命中则直接调用SessionInit创建新会话。这保证了即使客户端携带了未知的 sid服务端也会优雅地补建一个会话而不是报错。SessionDestroy按 sid 精确销毁func (pder *Provider) SessionDestroy(sid string) error { if element, ok : pder.sessions[sid]; ok { delete(pder.sessions, sid) pder.list.Remove(element) return nil } return nil }销毁时同步做两件事从 map 索引中删除、从链表中移除元素。注意这里没有加锁——从代码结构看调用方Manager 的SessionDestroy已经在更高层持有了锁这也是读者在自行扩展时需要注意的约定。SessionGCLRU 风格的过期回收GC 是本实现的精华它充分利用了链表头部最新、尾部最旧的天然顺序func (pder *Provider) SessionGC(maxlifetime int64) { pder.lock.Lock() defer pder.lock.Unlock() for { element : pder.list.Back() if element nil { break } if (element.Value.(*SessionStore).timeAccessed.Unix() maxlifetime) time.Now().Unix() { pder.list.Remove(element) delete(pder.sessions, element.Value.(*SessionStore).sid) } else { break } } }回收逻辑可以概括为从链表尾部开始只要最后访问时间 最大生命周期仍小于当前时间就删除一旦遇到未过期的元素立即停止。这个策略成立的前提是SessionUpdate会把被访问的 session 移动到链表头部见下文因此链表从头部到尾部严格按最近访问 → 最久未访问排序最久未访问的必然在尾部一旦尾部元素未过期后面的元素必然也都未过期可以安全 break。从实现结构看这是一种非常轻量的 LRULeast Recently Used淘汰策略——GC 无需遍历全部 session只需检查链表尾部。时间比较基于timeAccessed.Unix()秒级时间戳与maxlifetime同样以秒为单位的 int64与 6.2 小节 中NewManager(provideName, cookieName string, maxLifeTime int64)的秒级生命周期参数保持同一量纲。SessionUpdate刷新访问时间并移动链表位置func (pder *Provider) SessionUpdate(sid string) error { pder.lock.Lock() defer pder.lock.Unlock() if element, ok : pder.sessions[sid]; ok { element.Value.(*SessionStore).timeAccessed time.Now() pder.list.MoveToFront(element) return nil } return nil }SessionUpdate每次做两件事更新timeAccessed为当前时间并将对应元素MoveToFront移到链表头部。SessionStore的Set/Get/Delete都会触发它这正是内存版活跃会话永不误删的核心保障也直接支撑了上述 GC 的 LRU 语义。init 注册与空白导入如何接入 session 管理器实现写好后还需要把引擎注册进全局注册表。这通过包级init()完成func init() { pder.sessions make(map[string]*list.Element, 0) session.Register(memory, pder) }init()里做了两件事初始化sessionsmap因为pder声明时只初始化了list然后调用session.Register(memory, pder)把引擎注册为memory名字。在主程序中接入时只需要空白导入该包——空白导入会执行包的init()函数从而完成注册import ( github.com/astaxie/session _ github.com/astaxie/session/providers/memory )注意示例代码依赖的是书中配套的外部包github.com/astaxie/session其providers/memory子包即本文实现的存放位置主程序通过空白导入触发注册。这是 Go 语言中导入即注册设计模式的典型用法——database/sql的驱动注册也采用同样的机制。注册完成后即可在应用入口初始化 session 管理器var globalSessions *session.Manager //然后在init函数中初始化 func init() { globalSessions, _ session.NewManager(memory, gosessionid, 3600) go globalSessions.GC() }这里NewManager(memory, gosessionid, 3600)的三个参数含义为provider 名字memory、cookie 名字gosessionid、最大生命周期3600秒即 1 小时。go globalSessions.GC()以 goroutine 方式启动后台回收任务。从 6.2 小节 的实现可以看到Manager.GC()内部利用time.AfterFunc定时器在maxLifeTime超时后调用provider.SessionGC(manager.maxLifeTime)从而保证生命周期内的 session 始终可用——这种定时器方案也可用于统计在线用户数等场景。完成以上三步实现 → 注册 → 初始化globalSessions.SessionStart(w, r)便能正常创建、读取、销毁会话而底层数据全部由本文的内存 Provider 承载。内存存储的边界与其它存储引擎的扩展方向内存存储的优点是实现简单、读写极快但其局限性同样明显6.0 章节 对此有明确论述session 本质是服务器端数据可以存储在内存、数据库或文件中。内存方案下系统一旦掉电所有会话数据全部丢失对电子商务类网站是严重事故同时内存方案也无法支撑多实例部署下的 session 共享。因此当应用需要扩展、实现 session 共享时通常会把 session 存放到 Redis、Memcache 等外部存储中6.5 小节 将详细讲解如何把 session 存储到数据库以实现共享。而无论换成何种存储编写的思路都与本文完全一致实现Provider接口的四个方法SessionInit/SessionRead/SessionDestroy/SessionGC配套实现Session接口的四个操作最后在init()中session.Register注册即可——接口设计把存储细节与上层管理完全解耦这正是database/sql/driver风格设计带来的扩展性红利。小结本文完整剖析了内存版 session 存储引擎的实现与接入全流程SessionStore承载单会话的键值数据Provider用map保证 O(1) 查找、用container/list维护 LRU 访问顺序、用sync.Mutex保证并发安全SessionGC从链表尾部批量回收过期会话init() 空白导入 session.Register(memory, pder)完成引擎注册最终通过session.NewManager(memory, gosessionid, 3600)与go globalSessions.GC()在应用中启用。掌握这套接口 注册 实现的骨架后无论是写文件存储、还是对接 Redis/Memcache都只是照葫芦画瓢地替换Provider内部实现而已。延伸阅读6.1 session 和 cookiesession 机制与 cookie 机制的关系与区别6.2 Go 如何使用 sessionsession 管理器、Provider/Session 接口与注册机制的设计6.4 预防 session 劫持会话安全与劫持防护6.5 session 存储到数据库基于数据库的 session 共享实现目录全书章节导航赞分享文档教程【免费下载链接】build-web-application-with-golangA golang ebook intro how to build a web with golang项目地址https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang点击查看免费下载相关推荐Go Web 编程实战手写基于内存的 Session 存储引擎ProviderGo Web 编程实战手写基于内存的 Session 存储引擎Provider 本篇技术指南围绕《Build Web Application with G文档教程Go Web 编程实践基于内存的 session 存储 Provider 实现与注册调用全解析Go Web 编程实践基于内存的 session 存储 Provider 实现与注册调用全解析 在 build web application with go文档教程用 Go 实现内存型 Session 存储引擎基于 Provider 接口的会话存储实战用 Go 实现内存型 Session 存储引擎基于 Provider 接口的会话存储实战 Session 管理器Manager只负责会话的创建、销毁与超时文档教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考