Commit 3f2cfa61 3f2cfa61e7ee2ead10834d37d6dc455bcfb38f87 by 沈秋雨

feat(cache): 优化歌词缓存同步逻辑,追加写入避免全量读取

- 修改 sync_inserted_lyrics_cache 函数,新增 seen_keys 参数用于跳过已存在歌词缓存
- 实现追加写入方式,同步新增歌词缓存时不再全量读取,提升性能
- 在主逻辑中初始化并传递 lyrics_cache_seen_keys,避免重复写入缓存
- 更新测试用例,增加追加写入场景覆盖,防止误读缓存文件读取操作
- 修正缓存文件路径统一使用 lyrics_cache_path,增强代码一致性
1 parent 65615e7b
...@@ -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
......