From 60c51f44ab4345e4e74c451ad7b9dbd35744e386 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 14:17:20 +0800 Subject: [PATCH] fix(posts): move rebuild SELECT into transaction with FOR UPDATE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 发现:SELECT 在事务外读 rows,事务内 UPDATE 时若并发编辑了该 post, 会用旧 content_md 覆盖新内容(非可重复读)。改为 SELECT 移入事务并加 FOR UPDATE,锁住待处理行直到 UPDATE 完成,消除丢失更新。 权衡:FOR UPDATE 锁最多 500 行直到事务结束,阻塞并发编辑;rebuild 是 admin 运维操作,可接受。 --- src/api/posts/rebuild.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/api/posts/rebuild.rs b/src/api/posts/rebuild.rs index 5ba7b45..169092d 100644 --- a/src/api/posts/rebuild.rs +++ b/src/api/posts/rebuild.rs @@ -35,26 +35,29 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result = Vec::new(); - // 整批 UPDATE 纳入单事务:中途断连或写入失败整批回滚,避免产生 - // 「部分文章已重建」的中间态(M5)。 + // 整批 SELECT + UPDATE 纳入单事务:中途断连或写入失败整批回滚,避免产生 + // 「部分文章已重建」的中间态(M5);FOR UPDATE 锁住的行随事务结束释放。 let tx = client.transaction().await.map_err(AppError::query)?; + let rows = tx.query(&query, &[]).await.map_err(AppError::query)?; + for row in &rows { let id: i32 = row.get(0); let content_md: String = row.get(1);