---
title: "電商閃購搶購系統設計：樂觀鎖、悲觀鎖、Redis 原子性、佇列完整解決方案"
description: "用淺顯易懂的方式解說電商搶購系統的三大核心技術：樂觀鎖控制庫存、Redis 原子性保證資料一致性、佇列處理高併發，讓你快速理解高並發系統設計。"
canonical_url: "https://blog.markkulab.net/post/ecommerce-flash-sale-optimization"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2025-12-26 10:00:00 +0800"
category: "Architecture"
tags: ["ecommerce", "flash sale", "optimistic lock", "redis", "queue", "high concurrency", "inventory", "system design"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# 電商閃購搶購系統設計：樂觀鎖、悲觀鎖、Redis 原子性、佇列完整解決方案

## 前言

雖然沒實作過，但研究一下電商的搶購系統，怎麼設計，下次要做就會比較快

1. **樂觀鎖** - 怎麼防止超賣
2. **Redis 原子性** - 怎麼確保不會重複扣款  
3. **佇列** - 怎麼削峰填谷，不讓服務器累趴

可以先把三個角色想成這樣：Redis 衝在第一線，用記憶體超快扣庫存，負責「撐壓」；Queue 佇列負責先把請求收進來排隊，讓使用者看到「排隊中」；資料庫則當正本和記帳本，只管資料正不正確、有沒有存好，不負責硬撐所有流量。

## 問題：為什麼會超賣？

假設只剩 1 件商品，有 2 個人同時搶購。

```
時間    user1                 user2
t1      查庫存:1件            
t2      確認下單              查庫存:1件
t3      扣庫存 → 0 件         確認下單
t4                            扣庫存 → -1 件 ❌ 超賣！
```

**根本原因**：讀和寫之間的間隙，允許多人同時操作同一件商品。

---

## 方案 1：悲觀鎖（Pessimistic Lock）

在講樂觀鎖之前，先快速補一下「悲觀鎖」這個經典做法，很多舊系統或金流相關系統都還是用這一套，這樣的做法確實很容易，就能確保交易的一致性。

### 核心概念

所謂 **悲觀鎖**，就是「我先假設一定會衝突，所以乾脆一開始就鎖起來」。

簡化流程會像這樣：

1. 開啟交易（`BEGIN TRANSACTION`）。
2. `SELECT ... FOR UPDATE` 把那筆商品資料鎖起來（別人只能等你用完）。
3. 檢查庫存是否足夠，如果足夠就更新庫存。
4. `COMMIT` 提交交易，釋放鎖。

只要你還沒 `COMMIT / ROLLBACK`，其他人就算查同一筆資料也會被「卡住」等你處理完，天生不會超賣，但換來的是「大家都在排隊等資料庫」。

### SQL 範例（以 MySQL 為例）

```sql
-- 1. 開啟交易
BEGIN;

-- 2. 讀取並鎖住指定商品的那一列資料
SELECT id, quantity
FROM products
WHERE id = 123
FOR UPDATE;

-- 3. 檢查庫存是否足夠
--   （這段通常在應用程式內判斷）

-- 4. 足夠的話，扣庫存
UPDATE products
SET quantity = quantity - 1
WHERE id = 123;

-- 5. 提交交易
COMMIT;
```

這裡的關鍵是 `FOR UPDATE`：

- 第一個先拿到鎖的交易可以繼續往下跑；
- 之後來的人會在 `SELECT ... FOR UPDATE` 那行被卡住，一直等到前一個交易 `COMMIT / ROLLBACK` 釋放鎖。

### C# + Dapper 範例（簡化版）

```csharp
public async Task<GeneralResult> PurchaseWithPessimisticLock(
  int productId,
  IDbConnection connection,
  IDbTransaction transaction)
{
  // Step 1: 先在同一個 Transaction 裡鎖住這筆資料
  const string selectSql = @"SELECT Id, Quantity
FROM Products
WHERE Id = @Id
FOR UPDATE";

  var product = await connection.QuerySingleAsync<Product>(
    selectSql,
    new { Id = productId },
    transaction
  );

  if (product.Quantity <= 0)
  {
    return new GeneralResult
    {
      Success = false,
      Message = "已售完"
    };
  }

  // Step 2: 直接在同一個交易裡扣庫存
  const string updateSql = @"UPDATE Products
SET Quantity = Quantity - 1
WHERE Id = @Id";

  var affectedRows = await connection.ExecuteAsync(
    updateSql,
    new { Id = productId },
    transaction
  );

  if (affectedRows == 0)
  {
    return new GeneralResult
    {
      Success = false,
      Message = "購買失敗"
    };
  }

  return new GeneralResult
  {
    Success = true,
    Message = "購買成功（悲觀鎖）"
  };
}
```

> 注意：上面的程式碼假設交易 (`transaction`) 是在外層開好並且控制 `Commit / Rollback`，例如：
>
> ```csharp
> using (var transaction = connection.BeginTransaction())
> {
>   var result = await PurchaseWithPessimisticLock(productId, connection, transaction);
>
>   if (result.Success)
>     transaction.Commit();
>   else
>     transaction.Rollback();
> }
> ```

### 悲觀鎖的優缺點

- **優點**：
  - 寫法直覺，「先鎖再改」，不容易超賣；
  - 很適合「真的不能有任何衝突」或「交易金額很大」的情境。
- **缺點**：
  - 高併發時，大量交易都在等鎖，延遲飆高；
  - 鎖住的是資料庫 row / page，所有壓力都集中在 DB；
  - 鎖用得不好可能產生死鎖（Deadlock），需要小心順序與 timeout 設定。

所以在一般高併發搶購系統裡，悲觀鎖通常不會當成第一選擇，而是「你真的很在意資料不能有一絲誤差」的補充手段；多數情況會改用「樂觀鎖 + Redis + Queue」這種比較能撐流量的組合。

---

## 方案 2：樂觀鎖（Optimistic Lock）

### 核心概念

**樂觀鎖**的想法很簡單：我不鎖住數據，而是在提交時檢查「期間有沒有人改過」。

```
1. 讀取商品資訊 (quantity=1, version=5)
2. 計算扣款後的數量
3. 更新時檢查：version 還是 5 嗎？
   - 是 → 更新成功
   - 否 → 代表有人改過，重試
```

### 實作範例

**資料庫設計：**

```sql
CREATE TABLE products (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  quantity INT,
  version INT  -- 版本號，每次更新 +1
);
```

**購買邏輯（C# 範例，簡化版）：**

```csharp
public GeneralResult PurchaseWithOptimisticLock(int productId, IDbConnection connection)
{
  // Step 1: 讀取當前庫存和版本
  const string selectSql = @"SELECT Id, Quantity, Version FROM Products WHERE Id = @Id";

  var product = connection.QuerySingle<Product>(selectSql, new { Id = productId });

  var currentVersion = product.Version;
  var currentQty = product.Quantity;

  // Step 2: 檢查庫存是否足夠
  if (currentQty <= 0)
  {
    return new GeneralResult
    {
      Success = false,
      Message = "已售完"
    };
  }

  // Step 3: 嘗試更新（樂觀鎖檢查）
  const string updateSql = @"UPDATE Products 
SET Quantity = Quantity - 1, Version = Version + 1 
WHERE Id = @Id AND Version = @Version";

  var affectedRows = connection.Execute(updateSql, new
  {
    Id = productId,
    Version = currentVersion
  });

  // Step 4: 檢查更新是否成功
  if (affectedRows == 0)
  {
    // Version 不符 → 代表有人搶先更新了，請使用者重試
    return new GeneralResult
    {
      Success = false,
      Message = "購買失敗，請重試"
    };
  }

  // 成功！
  return new GeneralResult
  {
    Success = true,
    Message = "購買成功"
  };
}

public class Product
{
  public int Id { get; set; }
  public int Quantity { get; set; }
  public int Version { get; set; }
}

public class GeneralResult
{
  public bool Success { get; set; }
  public string Message { get; set; }
}
```

### 優缺點

整體來說，樂觀鎖的好處是很好懂，也不用真的去「鎖表」，自然也不會卡死鎖，在一般系統裡效能通常都不錯。缺點是，一旦大家常常同時搶同一筆資料，就會出現一堆「更新失敗、請重試」，前端或服務端還要寫重試邏輯，衝突一多，效能就會開始掉。

### 單靠資料庫樂觀鎖會遇到什麼問題？

如果整套架構只靠資料庫樂觀鎖，問題會慢慢浮出來：所有讀寫壓力都堆在資料庫上，高峰時連線數、IO、鎖競爭都會被撐到極限；熱門商品那幾筆 row 會變成大塞車現場，大家一直重讀、重試，效能只會越來越差，而且前面就算有 CDN 或反向代理，最後流量還是一次灌進 DB，幾乎沒辦法幫你削峰。

小結一下，樂觀鎖主要在顧「資料正不正確」，不太管「一次湧進多少人」。平常一般下單、後台操作，用樂觀鎖就很夠；但秒殺、搶購這種幾萬人同時衝進來，只靠樂觀鎖 DB 還是會爆，一定要搭配前面的 Redis + Queue 幫忙分壓。

---

## 方案 3：Redis 原子性（Atomic Operation）

先簡單解釋一下「原子性」：可以把它想成一個動作要嘛整個做完、要嘛完全沒發生，中間不會卡在「做到一半」的奇怪狀態。  

樂觀鎖通常是「先查再決決定要不要更新」，在這中間還會經過應用程式和網路；Redis 的做法比較像是**把整個扣庫存的流程包成包成一次原子操作**，丟進去之後在執行的那一刻不會被別的指令打斷。

### 核心概念

Redis 是單線程，所有操作都在記憶體，指令執行不會被中斷，相當的快速。

```
User1              User2              Redis
減庫存             減庫存              庫存 = 1
─────────────────────────────────────────
  ├─ 執行 DECR       
  │                                   庫存 = 0
  │                 ├─ 執行 DECR
  │                 │                庫存 = -1
  └─ 返回 0 ✓       └─ 返回 -1 ❌
```

### 為什麼 Redis 可以做到原子性？

先從 Redis 自己說起：它是單線程模型，一次只會處理一個指令，其他請求都乖乖排在隊伍裡等，所以不會出現「兩個人同時改同一個值」這種情況。

再來是 Lua。可以把 Lua 想成一個很輕量的腳本語言，很常被「嵌」進別的系統裡當客製化邏輯在用，像 Nginx / OpenResty、很多遊戲引擎都有 Lua 的身影。Redis 本身就內建 Lua 執行環境，所以我們可以把一串 Redis 指令寫成一小段 Lua 程式碼，交給 Redis 一次跑完。

用更白話一點講，Lua 腳本就是把很多 Redis 指令打包成**一顆不可拆的膠囊**。  

裡面可以先 `GET` 再 `DECR`，中間不會有人插隊，外面看起來就像送進去一個「大指令」，要嘛整包成功、要嘛整包失敗，這就是 Lua 原子性的概念，這也是為什麼搶購場景更傾向用 Redis 當前線閘門。

雖然 Redis 在跑 Lua 腳本的那一小段時間，其他請求會被稍微卡一下，但因為全部都在記憶體裡運作，整段腳本通常不到 1 毫秒就結束，實務上幾乎感受不到。

對照資料庫的樂觀鎖：DB 要「先查再改」，中間還要經過應用程式和網路，這段空窗期內其他交易可能讀到舊資料，造成版本衝突、一直重試；Redis 則是把「檢查 + 扣庫存」綁成同一個步驟，天生衝突就比較少。


### 實作範例

**初始化庫存：**

```bash
# 設定商品 123 的庫存為 100
SET product:123:qty 100
```

**減少庫存（Lua 腳本保證原子性）：**

```lua
-- redis_script.lua
local key = KEYS[1]
local qty = redis.call('GET', key)

if not qty or tonumber(qty) <= 0 then
  return 0  -- 已售完
end

redis.call('DECR', key)
return 1  -- 成功
```

**後端程式碼：**

```javascript
// Node.js + ioredis
const redis = require('ioredis');
const client = new redis();

async function purchaseWithRedis(productId, quantity = 1) {
  const key = `product:${productId}:qty`;
  
  // 使用 DECR 自動減庫存（原子操作）
  const remaining = await client.decr(key);
  
  if (remaining < 0) {
    // 庫存不足，還原
    await client.incr(key);
    return false;
  }
  
  return true;  // 成功
}
```

### 優缺點

Redis 最大的優點就是快，所有東西都在記憶體裡，指令又是原子執行，很適合拿來當庫存、計數器這種「數字型」的前線閘門，大多數情況也不太需要重試。缺點是，資料本身只存在 Redis，如果持久化沒設好，斷電或異常時有遺失風險，而且最後還是得同步回主資料庫，中間會有一點點時間差，要用對場景。

---

## 方案 4：Queue 佇列削峰（Queue-Based Load Shedding）

洪峰來臨時，與其讓所有請求同時進來把伺服器壓爆，不如先丟進 Queue 佇列排隊慢慢處理。

> 心智模型可以這樣想：
> - 前端 API：**同步** 把這次購買請求「寫進佇列」，很快回應「排隊中」。
> - 後端 worker：在背景 **非同步** 把佇列裡的請求一個一個拿出來，扣庫存、建訂單、刷卡。

### 使用者到底在等什麼？

在這個設計裡，**使用者不需要等 queue 把事情做完** 才拿到 API 回應。

可以簡單分成兩層來看：

- **HTTP API 層**：
  - 只會「同步等」一件事：`purchaseQueue.add(...)` 有沒有成功把這次請求加進佇列（或是佇列已滿、Redis 掛掉等錯誤）。
  - 這通常只要幾毫秒 → 加完就立刻回 `{ status: '排隊中', jobId / position }`，**不會等到扣庫存、刷卡整個流程都跑完**。
- **背景處理層（worker）**：
  - 真正的「扣庫存 / 建訂單 / 刷卡」都在 worker 裡 **非同步慢慢處理**。
  - 使用者接下來可以：
    - 看前端顯示的「排隊中」畫面，前端自己定期去 call 狀態查詢 API；或
    - 用 WebSocket / push 通知，在「搶購成功 / 失敗」時再主動提醒使用者。

### 核心概念

```
使用者         佇列服務           庫存檢查          付款
├─ 同步：加入佇列 ──→ 
├─ 排隊等待
├─ 輪到你
└─ 非同步：扣款、確認 ──→ 減庫存 ──→ 成功返回
```

流量從 **尖峰** 變成 **平緩**。

### 實作範例

**使用 RabbitMQ / Kafka 或 Redis Queue：**

```javascript
// 1. 使用者點購買 → 加入佇列
const queue = require('bull');
const purchaseQueue = new queue('purchases', {
  redis: { host: '127.0.0.1', port: 6379 }
});

app.post('/purchase', async (req, res) => {
  const { productId, userId } = req.body;
  
  // 直接返回，交給佇列處理
  await purchaseQueue.add({ productId, userId });
  
  res.json({ 
    status: '排隊中',
    position: await purchaseQueue.count()
  });
});

// 2. 佇列背景處理
purchaseQueue.process(async (job) => {
  const { productId, userId } = job.data;
  
  // 真正執行購買邏輯
  const success = await deductInventory(productId);
  
  if (success) {
    await processPayment(userId, productId);
    await sendConfirmationEmail(userId);
    return { status: 'success' };
  } else {
    throw new Error('庫存不足');
  }
});
```

### 優缺點

Queue 的好處，就是把原本一秒湧進來的海量請求攤平變成一條隊伍，後端伺服器的壓力會平均很多，加上多半有內建重試機制，整體穩定度也會比較好。相對的，使用者就得接受「排隊一下」這件事，系統本身也多了一套佇列基礎建設要維護，整體複雜度會比單一資料庫高一些。

---

## 整合方案：三層防守

實務上，通常三個技術都用上，形成 **多層防禦**：

```
┌─────────────────────────────────────┐
│  使用者請求                          │
└──────────────┬──────────────────────┘
               │
        ┌──────▼─────────┐
        │  佇列削峰       │  防止雪崩
        │  (限流)        │
        └──────┬─────────┘
               │
        ┌──────▼─────────────────┐
        │  Redis 原子扣庫存      │  快速、可靠
        │  DECR / Lua Script    │
        └──────┬─────────────────┘
               │
        ┌──────▼─────────────────┐
        │  樂觀鎖同步到 DB       │  持久化
        │  (最終一致性)         │
        └──────┬─────────────────┘
               │
        ┌──────▼──────────┐
        │  訂單、支付流程  │
        │  (異步處理)     │
        └─────────────────┘
```

      「樂觀鎖同步到 DB」可以這樣實作：

      1. 資料庫的 `products` 表保留 `quantity` + `version` 欄位。
      2. 佇列消費者在 **Redis 扣庫存成功之後**，再從 DB 讀取該商品當前 `version`。
      3. 以樂觀鎖更新：`UPDATE ... SET quantity = quantity - 1, version = version + 1 WHERE id = ? AND version = ?`。
      4. 若 `affectedRows = 0`，代表版本被別人更新過，可以：
         - 重試幾次；或
         - 記錄成待對帳資料，之後用批次任務修正。

      這樣 Redis 只負責「前線搶購速度」，DB 則負責「最終正本」，兩者透過樂觀鎖維持最終一致性。

    換句話說，在這個架構裡：

    - **資料庫只負責「資料正確、記錄完整」**（正本、對帳、報表）。
    - **不要把「撐高併發」丟給資料庫**，那是 Redis + Queue 要扛的事。

**具體流程：**

實務上流程可以想成：

1. API 收到請求 → 加入佇列，立刻回「排隊中」。
2. 佇列 worker 從 Queue 拿出請求，先用 Redis 原子扣庫存。
3. 扣成功後，再用樂觀鎖更新 DB，確保資料正確。
4. 最後在背景處理付款、寄信等後續動作。

---

## 實戰檢查清單

真的要把搶購系統丟上線，可以先簡單檢查幾個方向就好。庫存這邊，記得幫 Redis 開好持久化機制，偶爾把數字對回資料庫，賣完要能立刻停掉入隊，避免還在收單。佇列這邊，設一個合理的長度上限，有失敗重試和告警機制，避免佇列爆掉都沒人發現。資料一致性的部分，可以規劃定期對帳、重試策略，以及退貨、異常時要怎麼補償。最後是使用者體驗，給使用者一個「排隊中」或大概的排隊位置，有查詢訂單狀態的方式，真的沒搶到時，也有說明或補償方案，感受會好很多。

---

## 總結

| 技術 | 用途 | 優點 | 缺點 |
|------|------|------|------|
| **樂觀鎖** | 版本檢查，防止超賣 | 簡單、高效 | 衝突時需重試 |
| **Redis 原子性** | 快速扣庫存 | 超快、原子 | 需持久化、有延遲 |
| **佇列** | 削峰填谷、限流 | 穩定、容錯 | 使用者等待 |

**最佳實踐**：簡單講，就是「佇列先把請求排隊、Redis 負責快速扣庫存、資料庫用樂觀鎖把結果寫正確」，這三招一起用，通常就能撐過大部分的搶購活動。🚀

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/ecommerce-flash-sale-optimization)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
