首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> Oracle 資料庫 >> Oracle 資料庫基礎

PHP 性能優化利器:Oracle Database Change Notification 驅動的智慧快取失效機制

     編輯:Oracle 資料庫基礎

傳統快取困境:時間觸發 vs 資料驅動

快取資料庫查詢結果能大幅降低延遲與資料庫負載,但快取失效策略是核心難題:

  • TTL 時間觸發:簡單但會導致「資料已變更但快取未失效」或「資料未變但頻繁重建」
  • 主動輪詢:應用端定期檢查版本號/時間戳,增加資料庫查詢開銷
  • 理想模式:資料庫主動通知應用端「資料已變更,請失效快取」—— 即 Oracle Database Change Notification (DCN)

Oracle DCN 架構原理

  1. 應用註冊查詢(DBMS_CHANGE_NOTIFICATION.REG_START),指定回呼處理程序(PL/SQL 程序或 OCI Callback)
  2. 當註冊物件(表/視圖)發生 DML/DDL 變更且提交,資料庫非同步喚醒回呼程序
  3. 回呼程序接收變更描述(表名、操作類型、可選 ROWID),推送通知至應用端點(HTTP、佇列、檔案等)
  4. 應用端收到通知,精準失效對應快取項目

完整實作範例:PHP + PEAR::Cache_Lite + Oracle 10g R2 DCN

步驟 1:資料庫端授權與環境準備

-- SYS 權限授予
GRANT CHANGE NOTIFICATION TO oe;
GRANT EXECUTE ON DBMS_CHANGE_NOTIFICATION TO oe;
-- 確保 JOB_QUEUE_PROCESSES > 0
ALTER SYSTEM SET JOB_QUEUE_PROCESSES = 2;

步驟 2:建立通知記錄表

CONNECT oe/oe;
CREATE TABLE nfresults (
    operdate DATE,
    tblname VARCHAR2(60),
    rslt_msg VARCHAR2(100)
);

步驟 3:PL/SQL 通知發送程序(使用 UTL_HTTP 推送至 PHP 端點)

CREATE OR REPLACE PROCEDURE sendNotification(
    url IN VARCHAR2, tblname IN VARCHAR2, order_id IN VARCHAR2
) IS
    req UTL_HTTP.REQ;
    resp UTL_HTTP.RESP;
    err_msg VARCHAR2(100);
    tbl VARCHAR2(60);
BEGIN
    tbl := SUBSTR(tblname, INSTR(tblname, '.', 1, 1) + 1, 60);  -- 移除 schema 前綴
    BEGIN
        req := UTL_HTTP.BEGIN_REQUEST(url || order_id || '&table=' || tbl);
        resp := UTL_HTTP.GET_RESPONSE(req);
        INSERT INTO nfresults VALUES (SYSDATE, tblname, resp.reason_phrase);
        UTL_HTTP.END_RESPONSE(resp);
    EXCEPTION WHEN OTHERS THEN
        err_msg := SUBSTR(SQLERRM, 1, 100);
        INSERT INTO nfresults VALUES (SYSDATE, tblname, err_msg);
    END;
    COMMIT;
END;
/
-- 測試:EXEC sendNotification('https://webhost/phpcache/dropResults.php?order_no=', 'OE.ORDERS', '2408');

步驟 4:核心回呼程序 - 處理 ORDERS 表變更

CREATE OR REPLACE PROCEDURE orders_nf_callback(ntfnds IN SYS.CHNF$_DESC) IS
    tblname VARCHAR2(60);
    numtables NUMBER;
    event_type NUMBER;
    row_id VARCHAR2(20);
    numrows NUMBER;
    ord_id VARCHAR2(12);
    url VARCHAR2(256) := 'https://webhost/phpcache/dropResults.php?order_no=';
BEGIN
    event_type := ntfnds.event_type;
    numtables := ntfnds.numtables;
    
    IF event_type = DBMS_CHANGE_NOTIFICATION.EVENT_OBJCHANGE THEN
        FOR i IN 1..numtables LOOP
            tblname := ntfnds.table_desc_array(i).table_name;
            
            -- 僅處理行級變更(非 DDL/截斷)
            IF BITAND(ntfnds.table_desc_array(i).opflags, DBMS_CHANGE_NOTIFICATION.ALL_ROWS) = 0 THEN
                numrows := ntfnds.table_desc_array(i).numrows;
                
                IF tblname = 'OE.ORDERS' THEN
                    FOR j IN 1..numrows LOOP
                        row_id := ntfnds.table_desc_array(i).row_desc_array(j).row_id;
                        SELECT order_id INTO ord_id FROM orders WHERE ROWID = row_id;
                        sendNotification(url, tblname, ord_id);
                    END LOOP;
                END IF;
            END IF;
        END LOOP;
    END IF;
    COMMIT;
END;
/
-- 註冊查詢
DECLARE
    regds SYS.CHNF$_REG_INFO;
    regid NUMBER;
    ord_id NUMBER;
    qosflags NUMBER;
BEGIN
    qosflags := DBMS_CHANGE_NOTIFICATION.QOS_RELIABLE + DBMS_CHANGE_NOTIFICATION.QOS_ROWIDS;
    regds := SYS.CHNF$_REG_INFO('orders_nf_callback', qosflags, 0, 0, 0);
    regid := DBMS_CHANGE_NOTIFICATION.NEW_REG_START(regds);
    SELECT order_id INTO ord_id FROM orders WHERE ROWNUM < 2;  -- 觸發註冊的任意查詢
    DBMS_CHANGE_NOTIFICATION.REG_END;
END;
/
-- 測試:UPDATE orders SET order_mode='direct' WHERE order_id=2421; COMMIT;
-- 查詢 nfresults 驗證通知發送

步驟 5:擴充支援 ORDER_ITEMS 表(動態新增註冊物件)

-- 1. 查詢現有註冊 ID
SELECT regid, table_name FROM user_change_notification_regs;
-- 假設 regid=241

-- 2. 建立 ROW 描述陣列型別(供 CAST 使用)
CREATE TYPE rdesc_tab AS TABLE OF SYS.CHNF$_RDESC;

-- 3. 修改回呼程序,新增 ORDER_ITEMS 處理邏輯
-- 在 orders_nf_callback 中原有 ORDERS 處理迴圈後加入:
IF tblname = 'OE.ORDER_ITEMS' THEN
    FOR rec IN (
        SELECT DISTINCT o.order_id AS o_id
        FROM TABLE(CAST(ntfnds.table_desc_array(i).row_desc_array AS rdesc_tab)) t
        JOIN order_items d ON t.row_id = d.ROWID
        JOIN orders o ON d.order_id = o.order_id
    ) LOOP
        sendNotification(url, tblname, rec.o_id);
    END LOOP;
END IF;
-- 重新編譯程序後生效

-- 4. 將 ORDER_ITEMS 加入現有註冊
DECLARE
    ord_id NUMBER;
BEGIN
    DBMS_CHANGE_NOTIFICATION.ENABLE_REG(241);
    SELECT order_id INTO ord_id FROM order_items WHERE ROWNUM < 2;
    DBMS_CHANGE_NOTIFICATION.REG_END;
END;
/
-- 測試:UPDATE order_items SET quantity=160 WHERE order_id=2421; COMMIT;

步驟 6:PHP 端快取與通知處理

資料存取函數(快取目標)


// getOrderFields.php
function getOrderFields($order_no) {
    $conn = oci_connect('oe','oe','//dbhost:1521/orcl');
    $stid = oci_parse($conn, 'SELECT TO_CHAR(order_date) order_date, customer_id, order_total FROM orders WHERE order_id = :oid');
    oci_bind_by_name($stid, ':oid', $order_no, 12);
    oci_execute($stid);
    return oci_fetch_assoc($stid);  // 關聯陣列
}

// getOrderItems.php
function getOrderItems($order_no) {
    $conn = oci_connect('oe','oe','//dbhost:1521/orcl');
    $stid = oci_parse($conn, 'SELECT * FROM order_items WHERE order_id = :oid ORDER BY line_item_id');
    oci_bind_by_name($stid, ':oid', $order_no, 12);
    oci_execute($stid);
    $nrows = oci_fetch_all($stid, $results);
    return array($nrows, $results);
}

顯示頁面:使用 Cache_Lite_Function 透明快取


// testCache.php
require_once 'Cache/Lite/Function.php';
require_once 'getOrderFields.php';
require_once 'getOrderItems.php';

$cache = new Cache_Lite_Function(array('cacheDir'=>'/tmp/', 'lifeTime'=>86400));
$order_no = $_GET['order_no'];

// 訂單基本資料
if ($fields = $cache->call('getOrderFields', $order_no)) {
    echo "

ORDER #$order_no

"; foreach (['ORDER_DATE','CUSTOMER_ID','ORDER_TOTAL'] as $k) echo ""; echo "
$k:{$fields[$k]}
"; } else { $cache->drop('getOrderFields', $order_no); } // 訂單明細 if (list($nrows, $items) = $cache->call('getOrderItems', $order_no)) { echo ""; foreach ($items as $k=>$v) echo ""; echo ""; for ($i=0; $i<$nrows; $i++) { echo ""; foreach ($items as $col=>$vals) echo ""; echo ""; } echo "
$k
{$vals[$i]}
"; } else { $cache->drop('getOrderItems', $order_no); }

通知接收端點:精準失效快取


// dropResults.php
require_once 'Cache/Lite/Function.php';
$cache = new Cache_Lite_Function(array('cacheDir'=>'/tmp/'));

if (isset($_GET['order_no'], $_GET['table'])) {
    $order_no = $_GET['order_no'];
    if ($_GET['table'] === 'ORDERS') {
        $cache->drop('getOrderFields', $order_no);
        // 可選:立即重建 $cache->call('getOrderFields', $order_no);
    } elseif ($_GET['table'] === 'ORDER_ITEMS') {
        $cache->drop('getOrderItems', $order_no);
        // 可選:立即重建 $cache->call('getOrderItems', $order_no);
    }
}

運作流程驗證

  1. 瀏覽 testCache.php?order_no=2408 → 首次查詢資料庫,結果寫入 /tmp/cache_*
  2. 重新整理 → 讀取快取,無資料庫查詢(驗證:暫時改錯 DB 連線參數,頁面仍正常顯示)
  3. 資料庫執行 UPDATE orders ... WHERE order_id=2408; COMMIT;
  4. 資料庫 DCN 觸發 orders_nf_callback → 呼叫 sendNotification → HTTP GET dropResults.php?order_no=2408&table=ORDERS
  5. PHP 端 drop() 刪除對應快取檔案
  6. 再次瀏覽 testCache.php?order_no=2408 → 快取未命中 → 重新查詢資料庫 → 顯示最新資料

進階考量與生產環境建議

  • 可靠性:QOS_RELIABLE 確保通知不遺失(資料庫端持久化佇列),但需監控 USER_CHANGE_NOTIFICATION_REGS 狀態
  • 效能:回呼程序中避免耗時操作;大量變更考慮批次通知或非同步佇列(AQ/Streams)
  • 安全:通知端點驗證來源 IP、簽名;HTTPS 傳輸;最小權限資料庫帳號
  • 替代方案:Oracle 12c+ Continuous Query Notification (CQN) 增強易用性;或使用 GoldenGate / Kafka CDC 架構解耦
  • PHP 快取升級:生產建議 Redis/Memcached 替換檔案快取,支援分叢集、TTL、標籤式失效

總結

Oracle DCN 實現了資料庫驅動的精準快取失效,解決了傳統 TTL 快取的「資料不一致」與「資源浪費」雙重痛點。結合 PHP Cache_Lite(或 Redis),只需少量 PL/SQL 與 PHP 程式碼,即可構建高效能、強一致性的讀取加速層。此模式適用於讀多寫少、資料變更頻率不可預測、對一致性要求高的場景(如產品目錄、訂單查詢、配置中心)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved