使用 hash 优化用户信息获取速度

This commit is contained in:
shinya
2026-02-27 19:57:14 +08:00
parent 3a201c7546
commit 13f1fb7166
6 changed files with 252 additions and 105 deletions

View File

@@ -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 {