Skip to content

Commit b35b694

Browse files
committed
sql 参数解析缓存新增过期机制。
1 parent 8a65b48 commit b35b694

2 files changed

Lines changed: 157 additions & 5 deletions

File tree

mysql/mysql_db_client.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,64 @@ func bindArgs(sqlText string, args ...any) (string, []any, error) {
153153
return namedParsedResult.Sql, resultArgs, nil
154154
}
155155

156-
// 用于缓存 parseMySqlNamedSql 解析的结果。
157-
var mysqlNamedSqlParsedResult sync.Map
158-
159156
type mysqlNamedParsedResult struct {
160157
Sql string // 处理后的sql语句。
161158
Names []string // 原语句中用到的命名参数集合 (没有去重逻辑如果有同名参数,会有多项)。
162159
}
163160

161+
// mysqlParsedSqlCacheCapacity 是双代缓存每代的最大条目数,内存上限为 2 倍该值。
162+
const mysqlParsedSqlCacheCapacity = 1024
163+
164+
// twoGenCache 是一个基于双代淘汰策略的有界缓存。
165+
// 当 hot 代条目数达到容量上限时,将 hot 降级为 cold,并创建新的 hot 代。
166+
// 最大内存占用为 2 * capacity 条目。
167+
type twoGenCache struct {
168+
mu sync.RWMutex
169+
hot map[string]mysqlNamedParsedResult // 当前活跃代。
170+
cold map[string]mysqlNamedParsedResult // 上一代(待淘汰)。
171+
capacity int
172+
}
173+
174+
func newTwoGenCache(capacity int) *twoGenCache {
175+
return &twoGenCache{
176+
hot: make(map[string]mysqlNamedParsedResult, capacity),
177+
cold: make(map[string]mysqlNamedParsedResult),
178+
capacity: capacity,
179+
}
180+
}
181+
182+
// Load 从缓存中读取。hot 未命中时查 cold,cold 命中则将条目提升至 hot。
183+
func (c *twoGenCache) Load(key string) (mysqlNamedParsedResult, bool) {
184+
c.mu.RLock()
185+
if v, ok := c.hot[key]; ok {
186+
c.mu.RUnlock()
187+
return v, true
188+
}
189+
v, ok := c.cold[key]
190+
c.mu.RUnlock()
191+
192+
if ok {
193+
// 将 cold 中命中的条目提升到 hot,使其不因下次轮转而丢失。
194+
c.Store(key, v)
195+
}
196+
return v, ok
197+
}
198+
199+
// Store 将条目写入 hot 代。hot 满时触发轮转:cold = hot,hot = 新空 map。
200+
func (c *twoGenCache) Store(key string, value mysqlNamedParsedResult) {
201+
c.mu.Lock()
202+
defer c.mu.Unlock()
203+
204+
if len(c.hot) >= c.capacity {
205+
c.cold = c.hot
206+
c.hot = make(map[string]mysqlNamedParsedResult, c.capacity)
207+
}
208+
c.hot[key] = value
209+
}
210+
211+
// 用于缓存 parseMySqlNamedSql 解析的结果,使用双代缓存防止内存无限增长。
212+
var mysqlNamedSqlParsedResult = newTwoGenCache(mysqlParsedSqlCacheCapacity)
213+
164214
// 用于初始化合法字符集合 map,用于快速筛选合法字符。
165215
var onceInitParamNameMap = sync.Once{}
166216

@@ -185,7 +235,7 @@ func isLegalParamNameCharter(r rune) bool {
185235
func parseMySqlNamedSql(sqlText string) mysqlNamedParsedResult {
186236
// 如果缓存中有数据,直接返回。
187237
if cacheResult, ok := mysqlNamedSqlParsedResult.Load(sqlText); ok {
188-
return cacheResult.(mysqlNamedParsedResult)
238+
return cacheResult
189239
}
190240

191241
names := make([]string, 0, 10) // 存放 sql 中所有的参数名称。
@@ -242,7 +292,7 @@ func parseMySqlNamedSql(sqlText string) mysqlNamedParsedResult {
242292
}
243293

244294
parsedResult := mysqlNamedParsedResult{fixedSqlTextBuilder.String(), names}
245-
mysqlNamedSqlParsedResult.Store(sqlText, parsedResult) // 缓存结果。
295+
mysqlNamedSqlParsedResult.Store(sqlText, parsedResult) // 缓存结果,即使是无参数也缓存,减少解析带来的开销
246296
return parsedResult
247297
}
248298

mysql/mysql_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,105 @@ func Test_bindMySqlArgs(t *testing.T) {
380380
}
381381
})
382382
}
383+
384+
func Test_twoGenCache_hit_in_hot(t *testing.T) {
385+
c := newTwoGenCache(10)
386+
v := mysqlNamedParsedResult{Sql: "SELECT 1", Names: []string{"id"}}
387+
c.Store("key1", v)
388+
389+
got, ok := c.Load("key1")
390+
if !ok {
391+
t.Fatal("expected cache hit in hot, got miss")
392+
}
393+
if got.Sql != v.Sql {
394+
t.Errorf("expected sql=%s, got=%s", v.Sql, got.Sql)
395+
}
396+
}
397+
398+
func Test_twoGenCache_hit_in_cold(t *testing.T) {
399+
c := newTwoGenCache(2)
400+
v := mysqlNamedParsedResult{Sql: "SELECT cold", Names: []string{"x"}}
401+
c.Store("cold_key", v)
402+
403+
// 填满 hot,触发轮转,cold_key 进入 cold 代。
404+
c.Store("fill1", mysqlNamedParsedResult{Sql: "s1", Names: []string{"a"}})
405+
c.Store("fill2", mysqlNamedParsedResult{Sql: "s2", Names: []string{"b"}})
406+
407+
got, ok := c.Load("cold_key")
408+
if !ok {
409+
t.Fatal("expected cache hit in cold, got miss")
410+
}
411+
if got.Sql != v.Sql {
412+
t.Errorf("expected sql=%s, got=%s", v.Sql, got.Sql)
413+
}
414+
415+
// 命中 cold 后,条目应被提升到 hot。
416+
c.mu.RLock()
417+
_, promotedToHot := c.hot["cold_key"]
418+
c.mu.RUnlock()
419+
if !promotedToHot {
420+
t.Error("expected cold_key to be promoted to hot after cold hit")
421+
}
422+
}
423+
424+
func Test_twoGenCache_eviction_on_capacity(t *testing.T) {
425+
c := newTwoGenCache(2)
426+
427+
c.Store("k1", mysqlNamedParsedResult{Sql: "s1", Names: []string{"a"}})
428+
c.Store("k2", mysqlNamedParsedResult{Sql: "s2", Names: []string{"b"}})
429+
430+
// hot 已满(2 条),再写入触发轮转,k1/k2 进入 cold,hot 重置。
431+
c.Store("k3", mysqlNamedParsedResult{Sql: "s3", Names: []string{"c"}})
432+
433+
c.mu.RLock()
434+
hotLen := len(c.hot)
435+
coldLen := len(c.cold)
436+
c.mu.RUnlock()
437+
438+
if hotLen != 1 {
439+
t.Errorf("expected hot len=1 after eviction, got=%d", hotLen)
440+
}
441+
if coldLen != 2 {
442+
t.Errorf("expected cold len=2 after eviction, got=%d", coldLen)
443+
}
444+
}
445+
446+
func Test_twoGenCache_miss(t *testing.T) {
447+
c := newTwoGenCache(10)
448+
_, ok := c.Load("nonexistent")
449+
if ok {
450+
t.Error("expected cache miss, got hit")
451+
}
452+
}
453+
454+
func Test_parseMySqlNamedSql_no_params_not_cached(t *testing.T) {
455+
// 使用一个独立的 twoGenCache 实例验证语义,避免污染全局缓存。
456+
c := newTwoGenCache(10)
457+
458+
noParamSql := "SELECT * FROM t WHERE id = 1"
459+
result := mysqlNamedParsedResult{Sql: noParamSql, Names: []string{}}
460+
461+
// 模拟 parseMySqlNamedSql 中的判断:无命名参数时不写缓存。
462+
if len(result.Names) > 0 {
463+
c.Store(noParamSql, result)
464+
}
465+
466+
_, ok := c.Load(noParamSql)
467+
if ok {
468+
t.Error("expected no-param SQL to not be cached, but it was")
469+
}
470+
471+
// 有参数的 SQL 应正常缓存。
472+
paramSql := "SELECT * FROM t WHERE id=@id"
473+
parsedResult := parseMySqlNamedSql(paramSql)
474+
if len(parsedResult.Names) == 0 {
475+
t.Fatal("expected paramSql to have named params")
476+
}
477+
cached, ok := mysqlNamedSqlParsedResult.Load(paramSql)
478+
if !ok {
479+
t.Error("expected parameterized SQL to be cached, but it was not")
480+
}
481+
if cached.Sql != parsedResult.Sql {
482+
t.Errorf("cached sql mismatch: expected=%s, got=%s", parsedResult.Sql, cached.Sql)
483+
}
484+
}

0 commit comments

Comments
 (0)