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 redis4. 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 router6. 前端 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:创建基础 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 集成的核心技能,能够构建高性能、高可用的全栈应用。