企業中資料庫是否應允許應用程式直接連接?

  1. 問題
  2. 支持直接資料庫存取的理由
  3. 反對直接資料庫存取的理由
  4. 企業解決方案:API 層
  5. 混合方法:何時混合使用
  6. 決策框架
  7. 最佳實踐
  8. 結論
  9. 資源

你的 CRM 需要客戶資料。你的分析儀表板需要相同的資料。你的行動應用程式也需要它。資料庫擁有一切。為什麼不讓它們全部直接連接?

這似乎合乎邏輯。但在企業架構中,這個簡單的決定可能會成就或破壞你系統的可擴展性、安全性和可維護性。

問題

在企業環境中,多個應用程式是否應該直接連接到資料庫?

簡短回答:視情況而定——但通常不應該。

詳細回答:讓我們探討兩方面。

支持直接資料庫存取的理由

優勢

1. 簡單性

flowchart LR App1["📱 行動應用程式"] --> DB[("🗄️ 資料庫")] App2["💻 Web 應用程式"] --> DB App3["📊 分析"] --> DB style DB fill:#e3f2fd
  • 更少的移動部件
  • 無需維護中介軟體
  • 直接的開發
  • 快速原型製作

2. 效能

直接連接消除了中間層:

直接:應用程式 → 資料庫(1 跳)
API 層:應用程式 → API → 資料庫(2 跳)
  • 更低的延遲
  • 更少的網路呼叫
  • 無序列化開銷

3. 即時資料

應用程式總是看到最新的資料:

  • 無快取失效問題
  • 無同步延遲
  • 立即一致性

4. 開發速度

開發人員可以:

  • 查詢他們需要的確切內容
  • 快速迭代
  • 直接使用資料庫功能(預存程序、觸發器)

何時有意義

小型組織:

  • 2-3 個應用程式
  • 單一開發團隊
  • 低流量
  • 預算緊張

內部工具:

  • 管理儀表板
  • 報表工具
  • 資料分析腳本
  • 一次性工具

原型:

  • MVP 開發
  • 概念驗證
  • 快速實驗

反對直接資料庫存取的理由

問題

1. 安全噩夢

**問題:**每個應用程式都需要資料庫憑證。

flowchart TD subgraph "安全風險" App1["📱 行動應用程式
(程式碼中的資料庫憑證)"] App2["💻 Web 應用程式
(配置中的資料庫憑證)"] App3["📊 分析
(暴露的資料庫憑證)"] App4["🔧 管理工具
(完整資料庫存取)"] end App1 --> DB[("🗄️ 資料庫
⚠️ 單點妥協")] App2 --> DB App3 --> DB App4 --> DB style DB fill:#ffebee

風險:

  • **憑證擴散:**多個程式碼庫中的密碼
  • **行動應用程式:**可以從 APK/IPA 提取憑證
  • **第三方存取:**難以撤銷特定應用程式存取
  • **稽核噩夢:**無法追蹤哪個應用程式進行了哪個查詢

真實世界範例:

行動應用程式反編譯 → 提取資料庫密碼
→ 攻擊者擁有完整資料庫存取權限
→ 所有客戶資料被洩露

2. 緊密耦合

**問題:**應用程式直接依賴資料庫架構。

架構變更影響:

-- 重新命名欄位
ALTER TABLE users RENAME COLUMN email TO email_address;

結果:

  • ❌ 行動應用程式中斷
  • ❌ Web 應用程式中斷
  • ❌ 分析中斷
  • ❌ 管理工具中斷
  • ❌ 所有都需要同時更新

部署噩夢:

資料庫遷移 → 必須同時部署所有應用程式
→ 需要協調停機時間
→ 失敗風險高

3. 無業務邏輯層

**問題:**業務規則分散在各個應用程式中。

範例:折扣計算

行動應用程式:10% 折扣邏輯
Web 應用程式:15% 折扣邏輯(過時)
分析:無折扣邏輯(錯誤報表)

後果:

  • 不一致的行為
  • 重複的程式碼
  • 難以維護
  • 難以稽核

預存程序如何?

有些人認為:「將業務邏輯放在預存程序中——問題解決!」

預存程序方法:

-- 資料庫中的集中式折扣邏輯
CREATE PROCEDURE calculate_order_total(
  IN user_id INT,
  IN order_id INT,
  OUT final_total DECIMAL(10,2)
)
BEGIN
  DECLARE base_total DECIMAL(10,2);
  DECLARE discount DECIMAL(10,2);
  DECLARE is_premium BOOLEAN;
  
  SELECT total INTO base_total FROM orders WHERE id = order_id;
  SELECT premium INTO is_premium FROM users WHERE id = user_id;
  
  IF is_premium THEN
    SET discount = base_total * 0.15;
  ELSEIF base_total > 100 THEN
    SET discount = base_total * 0.10;
  ELSE
    SET discount = 0;
  END IF;
  
  SET final_total = base_total - discount;
END;

優勢:

  • ✅ 邏輯集中在一個地方
  • ✅ 所有應用程式使用相同的計算
  • ✅ 保證一致的行為
  • ✅ 效能(在資料附近執行)

但嚴重的缺點:

1. 有限的語言功能:

-- SQL/PL-SQL 不是為複雜邏輯設計的
-- 沒有現代語言功能:
-- - 無依賴注入
-- - 有限的錯誤處理
-- - 無單元測試框架
-- - 無 IDE 支援(與 Java/Python/Node.js 相比)

2. 難以測試:

// 應用程式程式碼 - 易於測試
function calculateDiscount(user, order) {
  if (user.isPremium) return order.total * 0.15;
  return order.total > 100 ? order.total * 0.10 : 0;
}

// 單元測試
test('premium user gets 15% discount', () => {
  const user = { isPremium: true };
  const order = { total: 100 };
  expect(calculateDiscount(user, order)).toBe(15);
});
-- 預存程序 - 難以測試
-- 需要資料庫連接
-- 需要測試資料設置
-- 測試執行緩慢
-- 無模擬/存根

3. 供應商鎖定:

Oracle PL/SQL ≠ SQL Server T-SQL ≠ PostgreSQL PL/pgSQL

-- 遷移資料庫意味著重寫所有程序
-- 不同的語法、功能、限制

4. 部署複雜性:

應用程式部署:
- Git 提交 → CI/CD → 部署 → 回滾容易

預存程序部署:
- 手動 SQL 腳本
- 版本控制困難
- 回滾有風險
- 無法與應用程式程式碼原子部署

5. 有限的可觀察性:

// 應用程式程式碼 - 完整的可觀察性
function processOrder(order) {
  logger.info('Processing order', { orderId: order.id });
  const discount = calculateDiscount(order);
  logger.debug('Discount calculated', { discount });
  metrics.increment('orders.processed');
  return applyDiscount(order, discount);
}
-- 預存程序 - 有限的可觀察性
-- 難以添加日誌
-- 難以添加指標
-- 難以追蹤執行
-- 難以在生產環境中除錯

6. 團隊技能:

大多數開發人員知道:JavaScript、Python、Java、Go
較少開發人員知道:PL/SQL、T-SQL、PL/pgSQL

→ 更難招聘
→ 更難維護
→ 知識孤島

何時預存程序有意義:

資料密集型操作:

-- 批量資料處理
CREATE PROCEDURE archive_old_orders()
BEGIN
  INSERT INTO orders_archive 
  SELECT * FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
  
  DELETE FROM orders WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);
END;

效能關鍵查詢:

-- 複雜聚合在資料庫中更好
CREATE PROCEDURE get_sales_report(IN start_date DATE, IN end_date DATE)
BEGIN
  SELECT 
    DATE(created_at) as date,
    COUNT(*) as order_count,
    SUM(total) as revenue,
    AVG(total) as avg_order_value
  FROM orders
  WHERE created_at BETWEEN start_date AND end_date
  GROUP BY DATE(created_at);
END;

遺留系統:

  • 已經大量投資於預存程序
  • 遷移成本太高
  • 團隊在資料庫程式設計方面的專業知識

現代替代方案:精簡預存程序

-- 預存程序僅用於資料存取
CREATE PROCEDURE get_user_orders(IN user_id INT)
BEGIN
  SELECT * FROM orders WHERE user_id = user_id;
END;
// 應用程式中的業務邏輯
class OrderService {
  async calculateTotal(userId, orderId) {
    const orders = await db.call('get_user_orders', [userId]);
    const user = await db.call('get_user', [userId]);
    
    // 業務邏輯在這裡 - 可測試、可維護
    const discount = this.calculateDiscount(user, orders);
    return this.applyDiscount(orders, discount);
  }
}

預存程序的結論:

預存程序可以集中邏輯,但它們:

  • ❌ 不能解決直接存取問題
  • ❌ 創造新的維護挑戰
  • ❌ 限制技術選擇
  • ⚠️ 應謹慎用於資料密集型操作
  • ✅ 更好:將業務邏輯保留在應用程式層

4. 效能瓶頸

**問題:**資料庫變得不堪重負。

連接限制:

PostgreSQL 預設:100 個連接
MySQL 預設:151 個連接

10 個應用程式 × 每個 20 個連接 = 200 個連接
→ 資料庫拒絕新連接
→ 應用程式崩潰

查詢混亂:

應用程式 1:SELECT * FROM orders(全表掃描)
應用程式 2:跨 5 個表的複雜 JOIN
應用程式 3:未優化的查詢(缺少索引)
→ 資料庫 CPU 達到 100%
→ 所有應用程式變慢

5. 無存取控制

**問題:**應用程式擁有太多存取權限。

典型設置:

-- 所有應用程式使用相同的使用者
GRANT ALL PRIVILEGES ON database.* TO 'app_user'@'%';

風險:

  • 分析工具可以刪除資料
  • 行動應用程式可以刪除表
  • 無最小權限原則
  • 意外資料遺失

6. 難以監控

**問題:**無法追蹤應用程式行為。

你無法回答的問題:

  • 哪個應用程式導致慢查詢?
  • 哪個應用程式發出最多請求?
  • 哪個應用程式存取了敏感資料?
  • 哪個應用程式導致了中斷?

企業解決方案:API 層

架構模式

在資料庫前放置 API 層有兩種主要模式:

模式 1:單體 API 層

flowchart TD subgraph Apps["應用程式"] App1["📱 行動應用程式"] App2["💻 Web 應用程式"] App3["📊 分析"] end subgraph API["API 層"] Auth["🔐 身份驗證"] BL["⚙️ 業務邏輯"] Cache["💾 快取"] RateLimit["🚦 速率限制"] end Apps --> Auth Auth --> BL BL --> Cache Cache --> DB[("🗄️ 資料庫")] style API fill:#e8f5e9 style DB fill:#e3f2fd

特徵:

  • 單一 API 服務
  • 一個資料庫(或共享資料庫)
  • 集中式業務邏輯
  • 簡單開始

模式 2:微服務(每服務一個資料庫)

flowchart TD subgraph Apps["應用程式"] App1["📱 行動應用程式"] App2["💻 Web 應用程式"] end subgraph Gateway["API 閘道"] GW["🚪 閘道
(路由)"] end subgraph Services["微服務"] UserSvc["👤 使用者服務"] OrderSvc["📦 訂單服務"] ProductSvc["🏷️ 產品服務"] end subgraph Databases["資料庫"] UserDB[("👤 使用者資料庫")] OrderDB[("📦 訂單資料庫")] ProductDB[("🏷️ 產品資料庫")] end Apps --> GW GW --> UserSvc GW --> OrderSvc GW --> ProductSvc UserSvc --> UserDB OrderSvc --> OrderDB ProductSvc --> ProductDB style Gateway fill:#fff3e0 style Services fill:#e8f5e9 style Databases fill:#e3f2fd

特徵:

  • 多個獨立服務
  • 每個服務擁有自己的資料庫
  • 分散式業務邏輯
  • 複雜但可擴展

微服務模式:深入探討

核心原則:每服務一個資料庫

❌ 反模式:共享資料庫
使用者服務 ──┐
             ├──> 共享資料庫
訂單服務 ───┘

問題:
- 透過架構緊密耦合
- 無法獨立部署
- 架構變更破壞多個服務

✅ 模式:每服務一個資料庫
使用者服務 ──> 使用者資料庫
訂單服務 ──> 訂單資料庫

優勢:
- 鬆散耦合
- 獨立部署
- 技術多樣性

由於篇幅限制,我將繼續創建文件的其餘部分。

範例實作:

使用者服務:

// user-service/api.js
const express = require('express');
const app = express();

// 使用者服務擁有使用者資料庫
const userDB = require('./db/user-db');

app.get('/api/users/:id', async (req, res) => {
  const user = await userDB.findById(req.params.id);
  res.json(user);
});

app.post('/api/users', async (req, res) => {
  const user = await userDB.create(req.body);
  res.json(user);
});

app.listen(3001);

訂單服務:

// order-service/api.js
const express = require('express');
const app = express();

// 訂單服務擁有訂單資料庫
const orderDB = require('./db/order-db');

app.get('/api/orders/:id', async (req, res) => {
  const order = await orderDB.findById(req.params.id);
  
  // 需要使用者資料?呼叫使用者服務 API
  const user = await fetch(`http://user-service:3001/api/users/${order.userId}`);
  
  res.json({
    ...order,
    user: await user.json()
  });
});

app.post('/api/orders', async (req, res) => {
  const order = await orderDB.create(req.body);
  res.json(order);
});

app.listen(3002);

API 閘道:

// api-gateway/gateway.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();

// 路由到適當的服務
app.use('/api/users', createProxyMiddleware({ 
  target: 'http://user-service:3001',
  changeOrigin: true 
}));

app.use('/api/orders', createProxyMiddleware({ 
  target: 'http://order-service:3002',
  changeOrigin: true 
}));

app.use('/api/products', createProxyMiddleware({ 
  target: 'http://product-service:3003',
  changeOrigin: true 
}));

app.listen(8080);

微服務模式的優勢:

1. 獨立擴展:

使用者服務:2 個實例(低流量)
訂單服務:10 個實例(高流量)
產品服務:3 個實例(中等流量)

每個根據自己的需求擴展

2. 技術多樣性:

// 使用者服務 - Node.js + PostgreSQL
const { Pool } = require('pg');
const pool = new Pool({ database: 'users' });
# 訂單服務 - Python + MongoDB
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017/')
db = client['orders']
// 產品服務 - Java + MySQL
DataSource ds = new MysqlDataSource();
ds.setURL("jdbc:mysql://localhost:3306/products");

3. 獨立部署:

部署使用者服務 v2.0
→ 只有使用者服務重啟
→ 訂單服務繼續運行
→ 產品服務繼續運行
→ 無需協調部署

4. 故障隔離:

訂單服務崩潰
→ 使用者仍可登入(使用者服務)
→ 使用者可瀏覽產品(產品服務)
→ 只有訂購功能中斷
→ 部分系統可用性

微服務模式的挑戰:

1. 資料一致性:

**問題:**無分散式交易

// ❌ 無法跨服務執行此操作
BEGIN TRANSACTION;
  INSERT INTO users (id, name) VALUES (1, 'Alice');
  INSERT INTO orders (user_id, total) VALUES (1, 100);
COMMIT;

// 使用者服務和訂單服務有獨立的資料庫

解決方案:Saga 模式

// 基於編排的 saga
class OrderService {
  async createOrder(userId, items) {
    // 步驟 1:建立訂單
    const order = await orderDB.create({ userId, items, status: 'pending' });
    
    // 步驟 2:發布事件
    await eventBus.publish('OrderCreated', { orderId: order.id, userId, items });
    
    return order;
  }
  
  // 監聽來自其他服務的事件
  async onPaymentFailed(event) {
    // 補償交易
    await orderDB.update(event.orderId, { status: 'cancelled' });
  }
}

class PaymentService {
  async onOrderCreated(event) {
    try {
      await this.chargeCustomer(event.userId, event.total);
      await eventBus.publish('PaymentSucceeded', { orderId: event.orderId });
    } catch (error) {
      await eventBus.publish('PaymentFailed', { orderId: event.orderId });
    }
  }
}

2. 資料重複:

**問題:**服務需要來自其他服務的資料

// 訂單服務需要使用者電子郵件進行通知
// 但使用者服務擁有使用者資料

// ❌ 不好:每次訂單都查詢使用者服務
const order = await orderDB.findById(orderId);
const user = await fetch(`http://user-service/api/users/${order.userId}`);
await sendEmail(user.email, order);
// 慢,創造耦合

// ✅ 好:在訂單服務中快取使用者資料
const order = await orderDB.findById(orderId);
const userCache = await orderDB.getUserCache(order.userId);
await sendEmail(userCache.email, order);
// 快,但資料可能過時

解決方案:事件驅動的資料複製

// 使用者服務發布事件
class UserService {
  async updateUser(userId, data) {
    await userDB.update(userId, data);
    
    // 發布事件
    await eventBus.publish('UserUpdated', {
      userId,
      email: data.email,
      name: data.name
    });
  }
}

// 訂單服務監聽並快取
class OrderService {
  async onUserUpdated(event) {
    // 更新本地快取
    await orderDB.updateUserCache(event.userId, {
      email: event.email,
      name: event.name
    });
  }
}

3. 分散式查詢:

**問題:**無法跨服務 JOIN

-- ❌ 無法使用微服務執行此操作
SELECT 
  u.name,
  o.total,
  p.name as product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN products p ON o.product_id = p.id;

解決方案:API 組合或 CQRS

// API 組合:在 API 閘道中聚合
app.get('/api/order-details/:orderId', async (req, res) => {
  // 呼叫多個服務
  const [order, user, product] = await Promise.all([
    fetch(`http://order-service/api/orders/${req.params.orderId}`),
    fetch(`http://user-service/api/users/${order.userId}`),
    fetch(`http://product-service/api/products/${order.productId}`)
  ]);
  
  // 組合結果
  res.json({
    order: await order.json(),
    user: await user.json(),
    product: await product.json()
  });
});
// CQRS:獨立的讀取模型
class OrderReadModel {
  // 查詢的非正規化視圖
  async getOrderDetails(orderId) {
    // 讀取資料庫中的預先連接資料
    return await readDB.query(`
      SELECT * FROM order_details_view
      WHERE order_id = ?
    `, [orderId]);
  }
  
  // 由所有服務的事件更新
  async onOrderCreated(event) { /* 更新視圖 */ }
  async onUserUpdated(event) { /* 更新視圖 */ }
  async onProductUpdated(event) { /* 更新視圖 */ }
}

何時使用微服務模式:

大型組織:

  • 多個團隊(5+ 團隊)
  • 每個團隊擁有一個服務
  • 獨立發布週期

不同的擴展需求:

  • 某些功能高流量
  • 某些功能低流量
  • 需要獨立擴展

技術多樣性:

  • 不同的語言/框架
  • 不同的資料庫類型
  • 遺留系統整合

領域複雜性:

  • 清晰的界限上下文
  • 明確定義的服務邊界
  • 成熟的領域理解

何時不使用微服務:

小團隊:

  • < 5 個開發人員
  • 開銷太高
  • 單體更簡單

不清楚的邊界:

  • 領域理解不足
  • 服務經常變更
  • 大量跨服務呼叫

簡單應用程式:

  • CRUD 操作
  • 無複雜工作流程
  • 單體就足夠

新創公司/MVP:

  • 需要快速行動
  • 需求經常變更
  • 過早優化

遷移路徑:單體到微服務

階段 1:帶模組的單體

單體 API
├── 使用者模組
├── 訂單模組
└── 產品模組
     ↓
  單一資料庫

階段 2:提取第一個服務

單體 API ──> 共享資料庫
     ↓
使用者服務 ──> 使用者資料庫(新)

階段 3:提取更多服務

產品服務 ──> 產品資料庫
訂單服務 ──> 訂單資料庫
使用者服務 ──> 使用者資料庫

階段 4:淘汰單體

API 閘道
├── 產品服務 ──> 產品資料庫
├── 訂單服務 ──> 訂單資料庫
└── 使用者服務 ──> 使用者資料庫

最佳實踐:

  1. 從單體開始
  2. 當痛點出現時提取服務
  3. 使用 API 閘道進行路由
  4. 實作服務發現
  5. 使用事件驅動通訊
  6. 監控一切
  7. 自動化部署
  8. 為故障設計

單體 API 層的優勢

1. 安全性

集中式身份驗證:

行動應用程式 → API(JWT 令牌)
Web 應用程式 → API(OAuth)
分析 → API(API 金鑰)

API → 資料庫(單一安全連接)

優勢:

  • 應用程式中無資料庫憑證
  • 撤銷每個應用程式的存取
  • 稽核所有資料存取
  • 實作速率限制

範例:

// 行動應用程式 - 無資料庫憑證
const response = await fetch('https://api.example.com/users', {
  headers: { 'Authorization': 'Bearer ' + token }
});

2. 鬆散耦合

架構獨立性:

-- 資料庫變更
ALTER TABLE users RENAME COLUMN email TO email_address;

API 保持不變:

GET /api/users/123
{
  "email": "user@example.com"  // API 契約不變
}

結果:

  • ✅ 行動應用程式運作
  • ✅ Web 應用程式運作
  • ✅ 分析運作
  • ✅ 只有 API 程式碼更新

3. 業務邏輯集中化

單一真相來源:

// API 層 - 折扣邏輯在一個地方
function calculateDiscount(user, order) {
  if (user.isPremium) return order.total * 0.15;
  if (order.total > 100) return order.total * 0.10;
  return 0;
}

優勢:

  • 所有應用程式的一致行為
  • 易於更新規則
  • 單一測試位置
  • 稽核軌跡

4. 效能優化

連接池:

10 個應用程式 → API(10 個連接)
API → 資料庫(5 個池化連接)

而不是:10 個應用程式 × 20 = 200 個連接

快取:

// 快取頻繁查詢
app.get('/api/products', async (req, res) => {
  const cached = await redis.get('products');
  if (cached) return res.json(cached);
  
  const products = await db.query('SELECT * FROM products');
  await redis.set('products', products, 'EX', 300);
  return res.json(products);
});

優勢:

  • 減少資料庫負載
  • 更快的回應時間
  • 更好的資源利用

5. 細粒度存取控制

每個應用程式的權限:

// 行動應用程式 - 唯讀
if (app === 'mobile') {
  allowedOperations = ['READ'];
}

// 管理工具 - 完整存取
if (app === 'admin' && user.isAdmin) {
  allowedOperations = ['READ', 'WRITE', 'DELETE'];
}

// 分析 - 僅特定表
if (app === 'analytics') {
  allowedTables = ['orders', 'products'];
}

6. 全面監控

追蹤一切:

// 記錄所有 API 請求
app.use((req, res, next) => {
  logger.info({
    app: req.headers['x-app-name'],
    user: req.user.id,
    endpoint: req.path,
    method: req.method,
    duration: Date.now() - req.startTime
  });
});

洞察:

  • 哪個應用程式最慢?
  • 哪些端點最常使用?
  • 哪個應用程式導致錯誤?
  • 每個應用程式的使用模式

混合方法:何時混合使用

唯讀直接存取

**情境:**分析和報表工具需要複雜查詢。

flowchart LR subgraph Write["寫入操作"] App1["📱 行動應用程式"] App2["💻 Web 應用程式"] end subgraph Read["唯讀"] Analytics["📊 分析"] Reports["📈 報表"] end Write --> API["🔐 API 層"] API --> DB[("🗄️ 主資料庫")] DB -.->|複製| ReadDB[("📖 讀取副本")] Read --> ReadDB style API fill:#e8f5e9 style DB fill:#e3f2fd style ReadDB fill:#fff3e0

設置:

-- 分析的唯讀使用者
CREATE USER 'analytics'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT ON database.* TO 'analytics'@'%';

-- 連接到讀取副本
-- 對生產資料庫無影響

優勢:

  • 分析不會拖慢生產環境
  • 允許複雜查詢
  • 無寫入存取風險
  • 獨立監控

讀取副本 vs ETL:如何選擇?

對於分析工作負載,你有兩個主要選項:

選項 1:讀取副本(即時)

flowchart LR Prod[("🗄️ 生產資料庫")] -.->|"持續
複製"| Replica[("📖 讀取副本")] Analytics["📊 分析工具"] --> Replica style Prod fill:#e3f2fd style Replica fill:#fff3e0
-- 分析查詢在副本上執行
SELECT 
  DATE(created_at) as date,
  COUNT(*) as orders,
  SUM(total) as revenue
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY DATE(created_at);

特徵:

  • ⚡ 即時或近即時資料(秒級延遲)
  • 🔄 持續複製
  • 📊 與生產環境相同的架構
  • 🎯 直接 SQL 查詢

⚠️「近即時」現實檢查

**讀取副本並非真正即時。**總是存在複製延遲。

典型複製延遲:

  • **最佳情況:**100ms - 1 秒
  • **正常:**1-5 秒
  • **負載下:**10-60 秒
  • **網路問題:**數分鐘或更長

這意味著什麼:

12:00:00.000 - 客戶在生產環境下訂單

12:00:00.500 - 複製延遲(500ms) 12:00:00.500 - 訂單出現在讀取副本 12:00:00.600 - 分析儀表板查詢副本 結果:儀表板在訂單發生後 600ms 顯示
**真實世界情境:**
-- 生產環境:剛建立訂單
INSERT INTO orders (id, status) VALUES (12345, 'pending');

-- 讀取副本:2 秒後
SELECT * FROM orders WHERE id = 12345;
-- 返回:無結果(複製延遲)

-- 副本上 2 秒後
SELECT * FROM orders WHERE id = 12345;
-- 返回:找到訂單
**複製延遲何時造成問題:** 1. **客戶看到過時資料:**
使用者:「我剛下了訂單!」
儀表板:「找不到訂單」
使用者:「你的系統壞了!」
2. **不一致的視圖:**
行動應用程式(生產):100 個訂單
儀表板(副本):98 個訂單(落後 2 秒)
3. **基於舊資料的業務決策:**
經理:「我們只有 5 件庫存」
現實:0 件(最後 3 秒賣出 5 件)
經理:「來做促銷!」
結果:超賣
**監控複製延遲:**
-- PostgreSQL
SELECT 
  client_addr,
  state,
  sync_state,
  replay_lag,
  write_lag,
  flush_lag
FROM pg_stat_replication;

-- MySQL
SHOW SLAVE STATUS\G
-- 查看:Seconds_Behind_Master
**高延遲警報:**
# Prometheus 警報
- alert: HighReplicationLag
  expr: mysql_slave_lag_seconds > 10
  for: 2m
  annotations:
    summary: "複製延遲為 {{ $value }} 秒"
**儘管有延遲仍可接受的使用案例:** - ✅ 歷史報表(昨天的銷售) - ✅ 趨勢分析(最近 30 天) - ✅ 帶有「資料截至 X 秒前」免責聲明的儀表板 - ✅ 非關鍵指標 **不可接受的使用案例:** - ❌ 即時庫存檢查 - ❌ 詐欺檢測 - ❌ 面向客戶的「你的訂單」頁面 - ❌ 關鍵業務決策 **如果你需要真正的即時:** - 直接查詢生產資料庫(謹慎) - 使用變更資料捕獲(CDC)與串流 - 實作事件驅動架構 - 接受延遲並圍繞它設計

選項 2:ETL 到資料倉儲(批次)

flowchart LR Prod[("🗄️ 生產資料庫")] -->|"夜間
提取"| ETL["⚙️ ETL 流程"] ETL -->|"轉換
& 載入"| DW[("📊 資料倉儲")] Analytics["📊 分析工具"] --> DW style Prod fill:#e3f2fd style ETL fill:#fff3e0 style DW fill:#e8f5e9
# ETL 作業每晚執行
def etl_orders():
    # 從生產環境提取
    orders = prod_db.query("""
        SELECT * FROM orders 
        WHERE updated_at >= CURRENT_DATE - INTERVAL '1 day'
    """)
    
    # 轉換
    for order in orders:
        order['revenue'] = order['total'] - order['discount']
        order['profit_margin'] = calculate_margin(order)
    
    # 載入到倉儲
    warehouse.bulk_insert('fact_orders', orders)

特徵:

  • 🕐 排程更新(每小時/每天)
  • 🔄 批次處理
  • 🏗️ 轉換的架構(為分析優化)
  • 📈 預先聚合的資料

📅 批次處理:可預測的過時性

ETL 資料是故意過時的——這沒關係。

典型 ETL 排程:

  • **每小時:**資料是 0-60 分鐘舊
  • **每天:**資料是 0-24 小時舊
  • **每週:**資料是 0-7 天舊

範例時間軸:

週一 9:00 AM - 客戶下訂單

週一 11:59 PM - ETL 作業開始 週二 12:30 AM - ETL 作業完成 週二 8:00 AM - 分析師查看報表 資料年齡:約 23 小時舊
**為什麼批次對分析更好:**

1. **一致的快照:**
# ETL 捕獲時間點快照
# 所有資料來自同一時刻
snapshot_time = '2024-01-15 23:59:59'

orders = extract_orders(snapshot_time)
customers = extract_customers(snapshot_time)
products = extract_products(snapshot_time)

# 所有資料都是一致的
# 無查詢中途變更
2. **無查詢中途更新:**
讀取副本(即時):
開始查詢:100 個訂單
查詢中途:5 個新訂單到達
結束查詢:不一致的結果

資料倉儲(批次):
開始查詢:100 個訂單
查詢中途:無變更(靜態快照)
結束查詢:一致的結果
3. **為聚合優化:**
-- 倉儲中預先聚合
SELECT date, SUM(revenue) 
FROM daily_sales_summary  -- 已經求和
WHERE date >= '2024-01-01';
-- 10ms 返回

-- vs 讀取副本
SELECT DATE(created_at), SUM(total)
FROM orders  -- 必須掃描數百萬行
WHERE created_at >= '2024-01-01'
GROUP BY DATE(created_at);
-- 30 秒返回
**過時性可接受的情況:** - ✅ 月度/季度報表 - ✅ 年度比較 - ✅ 趨勢分析 - ✅ 高階主管儀表板 - ✅ 合規報表 **過時性不可接受的情況:** - ❌ 即時操作儀表板 - ❌ 即時警報 - ❌ 面向客戶的資料 - ❌ 詐欺檢測 **混合解決方案:Lambda 架構**
即時層(讀取副本):
- 最近 24 小時的資料
- 對最近資料的快速查詢
- 可接受延遲:秒

批次層(資料倉儲):
- 歷史資料(>24 小時)
- 複雜分析
- 可接受延遲:小時/天

服務層:
- 合併兩個視圖
- 最近 + 歷史
**範例實作:**
def get_sales_report(start_date, end_date):
    today = datetime.now().date()
    
    # 從倉儲獲取歷史資料
    if end_date < today:
        return warehouse.query(
            "SELECT * FROM sales_summary WHERE date BETWEEN ? AND ?",
            start_date, end_date
        )
    
    # 從副本獲取最近資料
    historical = warehouse.query(
        "SELECT * FROM sales_summary WHERE date BETWEEN ? AND ?",
        start_date, today - timedelta(days=1)
    )
    
    recent = replica.query(
        "SELECT * FROM orders WHERE date >= ?",
        today
    )
    
    return merge(historical, recent)

比較:

因素 讀取副本 ETL 到資料倉儲
資料新鮮度 即時(秒) 批次(小時/天)
查詢效能 取決於生產架構 為分析優化
架構 與生產相同 轉換(星型/雪花型)
對生產的影響 最小(獨立伺服器) 最小(排程離峰)
複雜度
成本 較低 較高
資料轉換 廣泛
歷史資料 受保留限制 無限
多個來源 單一資料庫 多個資料庫/API

何時使用讀取副本:

即時儀表板:

// 即時訂單監控
SELECT COUNT(*) as active_orders
FROM orders
WHERE status = 'processing'
AND created_at >= NOW() - INTERVAL 1 HOUR;

操作報表:

  • 當前庫存水平
  • 活躍使用者會話
  • 今天的銷售數字
  • 系統健康指標

簡單分析:

  • 單一資料來源
  • 無複雜轉換
  • 生產架構運作良好

預算限制:

  • 小團隊
  • 有限資源
  • 需要快速設置

何時使用 ETL/資料倉儲:

複雜分析:

-- 多維分析
SELECT 
  d.year, d.quarter, d.month,
  p.category, p.brand,
  c.country, c.region,
  SUM(f.revenue) as total_revenue,
  SUM(f.profit) as total_profit
FROM fact_sales f
JOIN dim_date d ON f.date_key = d.date_key
JOIN dim_product p ON f.product_key = p.product_key
JOIN dim_customer c ON f.customer_key = c.customer_key
GROUP BY d.year, d.quarter, d.month, p.category, p.brand, c.country, c.region;

多個資料來源:

# 組合來自多個系統的資料
def build_customer_360():
    # 從生產資料庫
    orders = extract_from_postgres()
    
    # 從 CRM API
    interactions = extract_from_salesforce()
    
    # 從支援系統
    tickets = extract_from_zendesk()
    
    # 組合並載入
    customer_360 = merge_data(orders, interactions, tickets)
    warehouse.load('customer_360', customer_360)

歷史分析:

  • 長期趨勢(數年資料)
  • 年度比較
  • 季節性模式
  • 留存群組

資料轉換需求:

  • 為效能進行非正規化
  • 業務邏輯計算
  • 資料品質修正
  • 聚合和彙總

合規/稽核:

  • 不可變的歷史記錄
  • 時間點快照
  • 稽核軌跡
  • 監管報告

混合方法:

許多企業同時使用兩者:

即時需求 → 讀取副本
  - 即時儀表板
  - 操作報表
  - 當前指標

分析需求 → 資料倉儲
  - 歷史分析
  - 複雜查詢
  - 多來源報表

範例架構:

flowchart TD Prod[("🗄️ 生產資料庫")] Prod -.->|"即時
複製"| Replica[("📖 讀取副本")] Prod -->|"夜間
ETL"| DW[("📊 資料倉儲")] Replica --> LiveDash["⚡ 即時儀表板"] DW --> Analytics["📈 分析平台"] DW --> BI["📊 BI 工具"] style Prod fill:#e3f2fd style Replica fill:#fff3e0 style DW fill:#e8f5e9

遷移路徑:

階段 1:從讀取副本開始

生產資料庫 → 讀取副本 → 分析

- 快速設置
- 立即價值
- 低複雜度

階段 2:隨著需求增長添加 ETL

生產資料庫 → 讀取副本 → 即時儀表板
            ↓
           ETL → 資料倉儲 → 複雜分析

- 保留即時用於操作需求
- 添加倉儲用於分析需求
- 兩全其美

成本比較:

讀取副本:

資料庫副本:$200/月
設置時間:1 天
維護:低

第一年總計:約 $2,400

資料倉儲 + ETL:

倉儲:$500/月
ETL 工具:$300/月
設置時間:2-4 週
維護:中高

第一年總計:約 $9,600 + 設置成本

決策框架:

從讀取副本開始,如果:
- 需要即時資料
- 單一資料來源
- 簡單查詢
- 小預算
- 需要快速成功

遷移到資料倉儲,當:
- 需要歷史分析(>1 年)
- 多個資料來源
- 複雜轉換
- 副本上的慢查詢
- 合規要求

架構抽象的資料庫視圖

**情境:**需要直接存取但想隱藏架構複雜性。

-- 建立簡化視圖
CREATE VIEW customer_summary AS
SELECT 
  c.id,
  c.name,
  c.email_address AS email,  -- 隱藏欄位重新命名
  COUNT(o.id) AS order_count,
  SUM(o.total) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id;

-- 僅授予視圖存取權限
GRANT SELECT ON customer_summary TO 'reporting_app'@'%';

優勢:

  • 隱藏架構變更
  • 簡化的資料模型
  • 預先連接的資料
  • 存取控制

決策框架

選擇直接存取的情況:

小規模:

  • < 5 個應用程式
  • < 1000 個使用者
  • 低流量

僅內部:

  • 無外部存取
  • 可信任環境
  • 單一團隊

唯讀:

  • 分析工具
  • 報表儀表板
  • 資料科學

原型製作:

  • MVP 階段
  • 概念驗證
  • 時間緊迫的展示

選擇 API 層的情況:

企業規模:

  • 5+ 個應用程式
  • 1000+ 個使用者
  • 高流量

外部存取:

  • 行動應用程式
  • 第三方整合
  • 公共 API

安全關鍵:

  • 客戶資料
  • 財務資訊
  • 醫療記錄

長期產品:

  • 生產系統
  • 多個團隊
  • 頻繁變更

最佳實踐

如果你必須使用直接存取

1. 使用讀取副本:

寫入應用程式 → API → 主資料庫
讀取應用程式 → 讀取副本

2. 為每個應用程式建立資料庫使用者:

CREATE USER 'mobile_app'@'%' IDENTIFIED BY 'password1';
CREATE USER 'web_app'@'%' IDENTIFIED BY 'password2';
CREATE USER 'analytics'@'%' IDENTIFIED BY 'password3';

3. 授予最小權限:

-- 行動應用程式 - 只需要使用者和訂單
GRANT SELECT ON database.users TO 'mobile_app'@'%';
GRANT SELECT ON database.orders TO 'mobile_app'@'%';

-- 分析 - 所有內容唯讀
GRANT SELECT ON database.* TO 'analytics'@'%';

4. 使用連接池:

// 限制每個應用程式的連接
const pool = mysql.createPool({
  host: 'database.example.com',
  user: 'mobile_app',
  password: process.env.DB_PASSWORD,
  database: 'production',
  connectionLimit: 5  // 每個應用程式的限制
});

5. 監控一切:

-- 啟用查詢日誌
SET GLOBAL general_log = 'ON';
SET GLOBAL log_output = 'TABLE';

-- 檢視慢查詢
SELECT * FROM mysql.slow_log 
WHERE user_host LIKE '%mobile_app%';

結論

直接資料庫存取很誘人——它簡單且快速。但在企業環境中,風險通常超過好處。

關鍵要點:

  • 直接存取適用於小型、內部、唯讀情境
  • API 層提供安全性、靈活性和控制
  • 緊密耦合是最大的長期成本
  • 為生產系統從 API 層開始
  • 如果有遺留直接存取,逐步遷移

真正的問題:

不是「我們能直接連接嗎?」而是「我們應該嗎?」

對於大多數企業,答案是:**建立 API 層。**當你需要時,未來的你會感謝你:

  • 變更資料庫架構
  • 添加新應用程式
  • 撤銷被洩露應用程式的存取
  • 擴展以處理更多流量
  • 除錯生產問題

在 API 層的前期投資在安全性、可維護性和可擴展性方面帶來回報。🏗️

資源

分享到