mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-05-17 11:07:30 +08:00
使用 hash 优化用户信息获取速度
This commit is contained in:
@@ -151,8 +151,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
}
|
||||
|
||||
// ---------- 播放记录 ----------
|
||||
private prKey(user: string, key: string) {
|
||||
return `u:${user}:pr:${key}`; // u:username:pr:source+id
|
||||
private prHashKey(user: string) {
|
||||
return `u:${user}:pr`; // 一个用户的所有播放记录存在一个 Hash 中
|
||||
}
|
||||
|
||||
async getPlayRecord(
|
||||
@@ -160,7 +160,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
key: string
|
||||
): Promise<PlayRecord | null> {
|
||||
const val = await this.withRetry(() =>
|
||||
this.client.get(this.prKey(userName, key))
|
||||
this.client.hGet(this.prHashKey(userName), key)
|
||||
);
|
||||
return val ? (JSON.parse(val) as PlayRecord) : null;
|
||||
}
|
||||
@@ -171,42 +171,43 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
record: PlayRecord
|
||||
): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.set(this.prKey(userName, key), JSON.stringify(record))
|
||||
this.client.hSet(this.prHashKey(userName), key, JSON.stringify(record))
|
||||
);
|
||||
}
|
||||
|
||||
async getAllPlayRecords(
|
||||
userName: string
|
||||
): Promise<Record<string, PlayRecord>> {
|
||||
const pattern = `u:${userName}:pr:*`;
|
||||
const keys: string[] = await this.withRetry(() => this.client.keys(pattern));
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.withRetry(() => this.client.mGet(keys));
|
||||
const all = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.prHashKey(userName))
|
||||
);
|
||||
const result: Record<string, PlayRecord> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
for (const [field, raw] of Object.entries(all)) {
|
||||
if (raw) {
|
||||
const rec = JSON.parse(raw) as PlayRecord;
|
||||
// 截取 source+id 部分
|
||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
||||
result[keyPart] = rec;
|
||||
result[field] = JSON.parse(raw) as PlayRecord;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async deletePlayRecord(userName: string, key: string): Promise<void> {
|
||||
await this.withRetry(() => this.client.del(this.prKey(userName, key)));
|
||||
await this.withRetry(() =>
|
||||
this.client.hDel(this.prHashKey(userName), key)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteAllPlayRecords(userName: string): Promise<void> {
|
||||
await this.withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||
}
|
||||
|
||||
// ---------- 收藏 ----------
|
||||
private favKey(user: string, key: string) {
|
||||
return `u:${user}:fav:${key}`;
|
||||
private favHashKey(user: string) {
|
||||
return `u:${user}:fav`; // 一个用户的所有收藏存在一个 Hash 中
|
||||
}
|
||||
|
||||
async getFavorite(userName: string, key: string): Promise<Favorite | null> {
|
||||
const val = await this.withRetry(() =>
|
||||
this.client.get(this.favKey(userName, key))
|
||||
this.client.hGet(this.favHashKey(userName), key)
|
||||
);
|
||||
return val ? (JSON.parse(val) as Favorite) : null;
|
||||
}
|
||||
@@ -217,29 +218,31 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
favorite: Favorite
|
||||
): Promise<void> {
|
||||
await this.withRetry(() =>
|
||||
this.client.set(this.favKey(userName, key), JSON.stringify(favorite))
|
||||
this.client.hSet(this.favHashKey(userName), key, JSON.stringify(favorite))
|
||||
);
|
||||
}
|
||||
|
||||
async getAllFavorites(userName: string): Promise<Record<string, Favorite>> {
|
||||
const pattern = `u:${userName}:fav:*`;
|
||||
const keys: string[] = await this.withRetry(() => this.client.keys(pattern));
|
||||
if (keys.length === 0) return {};
|
||||
const values = await this.withRetry(() => this.client.mGet(keys));
|
||||
const all = await this.withRetry(() =>
|
||||
this.client.hGetAll(this.favHashKey(userName))
|
||||
);
|
||||
const result: Record<string, Favorite> = {};
|
||||
keys.forEach((fullKey: string, idx: number) => {
|
||||
const raw = values[idx];
|
||||
for (const [field, raw] of Object.entries(all)) {
|
||||
if (raw) {
|
||||
const fav = JSON.parse(raw) as Favorite;
|
||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
|
||||
result[keyPart] = fav;
|
||||
result[field] = JSON.parse(raw) as Favorite;
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteFavorite(userName: string, key: string): Promise<void> {
|
||||
await this.withRetry(() => this.client.del(this.favKey(userName, key)));
|
||||
await this.withRetry(() =>
|
||||
this.client.hDel(this.favHashKey(userName), key)
|
||||
);
|
||||
}
|
||||
|
||||
async deleteAllFavorites(userName: string): Promise<void> {
|
||||
await this.withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||
}
|
||||
|
||||
// ---------- 用户注册 / 登录 ----------
|
||||
@@ -286,23 +289,11 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
// 删除搜索历史
|
||||
await this.withRetry(() => this.client.del(this.shKey(userName)));
|
||||
|
||||
// 删除播放记录
|
||||
const playRecordPattern = `u:${userName}:pr:*`;
|
||||
const playRecordKeys = await this.withRetry(() =>
|
||||
this.client.keys(playRecordPattern)
|
||||
);
|
||||
if (playRecordKeys.length > 0) {
|
||||
await this.withRetry(() => this.client.del(playRecordKeys));
|
||||
}
|
||||
// 删除播放记录(Hash key 直接删除)
|
||||
await this.withRetry(() => this.client.del(this.prHashKey(userName)));
|
||||
|
||||
// 删除收藏夹
|
||||
const favoritePattern = `u:${userName}:fav:*`;
|
||||
const favoriteKeys = await this.withRetry(() =>
|
||||
this.client.keys(favoritePattern)
|
||||
);
|
||||
if (favoriteKeys.length > 0) {
|
||||
await this.withRetry(() => this.client.del(favoriteKeys));
|
||||
}
|
||||
// 删除收藏夹(Hash key 直接删除)
|
||||
await this.withRetry(() => this.client.del(this.favHashKey(userName)));
|
||||
|
||||
// 删除跳过片头片尾配置
|
||||
const skipConfigPattern = `u:${userName}:skip:*`;
|
||||
@@ -443,6 +434,81 @@ export abstract class BaseRedisStorage implements IStorage {
|
||||
return configs;
|
||||
}
|
||||
|
||||
// ---------- 数据迁移:旧扁平 key → Hash 结构 ----------
|
||||
private migrationKey() {
|
||||
return 'sys:migration:hash_v1';
|
||||
}
|
||||
|
||||
async migrateData(): Promise<void> {
|
||||
// 检查是否已迁移
|
||||
const migrated = await this.withRetry(() => this.client.get(this.migrationKey()));
|
||||
if (migrated === 'done') return;
|
||||
|
||||
console.log('开始数据迁移:扁平 key → Hash 结构...');
|
||||
|
||||
try {
|
||||
// 迁移播放记录:u:*:pr:* → u:username:pr (Hash)
|
||||
const prKeys = await this.withRetry(() => this.client.keys('u:*:pr:*'));
|
||||
if (prKeys.length > 0) {
|
||||
// 过滤掉新 Hash key(没有第四段的就是 Hash key 本身)
|
||||
const oldPrKeys = prKeys.filter((k) => {
|
||||
const parts = k.split(':');
|
||||
return parts.length >= 4 && parts[2] === 'pr' && parts[3] !== '';
|
||||
});
|
||||
|
||||
if (oldPrKeys.length > 0) {
|
||||
const values = await this.withRetry(() => this.client.mGet(oldPrKeys));
|
||||
for (let i = 0; i < oldPrKeys.length; i++) {
|
||||
const raw = values[i];
|
||||
if (!raw) continue;
|
||||
const match = oldPrKeys[i].match(/^u:(.+?):pr:(.+)$/);
|
||||
if (!match) continue;
|
||||
const [, userName, field] = match;
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.prHashKey(userName), field, raw)
|
||||
);
|
||||
}
|
||||
// 删除旧 key
|
||||
await this.withRetry(() => this.client.del(oldPrKeys));
|
||||
console.log(`迁移了 ${oldPrKeys.length} 条播放记录`);
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移收藏:u:*:fav:* → u:username:fav (Hash)
|
||||
const favKeys = await this.withRetry(() => this.client.keys('u:*:fav:*'));
|
||||
if (favKeys.length > 0) {
|
||||
const oldFavKeys = favKeys.filter((k) => {
|
||||
const parts = k.split(':');
|
||||
return parts.length >= 4 && parts[2] === 'fav' && parts[3] !== '';
|
||||
});
|
||||
|
||||
if (oldFavKeys.length > 0) {
|
||||
const values = await this.withRetry(() => this.client.mGet(oldFavKeys));
|
||||
for (let i = 0; i < oldFavKeys.length; i++) {
|
||||
const raw = values[i];
|
||||
if (!raw) continue;
|
||||
const match = oldFavKeys[i].match(/^u:(.+?):fav:(.+)$/);
|
||||
if (!match) continue;
|
||||
const [, userName, field] = match;
|
||||
await this.withRetry(() =>
|
||||
this.client.hSet(this.favHashKey(userName), field, raw)
|
||||
);
|
||||
}
|
||||
// 删除旧 key
|
||||
await this.withRetry(() => this.client.del(oldFavKeys));
|
||||
console.log(`迁移了 ${oldFavKeys.length} 条收藏`);
|
||||
}
|
||||
}
|
||||
|
||||
// 标记迁移完成
|
||||
await this.withRetry(() => this.client.set(this.migrationKey(), 'done'));
|
||||
console.log('数据迁移完成');
|
||||
} catch (error) {
|
||||
console.error('数据迁移失败:', error);
|
||||
// 不抛出异常,允许服务继续运行
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有数据
|
||||
async clearAllData(): Promise<void> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user