惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threat Research - Cisco Blogs
Microsoft Security Blog
Microsoft Security Blog
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
Recorded Future
Recorded Future
The Register - Security
The Register - Security
Microsoft Azure Blog
Microsoft Azure Blog
Stack Overflow Blog
Stack Overflow Blog
爱范儿
爱范儿
大猫的无限游戏
大猫的无限游戏
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
Webroot Blog
Webroot Blog
Help Net Security
Help Net Security
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
云风的 BLOG
云风的 BLOG
Hacker News: Ask HN
Hacker News: Ask HN
Security Archives - TechRepublic
Security Archives - TechRepublic
Google Online Security Blog
Google Online Security Blog
Attack and Defense Labs
Attack and Defense Labs
T
Tailwind CSS Blog
J
Java Code Geeks
C
CXSECURITY Database RSS Feed - CXSecurity.com
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Cyberwarzone
Cyberwarzone
小众软件
小众软件
G
Google Developers Blog
SecWiki News
SecWiki News
V
V2EX
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Blog of Author Tim Ferriss
S
SegmentFault 最新的问题
MyScale Blog
MyScale Blog
S
Security Affairs
AI
AI
S
Securelist
D
Docker
人人都是产品经理
人人都是产品经理
T
Troy Hunt's Blog
罗磊的独立博客
The Hacker News
The Hacker News
阮一峰的网络日志
阮一峰的网络日志
Google DeepMind News
Google DeepMind News
宝玉的分享
宝玉的分享
P
Proofpoint News Feed
P
Proofpoint News Feed
Vercel News
Vercel News
Jina AI
Jina AI

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
[🗄️DataBase] ACID - Consistency
Mercy · 2026-06-15 · via DEV Community

目錄

  1. ACID 簡單介紹
  2. Consistency 是什麼
  3. Consistency 的兩個層面:資料庫/應用層
  4. 如何達到 Consistency
  5. 總結

1. ACID 簡單介紹

想像你要從 A 銀行轉帳 $100 到 B 銀行:

A 帳戶:-$100
B 帳戶:+$100

如果轉帳到一半伺服器當機,會發生什麼事?

如果只扣了 A 沒加 B → A 少了 $100,B 沒收到 → 錢消失
如果扣了 A 也加了 B → 正常,但當機沒影響
如果兩邊都扣了 → 明顯不對

ACID 就是用來保證這種操作不會出錯的四個規則:

  • A = Atomicity(原子性):全部成功或全部失敗,沒有「只做一半」
  • C = Consistency(一致性):操作前後,所有的規則都沒有被打破
  • I = Isolation(隔離性):同時發生的操作,互相不要干擾
  • D = Durability(持久性):一旦成功,資料不會無故消失

要注意 Atomicity ≠ Consistency

回到轉帳的故事:

Atomicity 保證了:A 扣 $100 和 B 加 $100 要嘛一起成功,要嘛一起失敗
Consistency 保證了:轉帳前 A+B 的總額 = 轉帳後 A+B 的總額

看出差別了嗎?
Atomicity 管的是「操作有沒有完整執行」,Consistency 管的是「操作完結果合不合理」
即使 Atomicity、Isolation、Durability 都滿足了,還是可能違反Consistency。


2. Consistency 到底是什麼?

指的是交易開始前是合法的狀態,交易結束後也必須是合法的狀態。
這個「合法」不是法律上的合法,是「你定義的所有規則都沒有被打破」。

舉例來說

你規定:「冰箱裡的飲料不能少於 5 瓶。」

  • 開始前:冰箱有 8 瓶 ✅
  • 你拿走 2 瓶 → 還剩 6 瓶 ✅
  • 你拿走 4 瓶 → 還剩 4 瓶 ❌(規則被打破)
  • 你把冰箱洗劫一空 → 0 瓶 ❌(規則被打破)

Consistency 就是在檢查:
每一次你開關冰箱後,那個「不能少於 5 瓶」的規則還有沒有被遵守

同樣的道理,在資料庫裡就是:

規則類型 範例 誰來檢查
資料庫內建規則 Primary Key 不能重複、ForeignKey 不能指向不存在的資料、NOT NULL 欄位不能空、UNIQUE 不能重複 資料庫自動擋掉
應用層業務規則 「餘額不能為負」、「每天只能發 5 篇文」、「購物車總金額不得超過 $10,000」 你自己寫程式檢查

常見的誤區

很多人以為:「資料庫會自動保證一致性。」

資料庫只會保證 Primary Key 不重複、ForeignKey 正確指向這些「內建規則」。
但你寫的業務規則,像「餘額不能負的」、「每天提款上限 $30,000」,資料庫根本不知道

Consistency 的最終責任在開發者身上。


3. Consistency 的兩個層面: 資料庫/應用層

3.1. 資料庫層級的 Consistency

這是資料庫幫你做的事,不用寫額外程式碼:

  • Entity Integrity:Primary Key 不能是 NULL
  • Referential Integrity:ForeignKey 必須指向存在的資料
  • Domain Integrity:欄位的型別、長度、格式必須正確(例如 INT 欄位不能存文字)
  • Unique Constraints:不能有重複值
-- 資料庫會幫你擋掉這些:
INSERT INTO users (id, email) VALUES (NULL, 'test@test.com');
-- ❌ Primary Key 不能為 NULL

INSERT INTO orders (user_id) VALUES (999);
-- ❌ 如果 user_id 999 不存在,FK 約束會拒絕

INSERT INTO products (price) VALUES (-100);
-- ❌ 如果有 CHECK(price >= 0) 約束

違反了資料庫直接拒絕操作,回傳錯誤
你不需要寫任何業務邏輯來防止這種情況。

3.2. 應用層級的 Consistency

這是你自己要寫程式處理的,像是:

  • 業務邏輯規則:例如「用戶餘額不能為負」、「訂單折扣不能超過 30%」
  • 跨表狀態一致性:例如「A 表扣庫存時,B 表的訂單明細也要寫入」
  • 衍生資料一致性:例如「快取裡的資料必須和資料庫一致」
  • 操作紀錄的追蹤:例如「每次修改權限都要有操作記錄」

以業務邏輯規則的案例來看為何會違反應用層級的 Consistency :

違反的規則:用戶餘額不能為負(餘額必須 ≥ 0)

// 讀取和寫入之間有時間窗口
public async Task<bool> WithdrawAsync(int accountId, decimal amount)
{
    var account = await _db.Accounts.FindAsync(accountId);
    if (account.Balance < amount) return false;  

    account.Balance -= amount;
    await _db.SaveChangesAsync();
    return true;
}

這段程式碼的問題是,想像兩個人同時提款:

帳戶餘額 $100

Request 1(提款 $80):讀到 Balance = $100 → 通過 → 寫入 $20
Request 2(提款 $70):讀到 Balance = $100(髒資料)→ 通過 → 寫入 $30 ❌

最終餘額:$30(R1 扣的 $80 被覆蓋了,且 R2 基於過期資料做了錯誤決定)
正確應該要是:R1 扣成 $20 → R2 發現 $20 < $70,拒絕提款 → 最終 $20

這不是 Atomicity 的問題(每個扣款都完整執行),也不是資料庫層級的問題(沒有違反 PK/FK),而是應用層的 Consistency 被違反了。

這其實就是典型的 Lost Update(遺失更新)
指的是:兩個交易先後讀取了同一筆資料,各自做了判斷後寫入,後寫的那個把前一個的修改覆蓋掉了


4. 如何達到 Consistency

Consistency 沒有單一解法,需要在資料庫約束、隔離層級、鎖機制、業務邏輯檢查等多方面做處理。

4.1. 資料庫層級:用 Constraints 建立防火牆

把你能想到的規則,盡可能用資料庫的 Constraints 表達出來,讓資料庫自動幫你檢查

Constraint 用途 範例
PRIMARY KEY 每筆資料都要有唯一識別 id INT PRIMARY KEY
FOREIGN KEY 確保關聯存在 user_id INT REFERENCES users(id)
UNIQUE 值不能重複 email VARCHAR(255) UNIQUE
CHECK 自訂條件 CHECK (price >= 0)
NOT NULL 欄位不能為空 name VARCHAR(100) NOT NULL
-- ✅ CHECK 約束:價格不能為負
CREATE TABLE products (
    id INT PRIMARY KEY,
    price DECIMAL(10,2) CHECK (price >= 0)
);

-- ✅ 違反時資料庫直接拒絕
INSERT INTO products (id, price) VALUES (1, -100);
-- ❌ ERROR: CHECK constraint violation

4.2. 應用層級:Consistency 需要 Isolation 來保護

前面 3.2 的提款範例有一個關鍵問題:沒有違反原子性 Atomicity,每個扣款都完整執行,但還是違反 Consistency 。

為什麼?因為 Isolation 不夠

問題的本質

// 這段程式碼有 Transaction,也有 Atomicity,但還是錯了
public async Task<bool> WithdrawAsync(int accountId, decimal amount)
{
    var account = await _db.Accounts.FindAsync(accountId);  // 讀取
    if (account.Balance < amount) return false;             // 檢查
    account.Balance -= amount;                               // 寫入
    await _db.SaveChangesAsync();
    return true;
}

兩個請求同時進來:

餘額 $100
R1(取 $80):讀到 $100 → 檢查通過 → 寫入 $20
R2(取 $70):讀到 $100(還沒被 R1 提交蓋掉)→ 檢查通過 → 寫入 $30 ❌

問題出在「讀取」和「寫入」之間,另一個交易插了進來,讀到了舊資料

要防止這種情況,就要靠隔離層級來控制一個交易能不能看到別人「還沒提交」的變更。

什麼是隔離層級 (Isolation Level)?

資料庫為了效能,不會讓交易真的排隊一個一個執行,而是讓它們同時跑。隔離層級就是在控制一件事:

一個交易能不能看到別人「還沒提交」的變更?

先搞懂「提交」是什麼意思:

在資料庫裡,你對資料做完修改後,要下 COMMIT 指令資料才算真的寫進去。
執行 COMMIT 之前,你的修改叫「還沒提交」。
這時候如果執行 ROLLBACK,修改就會取消,像沒發生過一樣。

回到提款的例子,R1 執行了 account.Balance -= amount 但還沒下 COMMIT:

R1 的修改:$100 → $20(還沒提交,隨時可以 ROLLBACK 取消)
R2 能不能看到這個 $20? → 看隔離層級決定

各隔離層級對 Consistency 的保護程度:

隔離層級 白話解釋
Read Uncommitted 別人還沒 COMMIT 的資料你也看得到
Read Committed 只能看到別人已經 COMMIT 的資料
Repeatable Read 同一筆資料在這筆交易內,不管讀幾次結果都不變
Serializable 所有交易像排隊一個一個執行,互不干擾

舉例來說

原本餘額 $100,R1 取 $80,R2 取 $70:

Read Uncommitted(最弱)
  R1:讀到 $100 → 扣成 $20(還沒提交)
  R2:讀到 $20 ⚠️(看到 R1 還沒提交的變更)
       → $20 < $70 → 拒絕提款 ✅
  但如果 R1 最後 Rollback,R2 看到的 $20 就是髒資料 (Dirty Read)

Read Committed(通常資料庫的預設都是這個層級)
  R1:讀到 $100 → 扣成 $20(還沒提交)
  R2:讀到 $100(看不到 R1 未提交的 $20)
       → 檢查通過 → 扣成 $30 ❌ 結果超賣

Repeatable Read
  R1:讀到 $100 → 扣成 $20
  R2:讀到 $100 → 檢查通過 → 開始扣...
       └ 實際結果依資料庫而異:
         MySQL / SQL Server:UPDATE 會讀最新值 → 餘額變 -$50 ❌
         PostgreSQL:發現資料被改過 → 中止交易,報錯 ✅

Serializable
  R1:讀到 $100 → 扣成 $20
  R2:準備扣成 $30
       ❌ 資料庫發現衝突 → 中止 R2,請他重試

看到這邊你可能會想:「所以只有 Serializable 能解決問題?」
對,如果只靠隔離層級,確實只有 Serializable 保證安全。
但 Serializable 會把所有交易排隊執行,效能很差

所以我們常常會搭配下面幾種方法一同做保護。

4.3. 原子操作:讓資料庫一次做完

為什麼前面 3.2 的寫法會出事?因為程式碼是分三步走的:

var account = await _db.Accounts.FindAsync(accountId);  // 步驟 1:讀取
if (account.Balance < amount) return false;             // 步驟 2:檢查
account.Balance -= amount;                              // 步驟 3:寫入

步驟 1 和步驟 3 之間,別人可以趁機插進來修改資料。

這個從「讀取」到「寫入」之間的空檔,就是所謂的 時間窗口

時間窗口越長,別的交易插進來的機會就越大。

原子操作的思路:三步變一步

不要把「讀取 → 檢查 → 寫入」拆成三段程式碼分開執行,而是用一條 SQL 讓資料庫一次做完:

-- ✅ 庫存夠才扣,不夠則影響 0 筆,全程一條 SQL
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock >= 1;

這條 SQL 做的事情等於:

1. 讀取 stock          ← 資料庫內部一次完成
2. 檢查 stock >= 1     ← 資料庫內部一次完成
3. 夠的話就扣 1        ← 資料庫內部一次完成

因為檢查和寫入發生在同一條 SQL 裡,資料庫在執行這條 SQL 時不會讓其他人插隊,所以根本沒有時間窗口。

// ✅ 對應的 EF Core 寫法
var affected = await _db.Products
    .Where(p => p.Id == productId && p.Stock >= quantity)
    .ExecuteUpdateAsync(p => p.SetProperty(x => x.Stock, x => x.Stock - quantity));

if (affected == 0)
    throw new InvalidOperationException("庫存不足");

原子操作的優點

不需要 Transaction 也能保證一致性。 檢查和寫入在同一條 SQL 中完成,沒有時間差。

當你的操作可以濃縮成一條 SQL 時,等於資料庫幫你擋住了所有並發干擾。

什麼時候不該用

原子操作雖然簡單,但它有極限:

不適合的場景 原因 該用什麼
需要先讀取舊值做複雜判斷(eg. 計算每日提款總額、檢查折扣上限) 一條 SQL 塞不下這麼多邏輯 樂觀鎖(4.4)悲觀鎖(4.5)
需要跨多個 table 保持同步(eg. 扣庫存 + 同時寫訂單明細) 單條 SQL 只能動一個 table Transaction + 適當隔離層級
寫入後需要寫 Log 或發送通知 資料庫只管資料,不管 side effect 應用層處理 + Transaction

4.4. 樂觀鎖(Optimistic Locking)

適合衝突率低的場景,主要是在讀取資料時記下那筆資料的版本號寫入時檢查版本號有沒有被別人改過

舉例來說

想像兩位管理員同時打開商品編輯頁面:

管理員 A:讀到價格 $50 (version=1) → 改成 $100 → 存檔 → version 變 2 ✅
管理員 B:讀到價格 $50 (version=1) → 改成 $90 → 存檔 → version 已經是 2 了 → 拒絕 ❌

管理員 B 在開啟頁面到按下存檔之間,別人已經改過了。

樂觀鎖的做法很簡單:

每筆資料帶一個 version 欄位,寫入時檢查版本。

SQL 範例

UPDATE products SET price = @newPrice, version = version + 1
WHERE id = @id AND version = @oldVersion;
-- 管理員 B 傳入 @oldVersion = 1,但資料庫中的 version 已被 A 改成 2
-- WHERE version = 1 找不到資料 → 影響 0 筆 → 拋出 DbUpdateConcurrencyException

EF Core 實作

在 Entity 上加一個版本欄位,標記為 ConcurrencyToken

public class Product
{
    public int Id { get; set; }
    public decimal Price { get; set; }
    public int Version { get; set; }  // 樂觀鎖欄位
}

var product = await _db.Products.FindAsync(productId);
product.Price = newPrice;

// EF Core 自動生成類似這樣的 SQL:
// UPDATE products SET price = @p0, version = version + 1
// WHERE id = @p1 AND version = @p2
// 如果影響 0 筆 → 拋出 DbUpdateConcurrencyException
await _db.SaveChangesAsync();

衝突發生後怎麼辦

try
{
    await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // 重新載入最新資料
    var entry = _db.Entry(product);
    await entry.ReloadAsync();
    // 告訴使用者「資料已被別人修改,請重新確認」
    Console.WriteLine($"最新價格是 {product.Price},請重新填寫");
}

樂觀鎖的特色是:不阻止並發,只偵測衝突

意思是讀取時不鎖資料,只是在寫入時才檢查

適用場景 不適用場景
一般 CRUD、後台管理操作 秒殺搶票
同一個人編輯自己的資料 高頻扣庫存
使用者很少同時改同一筆資料 會大量重試且重試成本高

4.5. 悲觀鎖(Pessimistic Locking)

如果今天的場景不是「兩個管理員修改商品價格」,
而是一千個人同時搶最後一件商品,樂觀鎖就不適用了。
因為每個人都會衝突,所以每個人都在 Retry,系統反而更慢

悲觀鎖的邏輯與樂觀所相反:

讀取時直接鎖起來,不讓別人碰,做完才解鎖

舉例來說

想像一個線上購物系統在特賣活動中,只剩最後一台筆電:

Request A(小明):SELECT ... FOR UPDATE → 鎖住這筆資料 → 扣庫存 → Commit → 解鎖
Request B(小華):                                             等到 A 解鎖才能讀 → 發現沒貨了

小華不是「讀到舊庫存然後扣失敗」,他是根本讀不到,直到 A 做完。

SQL 範例

BEGIN;
SELECT * FROM products WHERE id = 1 FOR UPDATE;  -- 鎖住 id=1 這筆
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;  -- 解鎖

FOR UPDATE 的意思是:「我要鎖這筆,其他人要讀取或修改都請排隊。」

EF Core 實作

await using var tx = await _db.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted);

// 鎖住這筆資料,其他交易要讀同一筆時必須等
var product = await _db.Products
    .FromSqlRaw("SELECT * FROM products WHERE id = {0} FOR UPDATE", productId)
    .FirstAsync();

if (product.Stock >= quantity)
{
    product.Stock -= quantity;
    await _db.SaveChangesAsync();
    await tx.CommitAsync();
}
// Transaction 結束 → 自動解鎖

代價:並行度降低

A 鎖住資料時,B 必須排隊等

所以要確保 Transaction 短小輕快,千萬不要在使用者填表單的過程中鎖資料,不然使用者填 10 分鐘,資料就鎖 10 分鐘。

而且如果 Transaction A 鎖了商品等訂單,Transaction B 鎖了訂單等商品,兩邊都在等對方釋放,就會造成死鎖

有這種情況時,資料庫會自動偵測並中止其中一個,但你的程式碼要做好重試的處理。

樂觀鎖 vs 悲觀鎖

樂觀鎖 悲觀鎖
什麼時候檢查衝突 寫入時才檢查 讀取時就直接鎖
衝突時的代價 重試一次 排隊等待
適合情境 衝突率低 衝突率高
讀取效能 高(不鎖) 低(要等鎖)
典型場景 修改文章、更新個人資料 搶購、扣庫存、扣款

4.6. 業務規則檢查:自己寫程式碼檢查

資料庫不知道你的業務規則,例如「每日提款上限 $30,000」、「折扣不能超過 30%」。

這些必須你自己寫程式碼檢查

-- ❌ 資料庫不會幫你擋這個:
INSERT INTO transactions (user_id, amount) VALUES (1, 35000);
-- 即使超過日限額,資料庫照樣寫入,因為它不知道你的規則

// ✅ 你要自己檢查:
var todayTotal = await _db.Transactions
    .Where(t => t.UserId == userId && t.CreatedAt.Date == DateTime.UtcNow.Date)
    .SumAsync(t => t.Amount);

if (todayTotal + amount > 30000)
    throw new InvalidOperationException("每日提款上限 $30,000");


5. 總結

Consistency(一致性)的核心問題只有一個:交易結束後,你定義的規則還有沒有被遵守?

Consistency 不是資料庫單方面能保證的,它需要幾個層級來做保護:

  1. 資料庫 Constraints,例如 PK、FK、CHECK、UNIQUE 的基本防護
  2. Transaction + 隔離層級 ,確保同時發生的操作不互相干擾
  3. 鎖機制(樂觀鎖 / 悲觀鎖), 精確控制並發存取
  4. 業務規則檢查,只有開發者知道的業務規則,要自己寫程式檢查

沒有哪一招能搞定所有情況,應該要在不同的情境下,選用不同的工具組合。