GraphQL 缓存策略有哪些实现方式
GraphQL 缓存策略与实现GraphQL 的缓存机制对于提高性能、减少服务器负载和改善用户体验至关重要。以下是 GraphQL 缓存的各种策略和实现方法。1. 客户端缓存Apollo Client 缓存import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({ uri: 'https://api.example.com/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: ['filter'], merge(existing, incoming) { return incoming; } } } }, Post: { keyFields: ['id', 'slug'] } } })});缓存策略配置const cache = new InMemoryCache({ typePolicies: { Query: { fields: { // 缓存单个结果 user: { read(_, { args, toReference }) { return toReference({ __typename: 'User', id: args.id }); } }, // 缓存列表 posts: { keyArgs: ['filter', 'sort'], merge(existing = [], incoming) { return [...existing, ...incoming]; } }, // 缓存分页数据 paginatedPosts: { keyArgs: false, merge(existing = { edges: [] }, incoming) { return { ...incoming, edges: [...existing.edges, ...incoming.edges] }; } } } } }});2. 服务器端缓存Redis 缓存实现const Redis = require('ioredis');const redis = new Redis();async function cachedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; try { // 尝试从缓存获取 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 执行实际查询 const result = await fetchData(args); // 缓存结果(5 分钟过期) await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } catch (error) { console.error('Cache error:', error); // 缓存失败时直接查询 return await fetchData(args); }}const resolvers = { Query: { user: cachedResolver, posts: cachedResolver, post: cachedResolver }};Memcached 缓存const Memcached = require('memcached');const memcached = new Memcached('localhost:11211');async function memcachedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; return new Promise((resolve, reject) => { memcached.get(cacheKey, async (err, data) => { if (err) { console.error('Memcached error:', err); return resolve(await fetchData(args)); } if (data) { return resolve(JSON.parse(data)); } // 执行查询并缓存 const result = await fetchData(args); memcached.set(cacheKey, JSON.stringify(result), 300, (err) => { if (err) console.error('Memcached set error:', err); }); resolve(result); }); });}3. 缓存失效策略基于时间的失效const TTL = { SHORT: 60, // 1 分钟 MEDIUM: 300, // 5 分钟 LONG: 3600, // 1 小时 VERY_LONG: 86400 // 24 小时};async function timeBasedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const ttl = getTTL(info.fieldName); const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const result = await fetchData(args); await redis.setex(cacheKey, ttl, JSON.stringify(result)); return result;}function getTTL(fieldName) { const ttlMap = { 'user': TTL.MEDIUM, 'posts': TTL.SHORT, 'post': TTL.LONG, 'comments': TTL.SHORT }; return ttlMap[fieldName] || TTL.MEDIUM;}基于事件的失效const eventBus = new EventEmitter();// 监听数据变更事件eventBus.on('user.updated', async (userId) => { const pattern = `graphql:user:*${userId}*`; const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(keys); }});eventBus.on('post.created', async () => { const pattern = 'graphql:posts*'; const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(keys); }});// 在 Mutation 中触发事件const resolvers = { Mutation: { updateUser: async (_, { id, input }) => { const user = await User.update(id, input); eventBus.emit('user.updated', id); return user; }, createPost: async (_, { input }) => { const post = await Post.create(input); eventBus.emit('post.created'); return post; } }};4. 缓存预热预加载热门数据async function warmupCache() { console.log('Warming up cache...'); // 预加载热门用户 const popularUsers = await User.findPopular(100); for (const user of popularUsers) { const cacheKey = `graphql:user:${JSON.stringify({ id: user.id })}`; await redis.setex(cacheKey, 3600, JSON.stringify(user)); } // 预加载最新帖子 const latestPosts = await Post.findLatest(50); const cacheKey = `graphql:posts:${JSON.stringify({ limit: 50 })}`; await redis.setex(cacheKey, 300, JSON.stringify(latestPosts)); console.log('Cache warmed up successfully');}// 在应用启动时执行warmupCache();5. 缓存穿透保护布隆过滤器const { BloomFilter } = require('bloom-filters');// 创建布隆过滤器const userBloomFilter = new BloomFilter(1000000, 0.01);// 初始化时填充布隆过滤器async function initBloomFilter() { const userIds = await User.getAllIds(); userIds.forEach(id => userBloomFilter.add(id));}async function protectedResolver(parent, args, context, info) { const { id } = args; // 检查 ID 是否可能存在 if (!userBloomFilter.has(id)) { // ID 肯定不存在,直接返回 null return null; } // 可能存在,查询缓存或数据库 const cacheKey = `graphql:user:${JSON.stringify(args)}`; const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const user = await User.findById(id); if (user) { await redis.setex(cacheKey, 300, JSON.stringify(user)); } else { // 缓存空值,防止缓存穿透 await redis.setex(cacheKey, 60, JSON.stringify(null)); } return user;}6. 缓存雪崩保护随机过期时间function getRandomTTL(baseTTL, variance = 0.2) { const randomFactor = 1 + (Math.random() * variance * 2 - variance); return Math.floor(baseTTL * randomFactor);}async function avalancheProtectedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const baseTTL = getTTL(info.fieldName); const ttl = getRandomTTL(baseTTL); const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } const result = await fetchData(args); await redis.setex(cacheKey, ttl, JSON.stringify(result)); return result;}互斥锁async function lockedResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; const lockKey = `lock:${cacheKey}`; // 尝试获取缓存 const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // 尝试获取锁 const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10); if (lock) { try { // 获取锁成功,执行查询 const result = await fetchData(args); await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } finally { // 释放锁 await redis.del(lockKey); } } else { // 获取锁失败,等待并重试 await new Promise(resolve => setTimeout(resolve, 100)); return lockedResolver(parent, args, context, info); }}7. CDN 缓存使用 persisted queriesimport { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';import { sha256 } from 'crypto-hash';const persistedQueryLink = createPersistedQueryLink({ sha256, useGETForHashedQueries: true});const client = new ApolloClient({ link: persistedQueryLink.concat(httpLink), cache: new InMemoryCache()});配置 CDNconst server = new ApolloServer({ typeDefs, resolvers, cacheControl: { defaultMaxAge: 60, stripFormattedExtensions: false, calculateHttpHeaders: true }, plugins: [ require('apollo-cache-control')({ defaultMaxAge: 60 }) ]});8. 缓存监控缓存命中率监控const cacheMetrics = { hits: 0, misses: 0, errors: 0};async function monitoredResolver(parent, args, context, info) { const cacheKey = `graphql:${info.fieldName}:${JSON.stringify(args)}`; try { const cached = await redis.get(cacheKey); if (cached) { cacheMetrics.hits++; return JSON.parse(cached); } cacheMetrics.misses++; const result = await fetchData(args); await redis.setex(cacheKey, 300, JSON.stringify(result)); return result; } catch (error) { cacheMetrics.errors++; throw error; }}// 定期报告缓存指标setInterval(() => { const total = cacheMetrics.hits + cacheMetrics.misses; const hitRate = total > 0 ? cacheMetrics.hits / total : 0; console.log('Cache Metrics:', { hits: cacheMetrics.hits, misses: cacheMetrics.misses, errors: cacheMetrics.errors, hitRate: `${(hitRate * 100).toFixed(2)}%` });}, 60000);9. 缓存策略总结| 策略 | 适用场景 | 优势 | 劣势 ||------|----------|------|------|| 客户端缓存 | 重复查询相同数据 | 减少网络请求 | 占用客户端内存 || 服务器端缓存 | 高频查询 | 减少数据库负载 | 需要维护缓存一致性 || 基于时间失效 | 数据变化不频繁 | 实现简单 | 可能返回过期数据 || 基于事件失效 | 数据变化频繁 | 数据实时性高 | 实现复杂 || 缓存预热 | 热门数据 | 提升首次访问性能 | 需要识别热门数据 || CDN 缓存 | 静态数据 | 减少服务器负载 | 不适合动态数据 |10. 缓存最佳实践[ ] 根据数据特性选择合适的缓存策略[ ] 设置合理的缓存过期时间[ ] 实现缓存失效机制[ ] 监控缓存命中率[ ] 防止缓存穿透、雪崩、击穿[ ] 使用缓存预热提升性能[ ] 考虑使用 CDN 加速静态数据[ ] 定期清理无效缓存[ ] 实现缓存降级机制[ ] 记录缓存操作日志