@@ -2,36 +2,43 @@ import BLOG from '@/blog.config'
22import FileCache from './local_file_cache'
33import MemoryCache from './memory_cache'
44import 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- */
3542export 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
87130export 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+
98149export 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+ }
0 commit comments