feat(cache): 优化歌词缓存同步逻辑,追加写入避免全量读取
- 修改 sync_inserted_lyrics_cache 函数,新增 seen_keys 参数用于跳过已存在歌词缓存 - 实现追加写入方式,同步新增歌词缓存时不再全量读取,提升性能 - 在主逻辑中初始化并传递 lyrics_cache_seen_keys,避免重复写入缓存 - 更新测试用例,增加追加写入场景覆盖,防止误读缓存文件读取操作 - 修正缓存文件路径统一使用 lyrics_cache_path,增强代码一致性
Showing
2 changed files
with
64 additions
and
15 deletions
| ... | @@ -752,13 +752,16 @@ def _read_existing_lyrics_cache(cache_path: Path) -> dict[tuple[str, str], dict] | ... | @@ -752,13 +752,16 @@ def _read_existing_lyrics_cache(cache_path: Path) -> dict[tuple[str, str], dict] |
| 752 | return cached_by_key | 752 | return cached_by_key |
| 753 | 753 | ||
| 754 | 754 | ||
| 755 | def sync_inserted_lyrics_cache(cache_path: Path, inserted_rows: list[tuple[tuple, dict]]) -> int: | 755 | def sync_inserted_lyrics_cache( |
| 756 | """把本次已成功插入目标库的歌词同步到 L2 启动缓存。""" | 756 | cache_path: Path, |
| 757 | inserted_rows: list[tuple[tuple, dict]], | ||
| 758 | seen_keys: set[tuple[str, str]] | None = None, | ||
| 759 | ) -> int: | ||
| 760 | """把本次已成功插入目标库的歌词追加同步到 L2 启动缓存。""" | ||
| 757 | if not inserted_rows: | 761 | if not inserted_rows: |
| 758 | return 0 | 762 | return 0 |
| 759 | 763 | ||
| 760 | cached_by_key = _read_existing_lyrics_cache(cache_path) | 764 | new_items = [] |
| 761 | added = 0 | ||
| 762 | for tuple_row, source_row in inserted_rows: | 765 | for tuple_row, source_row in inserted_rows: |
| 763 | url = tuple_row[OSS_LYRIC_INDEX] | 766 | url = tuple_row[OSS_LYRIC_INDEX] |
| 764 | lyrics = source_row.get('lyrics_txt_content') | 767 | lyrics = source_row.get('lyrics_txt_content') |
| ... | @@ -769,9 +772,9 @@ def sync_inserted_lyrics_cache(cache_path: Path, inserted_rows: list[tuple[tuple | ... | @@ -769,9 +772,9 @@ def sync_inserted_lyrics_cache(cache_path: Path, inserted_rows: list[tuple[tuple |
| 769 | 772 | ||
| 770 | record_id = str(tuple_row[0]) | 773 | record_id = str(tuple_row[0]) |
| 771 | key = (record_id, str(url)) | 774 | key = (record_id, str(url)) |
| 772 | if key not in cached_by_key: | 775 | if seen_keys is not None and key in seen_keys: |
| 773 | added += 1 | 776 | continue |
| 774 | cached_by_key[key] = { | 777 | item = { |
| 775 | 'id': record_id, | 778 | 'id': record_id, |
| 776 | 'url': str(url), | 779 | 'url': str(url), |
| 777 | 'lyrics': str(lyrics), | 780 | 'lyrics': str(lyrics), |
| ... | @@ -780,10 +783,18 @@ def sync_inserted_lyrics_cache(cache_path: Path, inserted_rows: list[tuple[tuple | ... | @@ -780,10 +783,18 @@ def sync_inserted_lyrics_cache(cache_path: Path, inserted_rows: list[tuple[tuple |
| 780 | 'lyricist': tuple_row[2], | 783 | 'lyricist': tuple_row[2], |
| 781 | 'composer': tuple_row[3], | 784 | 'composer': tuple_row[3], |
| 782 | } | 785 | } |
| 786 | new_items.append(item) | ||
| 787 | if seen_keys is not None: | ||
| 788 | seen_keys.add(key) | ||
| 783 | 789 | ||
| 784 | if added: | 790 | if not new_items: |
| 785 | _write_existing_lyrics_cache(cache_path, cached_by_key) | 791 | return 0 |
| 786 | return added | 792 | |
| 793 | cache_path.parent.mkdir(parents=True, exist_ok=True) | ||
| 794 | with cache_path.open('a', encoding='utf-8') as f: | ||
| 795 | for item in new_items: | ||
| 796 | f.write(json.dumps(item, ensure_ascii=False) + '\n') | ||
| 797 | return len(new_items) | ||
| 787 | 798 | ||
| 788 | 799 | ||
| 789 | def load_existing_l2_candidates( | 800 | def load_existing_l2_candidates( |
| ... | @@ -1627,6 +1638,8 @@ def main(): | ... | @@ -1627,6 +1638,8 @@ def main(): |
| 1627 | dedup_report = None | 1638 | dedup_report = None |
| 1628 | review_report = None | 1639 | review_report = None |
| 1629 | source_record_counts: dict[str, int] = {} | 1640 | source_record_counts: dict[str, int] = {} |
| 1641 | lyrics_cache_path = CACHE_DIR / f'{TARGET_TABLE_NAME}_existing_lyrics.jsonl' | ||
| 1642 | lyrics_cache_seen_keys: set[tuple[str, str]] = set() | ||
| 1630 | 1643 | ||
| 1631 | if not args.skip_dedup: | 1644 | if not args.skip_dedup: |
| 1632 | logger.info("正在构建 L1 元数据索引...") | 1645 | logger.info("正在构建 L1 元数据索引...") |
| ... | @@ -1660,14 +1673,14 @@ def main(): | ... | @@ -1660,14 +1673,14 @@ def main(): |
| 1660 | with target_conn.cursor(pymysql.cursors.DictCursor) as cursor: | 1673 | with target_conn.cursor(pymysql.cursors.DictCursor) as cursor: |
| 1661 | cursor.execute(lyric_sql) | 1674 | cursor.execute(lyric_sql) |
| 1662 | existing_lyric_rows = cursor.fetchall() | 1675 | existing_lyric_rows = cursor.fetchall() |
| 1663 | cache_path = CACHE_DIR / f'{TARGET_TABLE_NAME}_existing_lyrics.jsonl' | 1676 | existing_records, cache_stats = load_existing_l2_candidates(existing_lyric_rows, lyrics_cache_path) |
| 1664 | existing_records, cache_stats = load_existing_l2_candidates(existing_lyric_rows, cache_path) | 1677 | lyrics_cache_seen_keys = set(_read_existing_lyrics_cache(lyrics_cache_path).keys()) |
| 1665 | for record in existing_records: | 1678 | for record in existing_records: |
| 1666 | l2_candidates.add(record) | 1679 | l2_candidates.add(record) |
| 1667 | logger.info( | 1680 | logger.info( |
| 1668 | f"L2 候选集加载完成: {len(l2_candidates)} 条 | " | 1681 | f"L2 候选集加载完成: {len(l2_candidates)} 条 | " |
| 1669 | f"缓存命中={cache_stats['cached']} 下载={cache_stats['downloaded']} 失败={cache_stats['failed']} | " | 1682 | f"缓存命中={cache_stats['cached']} 下载={cache_stats['downloaded']} 失败={cache_stats['failed']} | " |
| 1670 | f"缓存文件={cache_path}" | 1683 | f"缓存文件={lyrics_cache_path}" |
| 1671 | ) | 1684 | ) |
| 1672 | except Exception as e: | 1685 | except Exception as e: |
| 1673 | logger.error(f"加载已有歌词失败: {e}") | 1686 | logger.error(f"加载已有歌词失败: {e}") |
| ... | @@ -1776,8 +1789,9 @@ def main(): | ... | @@ -1776,8 +1789,9 @@ def main(): |
| 1776 | with stats_lock: | 1789 | with stats_lock: |
| 1777 | stats['inserted'] += batch_inserted | 1790 | stats['inserted'] += batch_inserted |
| 1778 | cache_added = sync_inserted_lyrics_cache( | 1791 | cache_added = sync_inserted_lyrics_cache( |
| 1779 | CACHE_DIR / f'{TARGET_TABLE_NAME}_existing_lyrics.jsonl', | 1792 | lyrics_cache_path, |
| 1780 | batch_cache_rows, | 1793 | batch_cache_rows, |
| 1794 | lyrics_cache_seen_keys, | ||
| 1781 | ) | 1795 | ) |
| 1782 | if cache_added: | 1796 | if cache_added: |
| 1783 | logger.info(f"目标库歌词缓存同步新增: {cache_added} 条") | 1797 | logger.info(f"目标库歌词缓存同步新增: {cache_added} 条") |
| ... | @@ -1795,8 +1809,9 @@ def main(): | ... | @@ -1795,8 +1809,9 @@ def main(): |
| 1795 | stats['inserted'] += 1 | 1809 | stats['inserted'] += 1 |
| 1796 | if row_i < len(batch_cache_rows): | 1810 | if row_i < len(batch_cache_rows): |
| 1797 | sync_inserted_lyrics_cache( | 1811 | sync_inserted_lyrics_cache( |
| 1798 | CACHE_DIR / f'{TARGET_TABLE_NAME}_existing_lyrics.jsonl', | 1812 | lyrics_cache_path, |
| 1799 | [batch_cache_rows[row_i]], | 1813 | [batch_cache_rows[row_i]], |
| 1814 | lyrics_cache_seen_keys, | ||
| 1800 | ) | 1815 | ) |
| 1801 | except Exception as e2: | 1816 | except Exception as e2: |
| 1802 | with stats_lock: | 1817 | with stats_lock: | ... | ... |
| ... | @@ -23,6 +23,7 @@ from tqdm import tqdm | ... | @@ -23,6 +23,7 @@ from tqdm import tqdm |
| 23 | # 确保项目根目录在 sys.path | 23 | # 确保项目根目录在 sys.path |
| 24 | sys.path.insert(0, str(Path(__file__).resolve().parent)) | 24 | sys.path.insert(0, str(Path(__file__).resolve().parent)) |
| 25 | 25 | ||
| 26 | import import_hk_songs as import_script | ||
| 26 | from import_hk_songs import ( | 27 | from import_hk_songs import ( |
| 27 | AGGREGATE_SQL, | 28 | AGGREGATE_SQL, |
| 28 | REPORT_DIR, | 29 | REPORT_DIR, |
| ... | @@ -301,6 +302,39 @@ class TestExistingLyricsCache: | ... | @@ -301,6 +302,39 @@ class TestExistingLyricsCache: |
| 301 | assert cache_items[1]['url'] == 'https://oss.example.test/200.txt' | 302 | assert cache_items[1]['url'] == 'https://oss.example.test/200.txt' |
| 302 | assert cache_items[1]['lyrics'] == '新歌词正文' | 303 | assert cache_items[1]['lyrics'] == '新歌词正文' |
| 303 | 304 | ||
| 305 | def test_sync_inserted_lyrics_cache_appends_without_reading_full_cache(self, tmp_path, monkeypatch): | ||
| 306 | cache_path = tmp_path / 'lyrics_cache.jsonl' | ||
| 307 | cache_path.write_text( | ||
| 308 | json.dumps({ | ||
| 309 | 'id': '100', | ||
| 310 | 'url': 'https://oss.example.test/100.txt', | ||
| 311 | 'lyrics': '已有歌词', | ||
| 312 | }, ensure_ascii=False) + '\n', | ||
| 313 | encoding='utf-8', | ||
| 314 | ) | ||
| 315 | tuple_row = [None] * 45 | ||
| 316 | tuple_row[0] = 201 | ||
| 317 | tuple_row[1] = '新歌' | ||
| 318 | tuple_row[2] = '新词' | ||
| 319 | tuple_row[3] = '新曲' | ||
| 320 | tuple_row[8] = 'https://oss.example.test/201.txt' | ||
| 321 | tuple_row[33] = '新歌手' | ||
| 322 | |||
| 323 | def fail_if_full_cache_read(path): | ||
| 324 | raise AssertionError('should not read full cache during append sync') | ||
| 325 | |||
| 326 | monkeypatch.setattr(import_script, '_read_existing_lyrics_cache', fail_if_full_cache_read) | ||
| 327 | |||
| 328 | added = sync_inserted_lyrics_cache( | ||
| 329 | cache_path, | ||
| 330 | [(tuple(tuple_row), {'lyrics_txt_content': '新增歌词'})], | ||
| 331 | ) | ||
| 332 | |||
| 333 | cache_lines = cache_path.read_text(encoding='utf-8').splitlines() | ||
| 334 | assert added == 1 | ||
| 335 | assert len(cache_lines) == 2 | ||
| 336 | assert json.loads(cache_lines[-1])['id'] == '201' | ||
| 337 | |||
| 304 | 338 | ||
| 305 | # ==================================================================== | 339 | # ==================================================================== |
| 306 | # L1 元数据去重 check_l1 | 340 | # L1 元数据去重 check_l1 | ... | ... |
-
Please register or sign in to post a comment