Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 173 additions & 70 deletions lib/cache/cache_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,45 @@ import BLOG from '@/blog.config'
import FileCache from './local_file_cache'
import MemoryCache from './memory_cache'
import RedisCache from './redis_cache'
import VercelCache from './vercel_cache'

// 配置是否开启Vercel环境中的缓存,因为Vercel中现有两种缓存方式在无服务环境下基本都是无意义的,纯粹的浪费资源
const enableCacheInVercel =
const cacheStats = {
hit: 0,
miss: 0,
set: 0,
error: 0,
total: 0,
perStore: {} // { redis: {hit, set}, memory: {...} }
}

const isBuildPhase =
process.env.npm_lifecycle_event === 'build' ||
process.env.npm_lifecycle_event === 'export' ||
!BLOG['isProd']

/**
* 尝试从缓存中获取数据,如果没有则尝试获取数据并写入缓存,最终返回所需数据
* @param key
* @param getDataFunction
* @param getDataArgs
* @returns {Promise<*|null>}
*/
export async function getOrSetDataWithCache(
key,
getDataFunction,
...getDataArgs
) {
process.env.npm_lifecycle_event === 'export'



const enableLocalCache = isBuildPhase || !BLOG['isProd']
const hasRedis = !!BLOG.REDIS_URL

const inflightMap = new Map()

const pid = process.pid

function isVercelEnv() {
return !!process.env.VERCEL
}

function cacheLog(action, key, extra = '') {
const type = getCacheType()
console.log(
`[Cache][${type.toUpperCase()}][pid:${process.pid}] ${action} key:${key} ${extra}`
)
}

export async function getOrSetDataWithCache(key, getDataFunction, ...getDataArgs) {
return getOrSetDataWithCustomCache(key, null, getDataFunction, ...getDataArgs)
}

/**
* 尝试从缓存中获取数据,如果没有则尝试获取数据并自定义写入缓存,最终返回所需数据
* @param key
* @param customCacheTime
* @param getDataFunction
* @param getDataArgs
* @returns {Promise<*|null>}
*/
export async function getOrSetDataWithCustomCache(
key,
customCacheTime,
Expand All @@ -40,67 +49,161 @@ export async function getOrSetDataWithCustomCache(
) {
const dataFromCache = await getDataFromCache(key)
if (dataFromCache) {
// console.log('[缓存-->>API]:', key) // 避免过多的缓存日志输出
// cacheLog('HIT', key)
return dataFromCache
}
const data = await getDataFunction(...getDataArgs)
if (data) {
// console.log('[API-->>缓存]:', key)
await setDataToCache(key, data, customCacheTime)

if (inflightMap.has(key)) {
// cacheLog('INFLIGHT-WAIT', key)
return inflightMap.get(key)
}
return data || null

// cacheLog('MISS', key, '缓存未命中,发起真实请求')

const promise = getDataFunction(...getDataArgs)
.then(async data => {
if (data) {
await setDataToCache(key, data, customCacheTime)
cacheLog('SET', key, '写入缓存成功')
}
inflightMap.delete(key)
return data || null
})
.catch(err => {
inflightMap.delete(key)
// cacheLog('ERROR', key, err.message)
throw err
})

inflightMap.set(key, promise)
return promise
}

/**
* 为减少频繁接口请求,notion数据将被缓存
* @param {*} key
* @returns
*/
export async function getDataFromCache(key, force) {
if (JSON.parse(BLOG.ENABLE_CACHE) || force) {
const dataFromCache = await getApi().getCache(key)
if (!dataFromCache || JSON.stringify(dataFromCache) === '[]') {
return null
export async function setDataToCache(key, data, customCacheTime) {
if (!data) return

const chain = getCacheChain()

for (const { name, api } of chain) {
try {
await api.setCache(key, data, customCacheTime)
// cacheLog('SET', key, `to:${name}`)

cacheStats.set++
cacheStats.perStore[name] = cacheStats.perStore[name] || { hit: 0, set: 0 }
cacheStats.perStore[name].set++

return
} catch (e) {
console.warn(`[Cache] ${name} set failed key:${key}`, e.message)
cacheStats.error++

}
// console.trace('[API-->>缓存]:', key, dataFromCache)
return dataFromCache
} else {
return null
}

console.warn(`[Cache] ALL set failed key:${key}`)
}

/**
* 写入缓存
* @param {*} key
* @param {*} data
* @param {*} customCacheTime
* @returns
*/
export async function setDataToCache(key, data, customCacheTime) {
if (!enableCacheInVercel || !data) {
return
export async function getDataFromCache(key, force) {
if (!JSON.parse(BLOG.ENABLE_CACHE) && !force) return null

const chain = getCacheChain()

for (const { name, api } of chain) {
try {
const data = await api.getCache(key)

if (data && JSON.stringify(data) !== '[]') {
// cacheLog('HIT', key, `from:${name}`)
cacheStats.hit++
cacheStats.perStore[name] = cacheStats.perStore[name] || { hit: 0, set: 0 }
cacheStats.perStore[name].hit++
return data
}
} catch (e) {
cacheStats.error++
console.warn(`[Cache] ${name} get failed key:${key}`, e.message)
}
}
// console.trace('[API-->>缓存写入]:', key)
await getApi().setCache(key, data, customCacheTime)
cacheStats.miss++
return null
}

export async function delCacheData(key) {
if (!JSON.parse(BLOG.ENABLE_CACHE)) {
return
const chain = getCacheChain()

for (const { name, api } of chain) {
try {
await api.delCache(key)
} catch (e) {
console.warn(`[Cache] ${name} del failed key:${key}`, e.message)
}
}
await getApi().delCache(key)
}

/**
* 缓存实现类
* @returns
*/
function getCacheType() {
if (hasRedis) return 'redis'
if (isVercelEnv()) return 'vercel'
if (isBuildPhase) return 'file'
return 'memory'
}

export function getApi() {
if (BLOG.REDIS_URL) {
return RedisCache
} else if (process.env.ENABLE_FILE_CACHE) {
return FileCache
} else {
return MemoryCache
const type = getCacheType()

switch (type) {
case 'redis':
return RedisCache
case 'vercel':
return VercelCache
case 'file':
return FileCache
default:
return MemoryCache
}
}

function getCacheChain() {
const chain = []

if (hasRedis) {
chain.push({ name: 'redis', api: RedisCache })
}

if (isVercelEnv()) {
chain.push({ name: 'vercel', api: VercelCache })
}

if (isBuildPhase || !BLOG.isProd) {
chain.push({ name: 'file', api: FileCache })
}

// 永远兜底
chain.push({ name: 'memory', api: MemoryCache })

return chain
}

function printCacheSummary() {
const hitRate = cacheStats.total
? ((cacheStats.hit / cacheStats.total) * 100).toFixed(1)
: 0

console.log('\n[Cache Summary]')
console.log('Strategy:', getCacheChain().map(c => c.name).join(' → '))
console.log(
`Stats: HIT ${hitRate}% | MISS ${cacheStats.miss} | ERROR ${cacheStats.error} | total ${cacheStats.total}`
)

console.log('[Per Store]')
Object.entries(cacheStats.perStore).forEach(([name, stat]) => {
console.log(` ${name}: hit=${stat.hit || 0}, set=${stat.set || 0}`)
})

console.log('----------------------------------\n')
}

// Node 进程结束时触发
if (typeof process !== 'undefined') {
process.on('exit', printCacheSummary)
}
25 changes: 25 additions & 0 deletions lib/cache/vercel_cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getCache } from '@vercel/functions'

const cache = getCache()

const VercelCache = {
async getCache(key) {
const data = await cache.get(key)
return data || null
},

async setCache(key, data, ttl = 3600) {
await cache.set(key, data, {
ttl,
tags: ['notion']
})
},

async delCache(key) {
// ⚠️ vercel runtime cache 不支持直接删除
// 可以用 tag 失效(可扩展)
console.warn('[VercelCache] delete not supported, use tag invalidation')
}
}

export default VercelCache
Loading
Loading