lolly/internal/lua/shared_dict.go
xfy ad177e9640 docs(lua): 为 Lua API 模块添加标准化 godoc 注释
为所有 Lua API 文件添加完整的包级和函数级文档注释:
- api_balancer: 负载均衡 API(set_current_peer, set_more_tries 等)
- api_ctx: 请求上下文存储 API(ngx.ctx)
- api_location: 子请求捕获 API(ngx.location.capture)
- api_log: 日志输出 API(ngx.log)
- api_req: 请求对象 API
- api_resp: 响应对象 API
- api_shared_dict: 共享字典 API
- api_socket_tcp: TCP socket API
- api_timer: 定时器 API
- api_var: 变量 API
- engine: Lua 引擎核心
- context: 请求上下文管理
- coroutine: 协程调度器
- middleware: 中间件集成
- filter_writer: 响应过滤器
- cache: Lua 脚本缓存
- shared_dict: 共享字典实现
- socket_manager: socket 连接管理

注释格式遵循 Go 官方风格,包含功能说明、参数说明和注意事项。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 10:59:17 +08:00

428 lines
9.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package lua 提供 Lua 脚本嵌入能力。
//
// 该文件实现共享内存字典SharedDict包括
// - SharedDict并发安全的 key-value 存储,带 LRU 淘汰策略
// - 过期机制:支持 TTL 过期,惰性删除 + 主动清理
// - 数值操作Incr 支持原子自增
//
// 注意事项:
// - 所有公开方法均为并发安全(使用 sync.Mutex
// - 容量满时优先淘汰过期条目,其次淘汰 LRU 最久未使用的条目
//
// 作者xfy
package lua
import (
"container/list"
"fmt"
"sync"
"time"
)
// SharedDict 共享内存字典。
//
// 提供并发安全的 key-value 存储,兼容 ngx.shared.DICT API。
// 特性:
// - 支持 TTL 过期(惰性删除 + FlushExpired 主动清理)
// - LRU 淘汰策略(容量满时淘汰最久未使用的条目)
// - 所有公开方法均为并发安全
type SharedDict struct {
// data 键值存储映射
data map[string]*sharedDictEntry
// lruList LRU 链表,用于淘汰策略
lruList *list.List
// 字典名称
name string
// 最大条目数
maxItems int
// 互斥锁
mu sync.Mutex
}
// sharedDictEntry 字典条目。
//
// 存储单个 key-value 对及其过期信息和 LRU 链表引用。
type sharedDictEntry struct {
// expiredAt 过期时间,零值表示永不过期
expiredAt time.Time
// element LRU 链表中的节点引用
element *list.Element
// 键名
key string
// 值(字符串类型)
value string
}
// NewSharedDict 创建新的共享字典实例。
//
// 参数:
// - name: 字典名称(用于标识)
// - maxItems: 最大条目数(达到上限时触发 LRU 淘汰)
//
// 返回值:
// - *SharedDict: 初始化的字典实例
func NewSharedDict(name string, maxItems int) *SharedDict {
return &SharedDict{
name: name,
maxItems: maxItems,
data: make(map[string]*sharedDictEntry),
lruList: list.New(),
}
}
// Get 获取指定键的值。
//
// 返回值:
// - value: 存储的值,不存在或过期时返回空字符串
// - expired: 是否存在但已过期true=存在但过期false=不存在或未过期)
// - err: 错误信息(当前实现始终返回 nil
//
// 注意:访问成功时会更新 LRU 位置(移到链表前端)。
func (d *SharedDict) Get(key string) (string, bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
entry, ok := d.data[key]
if !ok {
return "", false, nil // 不存在
}
// 检查过期
if !entry.expiredAt.IsZero() && time.Now().After(entry.expiredAt) {
// 已过期,删除并返回
d.deleteEntry(entry)
return "", true, nil // 存在但已过期
}
// 更新 LRU - 移到前端
d.lruList.MoveToFront(entry.element)
return entry.value, false, nil
}
// Set 设置键值对。
//
// 如果键已存在,更新其值和过期时间。
// 如果是新键且容量已满,先尝试淘汰过期条目,再淘汰 LRU 条目。
//
// 参数:
// - key: 键名
// - value: 值
// - ttl: 过期时间,零值表示永不过期
//
// 返回值:
// - ok: true 表示设置成功false 表示容量满且无法淘汰
// - err: 错误信息(当前实现始终返回 nil
func (d *SharedDict) Set(key, value string, ttl time.Duration) (bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
// 检查是否已存在
if entry, ok := d.data[key]; ok {
// 更新现有条目
entry.value = value
if ttl > 0 {
entry.expiredAt = time.Now().Add(ttl)
} else {
entry.expiredAt = time.Time{} // 清除过期时间
}
d.lruList.MoveToFront(entry.element)
return true, nil
}
// 新条目,检查容量
if len(d.data) >= d.maxItems {
// 尝试淘汰过期条目
d.evictExpired()
if len(d.data) >= d.maxItems {
// 淘汰 LRU 最久未使用的条目
if !d.evictLRU() {
return false, nil // 无法淘汰(字典为空?)
}
}
}
// 创建新条目
expiredAt := time.Time{}
if ttl > 0 {
expiredAt = time.Now().Add(ttl)
}
element := d.lruList.PushFront(key)
entry := &sharedDictEntry{
key: key,
value: value,
expiredAt: expiredAt,
element: element,
}
d.data[key] = entry
return true, nil
}
// Add 添加键值对(仅在键不存在时设置)。
//
// 与 Set 的区别如果键已存在包括已过期的条目Add 会返回 false。
//
// 参数:
// - key: 键名
// - value: 值
// - ttl: 过期时间
//
// 返回值:
// - ok: true 表示添加成功false 表示已存在或容量满
// - err: 错误信息
func (d *SharedDict) Add(key, value string, ttl time.Duration) (bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
// 检查是否已存在(包括过期的也算存在)
if _, ok := d.data[key]; ok {
return false, nil // 已存在
}
// 检查容量并淘汰
if len(d.data) >= d.maxItems {
d.evictExpired()
if len(d.data) >= d.maxItems {
if !d.evictLRU() {
return false, nil
}
}
}
// 创建新条目
expiredAt := time.Time{}
if ttl > 0 {
expiredAt = time.Now().Add(ttl)
}
element := d.lruList.PushFront(key)
entry := &sharedDictEntry{
key: key,
value: value,
expiredAt: expiredAt,
element: element,
}
d.data[key] = entry
return true, nil
}
// Incr 将指定键的值作为整数自增。
//
// 如果键不存在,创建初始值为 0 后再自增。
// 如果值不是纯数字字符串,返回 0非错误
//
// 参数:
// - key: 键名
// - increment: 自增量(可为负数)
//
// 返回值:
// - newValue: 自增后的值
// - err: 错误信息(当前实现始终返回 nil
func (d *SharedDict) Incr(key string, increment int) (int, error) {
d.mu.Lock()
defer d.mu.Unlock()
entry, ok := d.data[key]
if !ok {
// 不存在,创建初始值
if len(d.data) >= d.maxItems {
d.evictExpired()
if len(d.data) >= d.maxItems {
if !d.evictLRU() {
return 0, nil // 无法创建
}
}
}
element := d.lruList.PushFront(key)
entry = &sharedDictEntry{
key: key,
value: "0",
element: element,
}
d.data[key] = entry
}
// 解析数值
var current int
for _, c := range entry.value {
if c < '0' || c > '9' {
return 0, fmt.Errorf("not a number")
}
current = current*10 + int(c-'0')
}
newValue := current + increment
entry.value = intToStr(newValue)
d.lruList.MoveToFront(entry.element)
return newValue, nil
}
// Delete 删除指定键。
func (d *SharedDict) Delete(key string) error {
d.mu.Lock()
defer d.mu.Unlock()
if entry, ok := d.data[key]; ok {
d.deleteEntry(entry)
}
return nil
}
// FlushAll 清空字典中的所有条目。
func (d *SharedDict) FlushAll() error {
d.mu.Lock()
defer d.mu.Unlock()
d.data = make(map[string]*sharedDictEntry)
d.lruList = list.New()
return nil
}
// FlushExpired 清除所有过期条目。
//
// 返回值:
// - int: 被清除的条目数
func (d *SharedDict) FlushExpired() int {
d.mu.Lock()
defer d.mu.Unlock()
return d.evictExpired()
}
// Size 返回当前条目数(包括已过期的条目)。
func (d *SharedDict) Size() int {
d.mu.Lock()
defer d.mu.Unlock()
return len(d.data)
}
// FreeSlots 返回剩余可添加的条目数。
func (d *SharedDict) FreeSlots() int {
d.mu.Lock()
defer d.mu.Unlock()
return d.maxItems - len(d.data)
}
// deleteEntry 删除指定条目(内部方法,需已持有锁)。
func (d *SharedDict) deleteEntry(entry *sharedDictEntry) {
d.lruList.Remove(entry.element)
delete(d.data, entry.key)
}
// evictExpired 淘汰所有过期条目(内部方法,需已持有锁)。
//
// 从 LRU 链表尾部(最久未使用)开始扫描,删除过期条目。
//
// 返回值:
// - int: 被淘汰的条目数
func (d *SharedDict) evictExpired() int {
now := time.Now()
count := 0
// 从 LRU 链表尾部(最久未使用)开始检查
for elem := d.lruList.Back(); elem != nil; {
// 类型断言检查
key, ok := elem.Value.(string)
if !ok {
// 类型不正确,移除元素
next := elem.Prev()
d.lruList.Remove(elem)
elem = next
continue
}
entry, ok := d.data[key]
if !ok {
// 数据不一致,跳过
next := elem.Prev()
d.lruList.Remove(elem)
elem = next
continue
}
if !entry.expiredAt.IsZero() && now.After(entry.expiredAt) {
// 已过期,删除
d.deleteEntry(entry)
count++
elem = d.lruList.Back() // 重新从尾部开始
} else {
break // 未过期,停止(链表顺序保证前面都是未过期的)
}
}
return count
}
// evictLRU 淘汰 LRU 最久未使用的条目(内部方法,需已持有锁)。
//
// 返回值:
// - bool: true 表示成功淘汰false 表示链表为空
func (d *SharedDict) evictLRU() bool {
if d.lruList.Len() == 0 {
return false
}
elem := d.lruList.Back()
if elem == nil {
return false
}
// 类型断言检查
key, ok := elem.Value.(string)
if !ok {
// 类型不正确,移除链表元素
d.lruList.Remove(elem)
return d.evictLRU()
}
entry, ok := d.data[key]
if ok {
d.deleteEntry(entry)
return true
}
// 数据不一致,移除链表元素并重试
d.lruList.Remove(elem)
return d.evictLRU()
}
// intToStr 整数转字符串(简单实现,避免 strconv 依赖)
func intToStr(n int) string {
if n == 0 {
return "0"
}
var negative bool
if n < 0 {
negative = true
n = -n
}
var buf []byte
for n > 0 {
buf = append(buf, byte('0'+n%10))
n /= 10
}
if negative {
buf = append(buf, '-')
}
// 反转
for i, j := 0, len(buf)-1; i < j; i, j = i+1, j-1 {
buf[i], buf[j] = buf[j], buf[i]
}
return string(buf)
}