165 lines
3.7 KiB
Plaintext
165 lines
3.7 KiB
Plaintext
/**
|
||
* Redis实现示例(生产环境推荐)
|
||
*
|
||
* 使用方法:
|
||
* 1. 安装依赖:npm install ioredis
|
||
* 2. 配置环境变量:REDIS_URL=redis://localhost:6379
|
||
* 3. 在upload-sessions.ts中导入此模块
|
||
*
|
||
* 优势:
|
||
* - 数据持久化(可配置RDB/AOF)
|
||
* - 支持多实例部署
|
||
* - 自动过期(TTL)
|
||
* - 高可用性(支持主从复制、集群)
|
||
* - 跨服务共享
|
||
*/
|
||
|
||
import Redis from 'ioredis'
|
||
import type { UploadSession } from './upload-sessions'
|
||
|
||
// 初始化Redis连接
|
||
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
|
||
retryStrategy: (times) => {
|
||
const delay = Math.min(times * 50, 2000)
|
||
return delay
|
||
},
|
||
maxRetriesPerRequest: 3,
|
||
})
|
||
|
||
// Redis键前缀
|
||
const KEY_PREFIX = 'upload:session:'
|
||
const EXPIRE_TIME = 24 * 60 * 60 // 24小时(秒)
|
||
|
||
/**
|
||
* 保存上传会话
|
||
*/
|
||
export async function saveUploadSession(
|
||
uploadId: string,
|
||
session: UploadSession
|
||
): Promise<void> {
|
||
const key = `${KEY_PREFIX}${uploadId}`
|
||
|
||
// 将Map转换为数组以便序列化
|
||
const data = {
|
||
...session,
|
||
chunks: Array.from(session.chunks.entries()),
|
||
}
|
||
|
||
// 使用SETEX设置键值并指定过期时间
|
||
await redis.setex(key, EXPIRE_TIME, JSON.stringify(data))
|
||
|
||
console.log(`[Redis] 保存上传会话: ${uploadId}, 过期时间: ${EXPIRE_TIME}秒`)
|
||
}
|
||
|
||
/**
|
||
* 获取上传会话
|
||
*/
|
||
export async function getUploadSession(
|
||
uploadId: string
|
||
): Promise<UploadSession | null> {
|
||
const key = `${KEY_PREFIX}${uploadId}`
|
||
const data = await redis.get(key)
|
||
|
||
if (!data) {
|
||
return null
|
||
}
|
||
|
||
const session = JSON.parse(data)
|
||
|
||
// 将数组转换回Map
|
||
return {
|
||
...session,
|
||
chunks: new Map(session.chunks),
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除上传会话
|
||
*/
|
||
export async function deleteUploadSession(uploadId: string): Promise<void> {
|
||
const key = `${KEY_PREFIX}${uploadId}`
|
||
await redis.del(key)
|
||
console.log(`[Redis] 删除上传会话: ${uploadId}`)
|
||
}
|
||
|
||
/**
|
||
* 更新块信息
|
||
*/
|
||
export async function updateChunk(
|
||
uploadId: string,
|
||
chunkIndex: number,
|
||
chunkPath: string
|
||
): Promise<void> {
|
||
const session = await getUploadSession(uploadId)
|
||
if (!session) {
|
||
throw new Error(`上传会话不存在: ${uploadId}`)
|
||
}
|
||
|
||
session.chunks.set(chunkIndex, chunkPath)
|
||
await saveUploadSession(uploadId, session)
|
||
|
||
console.log(`[Redis] 更新块信息: ${uploadId}, 块 ${chunkIndex}`)
|
||
}
|
||
|
||
/**
|
||
* 检查会话是否存在
|
||
*/
|
||
export async function hasUploadSession(uploadId: string): Promise<boolean> {
|
||
const key = `${KEY_PREFIX}${uploadId}`
|
||
const exists = await redis.exists(key)
|
||
return exists === 1
|
||
}
|
||
|
||
/**
|
||
* 获取会话的剩余过期时间
|
||
*/
|
||
export async function getSessionTTL(uploadId: string): Promise<number> {
|
||
const key = `${KEY_PREFIX}${uploadId}`
|
||
const ttl = await redis.ttl(key)
|
||
return ttl
|
||
}
|
||
|
||
/**
|
||
* 获取所有活跃会话数量
|
||
*/
|
||
export async function getActiveSessionsCount(): Promise<number> {
|
||
const keys = await redis.keys(`${KEY_PREFIX}*`)
|
||
return keys.length
|
||
}
|
||
|
||
/**
|
||
* 清理所有过期会话(Redis会自动清理,此函数用于统计)
|
||
*/
|
||
export async function cleanupExpiredSessions(): Promise<number> {
|
||
// Redis使用TTL自动清理,这里只是统计
|
||
const keys = await redis.keys(`${KEY_PREFIX}*`)
|
||
let expiredCount = 0
|
||
|
||
for (const key of keys) {
|
||
const ttl = await redis.ttl(key)
|
||
if (ttl === -2) {
|
||
// 键已过期但未删除(理论上不会发生)
|
||
expiredCount++
|
||
}
|
||
}
|
||
|
||
return expiredCount
|
||
}
|
||
|
||
// 错误处理
|
||
redis.on('error', (err) => {
|
||
console.error('[Redis] 连接错误:', err)
|
||
})
|
||
|
||
redis.on('connect', () => {
|
||
console.log('[Redis] 连接成功')
|
||
})
|
||
|
||
redis.on('ready', () => {
|
||
console.log('[Redis] 准备就绪')
|
||
})
|
||
|
||
// 导出Redis实例(用于其他操作)
|
||
export { redis }
|
||
|