Vue 3 与 Redis 缓存集成

概述

Redis 是一个高性能的开源内存数据库,常用于缓存、会话存储、消息队列等场景。与传统数据库相比,Redis 具有更高的读写速度、支持多种数据结构、内置丰富的功能等优势。在全栈应用中,使用 Redis 可以显著提高应用性能,减少数据库压力,提升用户体验。Vue 3 与 Redis 的结合可以让开发者在前端享受快速的数据响应,同时在后端优化数据库查询。本集将详细介绍如何使用 Vue 3 和 Redis 构建高性能的全栈应用,包括 Redis 配置、缓存策略设计和前端集成。

核心知识点

1. Redis 基础概念

  • 内存数据库:数据存储在内存中,读写速度快
  • 数据结构:支持字符串、哈希、列表、集合、有序集合等多种数据结构
  • 持久化:支持 RDB 和 AOF 两种持久化方式
  • 高可用:支持主从复制、哨兵模式、集群模式
  • 原子操作:支持事务和 Lua 脚本
  • 发布/订阅:支持实时消息推送

2. Redis 缓存策略

  • 缓存穿透:查询不存在的数据,解决方案:布隆过滤器、缓存空值
  • 缓存雪崩:大量缓存同时失效,解决方案:设置随机过期时间、多级缓存
  • 缓存击穿:热点数据缓存失效,解决方案:互斥锁、逻辑过期
  • 缓存更新:手动更新、过期时间、发布/订阅
  • 缓存一致性:最终一致性、强一致性

3. 项目初始化

创建后端项目

# 创建后端目录
mkdir my-redis-backend
cd my-redis-backend

# 初始化 Node.js 项目
npm init -y

# 安装依赖
npm install express cors redis ioredis @prisma/client
npm install -D prisma typescript ts-node @types/node @types/express

# 初始化 Prisma
npx prisma init

配置 Redis 连接

// src/config/redis.ts
import Redis from 'ioredis'

// 创建 Redis 客户端
const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  db: 0,
  retryStrategy: (times) => {
    return Math.min(times * 50, 2000)
  }
})

// 测试连接
redis.ping((err, result) => {
  if (err) {
    console.error('Redis 连接失败:', err)
  } else {
    console.log('Redis 连接成功:', result)
  }
})

export default redis

4. Redis 缓存实现

缓存中间件

// src/middleware/cache.ts
import { Request, Response, NextFunction } from 'express'
import redis from '../config/redis'

// 缓存中间件
export const cache = (duration: number = 3600) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const key = `cache:${req.originalUrl}`
    
    try {
      // 尝试从缓存获取数据
      const cachedData = await redis.get(key)
      
      if (cachedData) {
        console.log(`Cache hit for ${key}`)
        return res.json(JSON.parse(cachedData))
      }
      
      // 缓存未命中,继续执行
      console.log(`Cache miss for ${key}`)
      
      // 重写 res.json 方法,在发送响应时缓存数据
      const originalJson = res.json
      res.json = function(data) {
        // 缓存数据
        redis.setex(key, duration, JSON.stringify(data))
        return originalJson.call(this, data)
      }
      
      next()
    } catch (error) {
      console.error('缓存中间件错误:', error)
      next() // 缓存错误时继续执行
    }
  }
}

手动缓存管理

// src/services/cacheService.ts
import redis from '../config/redis'

class CacheService {
  // 设置缓存
  async set(key: string, value: any, expireTime: number = 3600): Promise<void> {
    await redis.setex(key, expireTime, JSON.stringify(value))
  }
  
  // 获取缓存
  async get(key: string): Promise<any | null> {
    const value = await redis.get(key)
    return value ? JSON.parse(value) : null
  }
  
  // 删除缓存
  async del(key: string): Promise<void> {
    await redis.del(key)
  }
  
  // 清空缓存
  async clear(pattern: string): Promise<void> {
    const keys = await redis.keys(pattern)
    if (keys.length > 0) {
      await redis.del(...keys)
    }
  }
  
  // 缓存标签
  async addToTag(tag: string, key: string): Promise<void> {
    await redis.sadd(`tag:${tag}`, key)
  }
  
  // 清除标签下的所有缓存
  async clearByTag(tag: string): Promise<void> {
    const keys = await redis.smembers(`tag:${tag}`)
    if (keys.length > 0) {
      await redis.del(...keys)
      await redis.del(`tag:${tag}`)
    }
  }
}

export default new CacheService()

5. 与 Prisma 集成

创建数据访问层

// src/services/postService.ts
import { prisma } from '../prisma'
import cacheService from './cacheService'

class PostService {
  // 获取所有帖子
  async getAllPosts() {
    const cacheKey = 'posts:all'
    const cacheTag = 'posts'
    
    // 尝试从缓存获取
    const cachedPosts = await cacheService.get(cacheKey)
    if (cachedPosts) {
      return cachedPosts
    }
    
    // 从数据库获取
    const posts = await prisma.post.findMany({
      include: { author: { select: { id: true, name: true, email: true } } }
    })
    
    // 缓存数据
    await cacheService.set(cacheKey, posts, 3600)
    await cacheService.addToTag(cacheTag, cacheKey)
    
    return posts
  }
  
  // 获取单个帖子
  async getPostById(id: number) {
    const cacheKey = `post:${id}`
    const cacheTag = 'posts'
    
    // 尝试从缓存获取
    const cachedPost = await cacheService.get(cacheKey)
    if (cachedPost) {
      return cachedPost
    }
    
    // 从数据库获取
    const post = await prisma.post.findUnique({
      where: { id },
      include: { author: { select: { id: true, name: true, email: true } } }
    })
    
    if (post) {
      // 缓存数据
      await cacheService.set(cacheKey, post, 3600)
      await cacheService.addToTag(cacheTag, cacheKey)
    }
    
    return post
  }
  
  // 创建帖子
  async createPost(data: any, authorId: number) {
    const post = await prisma.post.create({
      data: {
        ...data,
        author: { connect: { id: authorId } }
      },
      include: { author: { select: { id: true, name: true } } }
    })
    
    // 清除帖子相关缓存
    await cacheService.clearByTag('posts')
    
    return post
  }
  
  // 更新帖子
  async updatePost(id: number, data: any, authorId: number) {
    const post = await prisma.post.update({
      where: { id, authorId },
      data,
      include: { author: { select: { id: true, name: true } } }
    })
    
    // 清除帖子相关缓存
    await cacheService.clearByTag('posts')
    
    return post
  }
  
  // 删除帖子
  async deletePost(id: number, authorId: number) {
    await prisma.post.delete({ where: { id, authorId } })
    
    // 清除帖子相关缓存
    await cacheService.clearByTag('posts')
    
    return { message: 'Post deleted' }
  }
}

export default new PostService()

实现 API 路由

// src/routes/posts.ts
import express from 'express'
import { auth } from '../middleware/auth'
import postService from '../services/postService'

const router = express.Router()

// 获取所有帖子(使用缓存)
router.get('/', async (req, res) => {
  const posts = await postService.getAllPosts()
  res.json(posts)
})

// 获取单个帖子(使用缓存)
router.get('/:id', async (req, res) => {
  const { id } = req.params
  const post = await postService.getPostById(parseInt(id))
  if (!post) {
    return res.status(404).json({ message: 'Post not found' })
  }
  res.json(post)
})

// 创建帖子
router.post('/', auth, async (req: any, res) => {
  const post = await postService.createPost(req.body, req.user.id)
  res.status(201).json(post)
})

// 更新帖子
router.put('/:id', auth, async (req: any, res) => {
  const { id } = req.params
  try {
    const post = await postService.updatePost(parseInt(id), req.body, req.user.id)
    res.json(post)
  } catch (error: any) {
    res.status(404).json({ message: 'Post not found or not authorized' })
  }
})

// 删除帖子
router.delete('/:id', auth, async (req: any, res) => {
  const { id } = req.params
  try {
    const result = await postService.deletePost(parseInt(id), req.user.id)
    res.json(result)
  } catch (error: any) {
    res.status(404).json({ message: 'Post not found or not authorized' })
  }
})

export default router

6. 前端 Vue 3 应用集成

创建 Vue 3 项目

# 创建前端目录
mkdir my-redis-frontend
cd my-redis-frontend

# 使用 Vite 创建 Vue 3 项目
yarn create vite . -- --template vue-ts

安装依赖

# 安装 Axios 和其他依赖
yarn add axios pinia vue-router

配置 Axios 实例

// src/utils/axios.ts
import axios from 'axios'

const apiClient = axios.create({
  baseURL: 'http://localhost:3000/api',
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
apiClient.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

export default apiClient

创建 Pinia Store

// src/stores/post.ts
import { defineStore } from 'pinia'
import apiClient from '../utils/axios'

export interface Post {
  id: number
  title: string
  content: string
  published: boolean
  author: {
    id: number
    name: string
    email?: string
  }
  createdAt: string
  updatedAt: string
}

export const usePostStore = defineStore('post', {
  state: () => ({
    posts: [] as Post[],
    post: null as Post | null,
    loading: false,
    error: null as string | null
  }),
  
  actions: {
    async fetchPosts() {
      this.loading = true
      this.error = null
      try {
        const response = await apiClient.get('/posts')
        this.posts = response.data
      } catch (error: any) {
        this.error = error.message
        console.error('Error fetching posts:', error)
      } finally {
        this.loading = false
      }
    },
    
    async fetchPost(id: number) {
      this.loading = true
      this.error = null
      try {
        const response = await apiClient.get(`/posts/${id}`)
        this.post = response.data
      } catch (error: any) {
        this.error = error.message
        console.error('Error fetching post:', error)
      } finally {
        this.loading = false
      }
    },
    
    async createPost(data: { title: string; content: string; published?: boolean }) {
      this.loading = true
      this.error = null
      try {
        const response = await apiClient.post('/posts', data)
        this.posts.push(response.data)
        return response.data
      } catch (error: any) {
        this.error = error.message
        console.error('Error creating post:', error)
        throw error
      } finally {
        this.loading = false
      }
    }
  }
})

创建 Vue 组件

<template>
  <div class="posts-container">
    <h1>帖子列表</h1>
    
    <div v-if="loading" class="loading">加载中...</div>
    
    <div v-else-if="error" class="error">{{ error }}</div>
    
    <div v-else class="posts-list">
      <div v-for="post in posts" :key="post.id" class="post">
        <h2>{{ post.title }}</h2>
        <p class="post-meta">
          作者: {{ post.author.name }} | 
          {{ new Date(post.createdAt).toLocaleString() }}
        </p>
        <p>{{ post.content }}</p>
        <div class="post-status">
          <span v-if="post.published" class="published">已发布</span>
          <span v-else class="draft">草稿</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { usePostStore } from '../stores/post'

const postStore = usePostStore()
const { posts, loading, error, fetchPosts } = postStore

onMounted(() => {
  fetchPosts()
})
</script>

<style scoped>
.posts-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.loading {
  text-align: center;
  color: #42b883;
}

.error {
  text-align: center;
  color: #ff4d4f;
}

.posts-list {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.post {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 20px;
  background-color: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.post h2 {
  margin-top: 0;
  color: #333;
}

.post-meta {
  color: #666;
  font-size: 14px;
  margin-bottom: 15px;
}

.post-status {
  margin: 15px 0;
}

.published {
  color: #52c41a;
  font-weight: bold;
}

.draft {
  color: #faad14;
  font-weight: bold;
}
</style>

7. Redis 高级功能

分布式锁

// src/utils/lock.ts
import redis from '../config/redis'

class RedisLock {
  // 获取锁
  async acquire(key: string, expiration: number = 10000): Promise<boolean> {
    const lockKey = `lock:${key}`
    const result = await redis.set(lockKey, '1', 'PX', expiration, 'NX')
    return result === 'OK'
  }
  
  // 释放锁
  async release(key: string): Promise<void> {
    const lockKey = `lock:${key}`
    await redis.del(lockKey)
  }
  
  // 带重试的获取锁
  async acquireWithRetry(key: string, retries: number = 5, delay: number = 1000): Promise<boolean> {
    for (let i = 0; i < retries; i++) {
      const acquired = await this.acquire(key)
      if (acquired) {
        return true
      }
      await new Promise(resolve => setTimeout(resolve, delay))
    }
    return false
  }
}

export default new RedisLock()

计数器

// src/services/counterService.ts
import redis from '../config/redis'

class CounterService {
  // 增加计数
  async increment(key: string, increment: number = 1): Promise<number> {
    return await redis.incrby(key, increment)
  }
  
  // 减少计数
  async decrement(key: string, decrement: number = 1): Promise<number> {
    return await redis.decrby(key, decrement)
  }
  
  // 获取计数
  async get(key: string): Promise<number> {
    const value = await redis.get(key)
    return value ? parseInt(value) : 0
  }
  
  // 设置计数
  async set(key: string, value: number): Promise<void> {
    await redis.set(key, value)
  }
  
  // 计数过期
  async expire(key: string, seconds: number): Promise<void> {
    await redis.expire(key, seconds)
  }
}

export default new CounterService()

排行榜

// src/services/rankingService.ts
import redis from '../config/redis'

class RankingService {
  // 添加分数
  async addScore(key: string, member: string, score: number): Promise<void> {
    await redis.zadd(key, score, member)
  }
  
  // 获取排行榜(从高到低)
  async getRanking(key: string, start: number = 0, stop: number = -1): Promise<any[]> {
    return await redis.zrevrange(key, start, stop, 'WITHSCORES')
  }
  
  // 获取用户排名
  async getRank(key: string, member: string): Promise<number> {
    const rank = await redis.zrevrank(key, member)
    return rank !== null ? rank + 1 : 0
  }
  
  // 获取用户分数
  async getScore(key: string, member: string): Promise<number> {
    const score = await redis.zscore(key, member)
    return score !== null ? parseFloat(score) : 0
  }
  
  // 获取排行榜长度
  async getCount(key: string): Promise<number> {
    return await redis.zcard(key)
  }
}

export default new RankingService()

8. 性能监控

监控 Redis 命令

// src/middleware/redis-monitor.ts
import redis from '../config/redis'

// 监控 Redis 命令
redis.monitor((err, monitor) => {
  if (err) {
    console.error('Redis 监控失败:', err)
    return
  }
  
  monitor.on('monitor', (time, args, source, database) => {
    console.log(`${time}: ${args.join(' ')} (db: ${database})`)
  })
})

统计缓存命中率

// src/services/metricsService.ts
import redis from '../config/redis'

class MetricsService {
  private hits = 0
  private misses = 0
  
  // 记录缓存命中
  hit(): void {
    this.hits++
    redis.incr('metrics:cache:hits')
  }
  
  // 记录缓存未命中
  miss(): void {
    this.misses++
    redis.incr('metrics:cache:misses')
  }
  
  // 获取缓存命中率
  getHitRate(): number {
    const total = this.hits + this.misses
    return total > 0 ? (this.hits / total) * 100 : 0
  }
  
  // 重置统计
  reset(): void {
    this.hits = 0
    this.misses = 0
    redis.del('metrics:cache:hits', 'metrics:cache:misses')
  }
}

export default new MetricsService()

最佳实践

1. 缓存策略设计

  • 选择合适的缓存粒度:根据业务需求选择缓存整个页面、单个对象或部分数据
  • 设置合理的过期时间:根据数据更新频率设置过期时间,热点数据可设置较短过期时间
  • 使用缓存标签:便于批量清除相关缓存
  • 实现多级缓存:内存缓存 + Redis 缓存,提高读取速度
  • 监控缓存命中率:根据命中率调整缓存策略

2. 性能优化

  • 使用连接池:避免频繁创建和关闭 Redis 连接
  • 批量操作:使用 Redis 批量命令减少网络往返
  • 管道操作:将多个命令打包发送,减少网络延迟
  • 数据压缩:对大型数据进行压缩后存储
  • 合理使用数据结构:根据业务需求选择合适的数据结构

3. 安全性

  • 设置密码:为 Redis 服务设置强密码
  • 限制访问 IP:通过防火墙限制只有应用服务器可以访问 Redis
  • 禁用危险命令:在 Redis 配置中禁用 FLUSHDB、FLUSHALL 等危险命令
  • 使用 SSL/TLS:加密 Redis 通信
  • 定期备份:定期备份 Redis 数据

4. 高可用设计

  • 主从复制:配置 Redis 主从复制,提高读性能和可用性
  • 哨兵模式:实现自动故障转移
  • 集群模式:分布式部署,提高扩展性和可用性
  • 持久化配置:开启 RDB 和 AOF 持久化,防止数据丢失
  • 监控和告警:监控 Redis 运行状态,设置告警阈值

5. 开发工作流

  • 本地开发:使用 Docker 运行 Redis 实例
  • 测试环境:使用独立的 Redis 实例
  • 生产环境:使用高可用 Redis 集群
  • 日志记录:记录 Redis 操作日志,便于调试和监控
  • 自动化部署:使用 CI/CD 流程部署 Redis 配置

常见问题与解决方案

1. 问题:Redis 连接超时

解决方案

  • 检查 Redis 服务器是否正在运行
  • 检查网络连接是否正常
  • 检查 Redis 配置中的 bind 和 port 设置
  • 调整 Redis 客户端的超时设置和重试策略

2. 问题:缓存一致性问题

解决方案

  • 使用最终一致性策略,设置合理的过期时间
  • 数据更新时主动清除相关缓存
  • 使用发布/订阅机制,通知其他服务更新缓存
  • 实现缓存版本控制

3. 问题:缓存内存占用过高

解决方案

  • 设置合理的过期时间
  • 使用 LRU 或 LFU 淘汰策略
  • 限制 Redis 最大内存使用
  • 定期清理无用缓存
  • 优化缓存数据结构,减少内存占用

4. 问题:缓存穿透

解决方案

  • 实现布隆过滤器,过滤不存在的数据
  • 缓存空值,设置较短的过期时间
  • 对请求参数进行验证和限制
  • 使用限流机制,防止恶意请求

5. 问题:缓存雪崩

解决方案

  • 设置随机过期时间,避免大量缓存同时失效
  • 实现多级缓存,减少单一缓存依赖
  • 预热缓存,提前加载热点数据
  • 使用熔断机制,防止数据库压力过大

6. 问题:Redis 性能下降

解决方案

  • 分析 Redis 命令执行时间,优化慢查询
  • 增加 Redis 节点,提高集群性能
  • 优化数据结构和命令使用
  • 监控系统资源使用情况,增加硬件资源

进一步学习资源

  1. 官方文档

  2. 学习资源

  3. 工具和库

  4. 社区资源

课后练习

练习 1:创建基础 Redis 应用

  • 初始化 Node.js 项目
  • 安装 Redis 和 Express
  • 实现 Redis 连接配置
  • 实现简单的缓存中间件
  • 测试缓存功能

练习 2:与数据库集成

  • 安装 Prisma 并配置数据库
  • 定义数据模型
  • 实现数据访问层,集成 Redis 缓存
  • 实现 API 路由
  • 测试缓存效果

练习 3:实现缓存策略

  • 实现缓存穿透解决方案
  • 实现缓存雪崩解决方案
  • 实现缓存击穿解决方案
  • 测试各种缓存策略

练习 4:高级 Redis 功能

  • 实现分布式锁
  • 实现计数器功能
  • 实现排行榜功能
  • 测试高级功能

练习 5:前端集成

  • 创建 Vue 3 项目
  • 配置 Axios 和 Pinia
  • 实现帖子列表和详情页面
  • 测试前端与后端的交互

练习 6:性能监控

  • 实现缓存命中率统计
  • 实现 Redis 命令监控
  • 实现性能指标收集
  • 测试监控效果

练习 7:高可用配置

  • 使用 Docker 部署 Redis 主从复制
  • 配置 Redis 哨兵模式
  • 测试故障转移功能
  • 实现 Redis 持久化

通过以上练习,你将掌握 Vue 3 与 Redis 集成的核心技能,能够构建高性能、高可用的全栈应用。

« 上一篇 Vue 3与GraphQL Yoga后端 - 高性能GraphQL全栈解决方案 下一篇 » Vue 3与WebSockets高级应用 - 实时通信全栈解决方案