✨ feat(cache): 幂等只读接口接入缓存
- 新增 withCache 包装:命中直接返回,miss 回源后写入,仅缓存成功响应 - 详情/目录/批量详情/章节摘要缓存 24 小时;搜索/分类列表/分类详情短缓存 10 分钟 - 7 个 fqapi 处理函数原函数体不动、改名 fetchXxx,外层套缓存壳 - redisGetCache 补 3 秒读超时,与写路径一致,防远程 Redis 网络抖动拖垮请求 - 新增 TestWithCache:验证命中不回源、失败响应不缓存
This commit is contained in:
1 parent
f0d89b462b
commit
586b714f3c
10 files changed
+109
-8
No files matched your search
@@ -255,6 +255,19 @@ func isCacheEnabled() bool {
|
|||||||
return cfg.Cache.Enabled
|
return cfg.Cache.Enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withCache 只读接口的缓存包装:命中直接返回,miss 回源后写入(仅缓存成功响应)
|
||||||
|
// ponytail: 空参等防御分支若漏到此处,其错误 JSON 也会被缓存到 TTL 到期——handler 层 validateParams 已拦截
|
||||||
|
func withCache(key string, ttl int, fetch func() (string, error)) (string, error) {
|
||||||
|
if c := GetCache(key); c != "" {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
result, err := fetch()
|
||||||
|
if err == nil {
|
||||||
|
UpdateCache(key, result, ttl)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetCache 获取正文内容缓存
|
// GetCache 获取正文内容缓存
|
||||||
func GetCache(key string) string {
|
func GetCache(key string) string {
|
||||||
if !isCacheEnabled() {
|
if !isCacheEnabled() {
|
||||||
|
|||||||
@@ -55,3 +55,46 @@ func TestConcurrentCacheAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithCache 验证缓存命中不回源、失败响应不缓存
|
||||||
|
func TestWithCache(t *testing.T) {
|
||||||
|
if err := config.LoadConfig("../../config/config.yaml"); err != nil {
|
||||||
|
t.Skipf("无法加载配置: %v", err)
|
||||||
|
}
|
||||||
|
db, err := sql.Open("sqlite3", t.TempDir()+"/test.db?_journal_mode=WAL&_busy_timeout=5000")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldDB, oldDriver := cacheDB, dbDriver
|
||||||
|
cacheDB, dbDriver = db, "sqlite3"
|
||||||
|
defer func() {
|
||||||
|
cacheDB, dbDriver = oldDB, oldDriver
|
||||||
|
db.Close()
|
||||||
|
}()
|
||||||
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS cache (
|
||||||
|
key TEXT PRIMARY KEY, value TEXT,
|
||||||
|
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||||
|
)`); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
calls := 0
|
||||||
|
fetch := func() (string, error) { calls++; return "result", nil }
|
||||||
|
if got, _ := withCache("wc_hit", 60, fetch); got != "result" {
|
||||||
|
t.Fatalf("首次应返回回源结果, got %q", got)
|
||||||
|
}
|
||||||
|
if got, _ := withCache("wc_hit", 60, fetch); got != "result" {
|
||||||
|
t.Fatalf("命中应返回缓存结果, got %q", got)
|
||||||
|
}
|
||||||
|
if calls != 1 {
|
||||||
|
t.Fatalf("命中缓存不应回源, fetch 调用 %d 次", calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
errCalls := 0
|
||||||
|
errFetch := func() (string, error) { errCalls++; return "", fmt.Errorf("boom") }
|
||||||
|
withCache("wc_miss", 60, errFetch)
|
||||||
|
withCache("wc_miss", 60, errFetch)
|
||||||
|
if errCalls != 2 {
|
||||||
|
t.Fatalf("失败响应不应被缓存, fetch 调用 %d 次", errCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,14 @@ import (
|
|||||||
|
|
||||||
var catalogLogger = common.NewLogger("catalog", "INFO", "")
|
var catalogLogger = common.NewLogger("catalog", "INFO", "")
|
||||||
|
|
||||||
// HandleGetCatalog 处理获取书籍目录
|
// HandleGetCatalog 处理获取书籍目录,目录数据基本不变,缓存 24 小时
|
||||||
func HandleGetCatalog(bookID string) (string, error) {
|
func HandleGetCatalog(bookID string) (string, error) {
|
||||||
|
return withCache("catalog_" + bookID, 86400, func() (string, error) {
|
||||||
|
return fetchCatalog(bookID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCatalog(bookID string) (string, error) {
|
||||||
if bookID == "" {
|
if bookID == "" {
|
||||||
return BuildErrorResponse("需要参数 book_id"), nil
|
return BuildErrorResponse("需要参数 book_id"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import (
|
|||||||
|
|
||||||
var categoryFrontLogger = common.NewLogger("category_front", "INFO", "")
|
var categoryFrontLogger = common.NewLogger("category_front", "INFO", "")
|
||||||
|
|
||||||
// HandleCategoryFront 处理获取分类列表
|
// HandleCategoryFront 处理获取分类列表,分类列表短缓存 10 分钟
|
||||||
func HandleCategoryFront(tab interface{}) (string, error) {
|
func HandleCategoryFront(tab interface{}) (string, error) {
|
||||||
|
return withCache(fmt.Sprintf("category_front_%v", tab), 600, func() (string, error) {
|
||||||
|
return fetchCategoryFront(tab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCategoryFront(tab interface{}) (string, error) {
|
||||||
tabInt := ParseIntParam(tab, 0)
|
tabInt := ParseIntParam(tab, 0)
|
||||||
|
|
||||||
reqURL := BuildURL(
|
reqURL := BuildURL(
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import (
|
|||||||
|
|
||||||
var categoryLandingLogger = common.NewLogger("category_landing", "INFO", "")
|
var categoryLandingLogger = common.NewLogger("category_landing", "INFO", "")
|
||||||
|
|
||||||
// HandleCategoryLanding 处理获取分类详情
|
// HandleCategoryLanding 处理获取分类详情,分类详情短缓存 10 分钟
|
||||||
func HandleCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy interface{}) (string, error) {
|
func HandleCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy interface{}) (string, error) {
|
||||||
|
return withCache(fmt.Sprintf("category_landing_%v_%v_%v_%v_%v", categoryID, offset, wordNumber, bookStatus, sortBy), 600, func() (string, error) {
|
||||||
|
return fetchCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCategoryLanding(categoryID, offset, wordNumber, bookStatus, sortBy interface{}) (string, error) {
|
||||||
// 参数转换
|
// 参数转换
|
||||||
categoryIDInt := ParseIntParam(categoryID, 0)
|
categoryIDInt := ParseIntParam(categoryID, 0)
|
||||||
offsetInt := ParseIntParam(offset, 0)
|
offsetInt := ParseIntParam(offset, 0)
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import (
|
|||||||
|
|
||||||
var detailLogger = common.NewLogger("detail", "INFO", "")
|
var detailLogger = common.NewLogger("detail", "INFO", "")
|
||||||
|
|
||||||
// HandleGetDetail 处理获取书籍详情
|
// HandleGetDetail 处理获取书籍详情,详情数据基本不变,缓存 24 小时
|
||||||
func HandleGetDetail(bookID string) (string, error) {
|
func HandleGetDetail(bookID string) (string, error) {
|
||||||
|
return withCache("detail_" + bookID, 86400, func() (string, error) {
|
||||||
|
return fetchDetail(bookID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchDetail(bookID string) (string, error) {
|
||||||
if bookID == "" {
|
if bookID == "" {
|
||||||
return BuildErrorResponse("需要参数 book_id"), nil
|
return BuildErrorResponse("需要参数 book_id"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import (
|
|||||||
|
|
||||||
var itemSummaryLogger = common.NewLogger("item_summary", "INFO", "")
|
var itemSummaryLogger = common.NewLogger("item_summary", "INFO", "")
|
||||||
|
|
||||||
// HandleItemSummary 处理获取章节摘要
|
// HandleItemSummary 处理获取章节摘要,章节摘要数据基本不变,缓存 24 小时
|
||||||
func HandleItemSummary(itemIDs, bookID string) (string, error) {
|
func HandleItemSummary(itemIDs, bookID string) (string, error) {
|
||||||
|
return withCache("item_summary_" + bookID + "_" + itemIDs, 86400, func() (string, error) {
|
||||||
|
return fetchItemSummary(itemIDs, bookID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchItemSummary(itemIDs, bookID string) (string, error) {
|
||||||
if itemIDs == "" || bookID == "" {
|
if itemIDs == "" || bookID == "" {
|
||||||
return BuildErrorResponseWithCode(100144, "参数错误"), nil
|
return BuildErrorResponseWithCode(100144, "参数错误"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,8 +107,14 @@ func chunkArray(arr []string, size int) [][]string {
|
|||||||
return chunks
|
return chunks
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleGetMultiDetail 处理批量获取书籍详情
|
// HandleGetMultiDetail 处理批量获取书籍详情,详情数据基本不变,缓存 24 小时
|
||||||
func HandleGetMultiDetail(bookID interface{}) (string, error) {
|
func HandleGetMultiDetail(bookID interface{}) (string, error) {
|
||||||
|
return withCache(fmt.Sprintf("multi_detail_%v", bookID), 86400, func() (string, error) {
|
||||||
|
return fetchMultiDetail(bookID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMultiDetail(bookID interface{}) (string, error) {
|
||||||
if bookID == nil {
|
if bookID == nil {
|
||||||
return BuildErrorResponse("需要参数 book_id"), nil
|
return BuildErrorResponse("需要参数 book_id"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,14 @@ var searchLogger = common.NewLogger("search", "INFO", "")
|
|||||||
// GetDeviceHeader 获取设备header(需要从外部注入)
|
// GetDeviceHeader 获取设备header(需要从外部注入)
|
||||||
var GetDeviceHeader func() string
|
var GetDeviceHeader func() string
|
||||||
|
|
||||||
// HandleBookSearch 处理书籍搜索
|
// HandleBookSearch 处理书籍搜索,搜索结果短缓存 10 分钟,减轻上游压力
|
||||||
func HandleBookSearch(key string, offset int, tabType int) (string, error) {
|
func HandleBookSearch(key string, offset int, tabType int) (string, error) {
|
||||||
|
return withCache(fmt.Sprintf("search_%s_%d_%d", key, offset, tabType), 600, func() (string, error) {
|
||||||
|
return fetchBookSearch(key, offset, tabType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBookSearch(key string, offset int, tabType int) (string, error) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return BuildErrorResponseWithCode(1, "需要参数 key"), nil
|
return BuildErrorResponseWithCode(1, "需要参数 key"), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ const (
|
|||||||
|
|
||||||
// redisGetCache 从 Redis 获取正文缓存
|
// redisGetCache 从 Redis 获取正文缓存
|
||||||
func redisGetCache(key string) string {
|
func redisGetCache(key string) string {
|
||||||
val, err := redisCache.Get(context.Background(), redisCacheKeyPrefix+key).Result()
|
// 读操作同样加 3s 超时,防止远程 Redis 网络抖动拖垮请求(与写路径一致)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
val, err := redisCache.Get(ctx, redisCacheKeyPrefix+key).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "" // miss 或错误都视为未命中
|
return "" // miss 或错误都视为未命中
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user