Skip to content

Commit 9e4a1ac

Browse files
committed
2 parents a9666f9 + fa52365 commit 9e4a1ac

14 files changed

Lines changed: 876 additions & 1169 deletions

File tree

lib/cache/cache_manager.js

Lines changed: 173 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,43 @@ import BLOG from '@/blog.config'
22
import FileCache from './local_file_cache'
33
import MemoryCache from './memory_cache'
44
import RedisCache from './redis_cache'
5+
// import VercelCache from './vercel_cache'
56

6-
// 配置是否开启Vercel环境中的缓存,因为Vercel中现有两种缓存方式在无服务环境下基本都是无意义的,纯粹的浪费资源
7-
const enableCacheInVercel =
7+
const cacheStats = {
8+
hit: 0,
9+
miss: 0,
10+
set: 0,
11+
error: 0,
12+
total: 0,
13+
perStore: {} // { redis: {hit, set}, memory: {...} }
14+
}
15+
16+
const isBuildPhase =
817
process.env.npm_lifecycle_event === 'build' ||
9-
process.env.npm_lifecycle_event === 'export' ||
10-
!BLOG['isProd']
11-
12-
/**
13-
* 尝试从缓存中获取数据,如果没有则尝试获取数据并写入缓存,最终返回所需数据
14-
* @param key
15-
* @param getDataFunction
16-
* @param getDataArgs
17-
* @returns {Promise<*|null>}
18-
*/
19-
export async function getOrSetDataWithCache(
20-
key,
21-
getDataFunction,
22-
...getDataArgs
23-
) {
18+
process.env.npm_lifecycle_event === 'export'
19+
20+
const enableLocalCache = isBuildPhase || !BLOG['isProd']
21+
const hasRedis = !!BLOG.REDIS_URL
22+
23+
const inflightMap = new Map()
24+
25+
const pid = process.pid
26+
27+
function isVercelEnv() {
28+
return !!process.env.VERCEL
29+
}
30+
31+
function cacheLog(action, key, extra = '') {
32+
const type = getCacheType()
33+
console.log(
34+
`[Cache][${type.toUpperCase()}][pid:${process.pid}] ${action} key:${key} ${extra}`
35+
)
36+
}
37+
38+
export async function getOrSetDataWithCache(key, getDataFunction, ...getDataArgs) {
2439
return getOrSetDataWithCustomCache(key, null, getDataFunction, ...getDataArgs)
2540
}
2641

27-
/**
28-
* 尝试从缓存中获取数据,如果没有则尝试获取数据并自定义写入缓存,最终返回所需数据
29-
* @param key
30-
* @param customCacheTime
31-
* @param getDataFunction
32-
* @param getDataArgs
33-
* @returns {Promise<*|null>}
34-
*/
3542
export async function getOrSetDataWithCustomCache(
3643
key,
3744
customCacheTime,
@@ -40,67 +47,163 @@ export async function getOrSetDataWithCustomCache(
4047
) {
4148
const dataFromCache = await getDataFromCache(key)
4249
if (dataFromCache) {
43-
// console.log('[缓存-->>API]:', key) // 避免过多的缓存日志输出
50+
// cacheLog('HIT', key)
4451
return dataFromCache
4552
}
46-
const data = await getDataFunction(...getDataArgs)
47-
if (data) {
48-
// console.log('[API-->>缓存]:', key)
49-
await setDataToCache(key, data, customCacheTime)
53+
54+
if (inflightMap.has(key)) {
55+
// cacheLog('INFLIGHT-WAIT', key)
56+
return inflightMap.get(key)
5057
}
51-
return data || null
58+
59+
cacheLog('MISS', key, '缓存未命中,发起真实请求')
60+
61+
const promise = getDataFunction(...getDataArgs)
62+
.then(async data => {
63+
if (data) {
64+
await setDataToCache(key, data, customCacheTime)
65+
cacheLog('SET', key, '写入缓存成功')
66+
}
67+
inflightMap.delete(key)
68+
return data || null
69+
})
70+
.catch(err => {
71+
inflightMap.delete(key)
72+
cacheLog('ERROR', key, err.message)
73+
throw err
74+
})
75+
76+
inflightMap.set(key, promise)
77+
return promise
5278
}
5379

54-
/**
55-
* 为减少频繁接口请求,notion数据将被缓存
56-
* @param {*} key
57-
* @returns
58-
*/
59-
export async function getDataFromCache(key, force) {
60-
if (JSON.parse(BLOG.ENABLE_CACHE) || force) {
61-
const dataFromCache = await getApi().getCache(key)
62-
if (!dataFromCache || JSON.stringify(dataFromCache) === '[]') {
63-
return null
80+
export async function setDataToCache(key, data, customCacheTime) {
81+
if (!data) return
82+
83+
const chain = getCacheChain()
84+
85+
for (const { name, api } of chain) {
86+
try {
87+
await api.setCache(key, data, customCacheTime)
88+
// cacheLog('SET', key, `to:${name}`)
89+
90+
cacheStats.set++
91+
cacheStats.perStore[name] = cacheStats.perStore[name] || { hit: 0, set: 0 }
92+
cacheStats.perStore[name].set++
93+
94+
return
95+
} catch (e) {
96+
console.warn(`[Cache] ${name} set failed key:${key}`, e.message)
97+
cacheStats.error++
98+
6499
}
65-
// console.trace('[API-->>缓存]:', key, dataFromCache)
66-
return dataFromCache
67-
} else {
68-
return null
69100
}
101+
102+
console.warn(`[Cache] ALL set failed key:${key}`)
70103
}
71104

72-
/**
73-
* 写入缓存
74-
* @param {*} key
75-
* @param {*} data
76-
* @param {*} customCacheTime
77-
* @returns
78-
*/
79-
export async function setDataToCache(key, data, customCacheTime) {
80-
if (!enableCacheInVercel || !data) {
81-
return
105+
export async function getDataFromCache(key, force) {
106+
if (!JSON.parse(BLOG.ENABLE_CACHE) && !force) return null
107+
108+
const chain = getCacheChain()
109+
110+
for (const { name, api } of chain) {
111+
try {
112+
const data = await api.getCache(key)
113+
114+
if (data && JSON.stringify(data) !== '[]') {
115+
// cacheLog('HIT', key, `from:${name}`)
116+
cacheStats.hit++
117+
cacheStats.perStore[name] = cacheStats.perStore[name] || { hit: 0, set: 0 }
118+
cacheStats.perStore[name].hit++
119+
return data
120+
}
121+
} catch (e) {
122+
cacheStats.error++
123+
console.warn(`[Cache] ${name} get failed key:${key}`, e.message)
124+
}
82125
}
83-
// console.trace('[API-->>缓存写入]:', key)
84-
await getApi().setCache(key, data, customCacheTime)
126+
cacheStats.miss++
127+
return null
85128
}
86129

87130
export async function delCacheData(key) {
88-
if (!JSON.parse(BLOG.ENABLE_CACHE)) {
89-
return
131+
const chain = getCacheChain()
132+
133+
for (const { name, api } of chain) {
134+
try {
135+
await api.delCache(key)
136+
} catch (e) {
137+
console.warn(`[Cache] ${name} del failed key:${key}`, e.message)
138+
}
90139
}
91-
await getApi().delCache(key)
92140
}
93141

94-
/**
95-
* 缓存实现类
96-
* @returns
97-
*/
142+
function getCacheType() {
143+
if (hasRedis) return 'redis'
144+
if (isVercelEnv()) return 'vercel'
145+
if (isBuildPhase) return 'file'
146+
return 'memory'
147+
}
148+
98149
export function getApi() {
99-
if (BLOG.REDIS_URL) {
100-
return RedisCache
101-
} else if (process.env.ENABLE_FILE_CACHE) {
102-
return FileCache
103-
} else {
104-
return MemoryCache
150+
const type = getCacheType()
151+
152+
switch (type) {
153+
case 'redis':
154+
return RedisCache
155+
// case 'vercel':
156+
// VercelCache 目前不稳定(有大小限制),先注释掉
157+
// return VercelCache
158+
// 文件速度和内存消耗存疑
159+
case 'file':
160+
return FileCache
161+
default:
162+
return MemoryCache
163+
}
164+
}
165+
166+
function getCacheChain() {
167+
const chain = []
168+
169+
if (hasRedis) {
170+
chain.push({ name: 'redis', api: RedisCache })
105171
}
172+
173+
// if (isVercelEnv()) {
174+
// chain.push({ name: 'vercel', api: VercelCache })
175+
// }
176+
177+
if (isBuildPhase || !BLOG.isProd) {
178+
chain.push({ name: 'file', api: FileCache })
179+
}
180+
181+
// 永远兜底
182+
chain.push({ name: 'memory', api: MemoryCache })
183+
184+
return chain
106185
}
186+
187+
function printCacheSummary() {
188+
const hitRate = cacheStats.total
189+
? ((cacheStats.hit / cacheStats.total) * 100).toFixed(1)
190+
: 0
191+
192+
console.log('\n[Cache Summary]')
193+
console.log('Strategy:', getCacheChain().map(c => c.name).join(' → '))
194+
console.log(
195+
`Stats: HIT ${hitRate}% | MISS ${cacheStats.miss} | ERROR ${cacheStats.error} | total ${cacheStats.total}`
196+
)
197+
198+
console.log('[Per Store]')
199+
Object.entries(cacheStats.perStore).forEach(([name, stat]) => {
200+
console.log(` ${name}: hit=${stat.hit || 0}, set=${stat.set || 0}`)
201+
})
202+
203+
console.log('----------------------------------\n')
204+
}
205+
206+
// Node 进程结束时触发
207+
if (typeof process !== 'undefined') {
208+
process.on('exit', printCacheSummary)
209+
}

lib/cache/vercel_cache.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getCache } from '@vercel/functions'
2+
3+
const cache = getCache()
4+
5+
const VercelCache = {
6+
async getCache(key) {
7+
const data = await cache.get(key)
8+
return data || null
9+
},
10+
11+
async setCache(key, data, ttl = 3600) {
12+
await cache.set(key, data, {
13+
ttl,
14+
tags: ['notion']
15+
})
16+
},
17+
18+
async delCache(key) {
19+
// ⚠️ vercel runtime cache 不支持直接删除
20+
// 可以用 tag 失效(可扩展)
21+
console.warn('[VercelCache] delete not supported, use tag invalidation')
22+
}
23+
}
24+
25+
export default VercelCache

0 commit comments

Comments
 (0)