ORACLE HANDBOOK系列之十四:變化通知(Change Notification)


在App開發的過程中,有些數據訪問頻率很高但是數據變化不大,我們一般會讓它駐留內存以提高訪問性能,但是此種機制存在一個問題,那就是如何監測數據的變化,Oracle 10g中引入的 Change Notification的引入能很好的解決這個問題。簡單來說,Change Notification即Oracle可以在你指定的表數據發生變化時,給出一個通知。我們結合ODP.NET作一個示例。首先創建一張示例表tab_cn,並插入數據,我們希望在數據發生變化時,App能夠收到通知。

create table tab_cn(id number, val number);

insert into tab_cn values(1,100);
insert into tab_cn values(2,200);
insert into tab_cn values(3,300);
commit;

SQL> select t.*, rowid from morven.tab_cn t;

        ID        VAL ROWID
---------- ---------- ------------------
         1       100  AAAarDAAKAADEmFAAA
         2        200 AAAarDAAKAADEmFAAB
         3        300 AAAarDAAKAADEmFAAC

除此之外,還要賦予數據庫用戶(本例中是morven)change notification權限:

grant change notification to morven;

下面則是相應的C#代碼(為簡單代碼,異常處理之類的就不貼出來了):

OracleDependency dep;
OracleConnection conn;
//
public MainWindow()
{
    InitializeComponent();
    //設置App的監聽端口,即使用哪個端口接收Change Notification。
    OracleDependency.Port = 49500;
    string cs = "User Id=morven;Password=tr;Data Source=mh";
    conn = new OracleConnection(cs);
    conn.Open();
}
//
private void btReg_Click(object sender, RoutedEventArgs e)
{
OracleCommand cmd = new OracleCommand("select * from tab_cn", conn);
//綁定OracleDependency實例與OracleCommand實例
dep = new OracleDependency(cmd);
//指定Notification是object-based還是query-based,前者表示表(本例中為tab_cn)中任意數據變化時都會發出Notification;后者提供更細粒度的Notification,例如可以在前面的sql語句中加上where子句,從而指定Notification只針對查詢結果里的數據,而不是全表。
dep.QueryBasedNotification = false;
//是否在Notification中包含變化數據對應的RowId
dep.RowidInfo = OracleRowidInfo.Include;
//指定收到Notification后的事件處理方法
    dep.OnChange += new OnChangeEventHandler(OnNotificaton);
    //是否在一次Notification后立即移除此次注冊
cmd.Notification.IsNotifiedOnce = false;
//此次注冊的超時時間(秒),超過此時間,注冊將被自動移除。0表示不超時。
cmd.Notification.Timeout = 0;
//False表示Notification將被存於內存中,True表示存於數據庫中,選擇True可以保證即便數據庫重啟之后,消息仍然不會丟失
    cmd.Notification.IsPersistent = true;
    //
    OracleDataReader odr = cmd.ExecuteReader();
    //
    this.rtb1.AppendText("Registration completed. " + DateTime.Now.ToLongTimeString() + Environment.NewLine);
}
 
private void btUnreg_Click(object sender, RoutedEventArgs e)
{
    //注銷
    dep.RemoveRegistration(conn);
    this.rtb1.AppendText("Registration Removed. " + DateTime.Now.ToLongTimeString() + Environment.NewLine);
}
 
private void OnNotificaton(object src, OracleNotificationEventArgs arg)
{
    //可以從arg.Details中獲得通知的具體信息,比如變化數據的RowId
    DataTable dt = arg.Details;
    //......
    this.rtb1.Dispatcher.BeginInvoke(
        DispatcherPriority.Normal,
        new Action(() =>
        {
            this.rtb1.AppendText("Notification Received. " + DateTime.Now.ToLongTimeString()+"  Changed data(rowid): "+arg.Details.Rows[0]["rowid"].ToString() + Environment.NewLine);
        }));
}

點擊此App的Register按鈕,然后在數據庫側通過下面語句更新tab_cn表:

Update tab_cn set val=1000 where id=1;
Commit;

此時App收到Notification,並能具體得到變化數據行所對應的RowId。隨后我們注銷此次注冊。輸出參見下圖:

Change Notification與Oracle Connection的關系

在實際測試中,無論我們是Connection.Close()還是在數據庫中手工Kill相應的Session或者是在OS層Kill相應的進程(線程),Notification仍然正常工作。

也就是說,除了初始化時,以及RemoveRegistration時依賴於相應的Connection,其它時候,它們並沒有依賴關系。

重復注冊

如果代碼有漏洞,就可能造成重復注冊的問題,此時在dba_change_notification_regs視圖中就能看到多條重復記錄(regid不同),曾經遇到過出現100000+記錄的情況。

上面的App中,如果我多次點擊Register按鈕,就會導致重復注冊,重復注冊的后果之一是,數據的一次改變,App會收到多條相同的通知。

重復注冊的另一個后果嚴重得多,會導致相應的表(本例中是tab_cn)更新之后的commit出現延時。當重復注冊10000時, update tab_cn表的一記錄后, commit花費一分鍾左右時間。同時也會影響數據庫shutdown或者startup的速度,因為這兩個動作都會發出notification(通知的內容為空)。

個人覺得Oracle應該從內部杜絕這種情況,因為重復注冊的意義何在實在有待商榷。下面我稍微修改代碼,嘗試避免重復注冊的問題。

if (dep == null || !dep.IsEnabled)
{
    OracleCommand cmd = new OracleCommand("select * from tab_cn", conn);
    dep = new OracleDependency(cmd);
    dep.QueryBasedNotification = false;
    dep.RowidInfo = OracleRowidInfo.Include;
    dep.OnChange += new OnChangeEventHandler(OnNotificaton);
    //
    cmd.Notification.IsNotifiedOnce = false;
    cmd.Notification.Timeout = 0;
    cmd.Notification.IsPersistent = true;
    //
    OracleDataReader odr = cmd.ExecuteReader();
    this.rtb1.AppendText("Registration completed. " + DateTime.Now.ToLongTimeString() + Environment.NewLine);
}

我在這里添加了一個判斷。首先是判斷OracleDependency實例是否為空(即第一次點擊Register按鈕),其次判斷OracleDependency.IsEnabled,此屬性在以下幾種情況時為False,1)已經初始化但command尚未執行、2)注冊時設置的Timeout到期、3)或者被RemoveRegistration注銷了,注意RemoveRegistration並不會導致OracleDependency實例Dispose。修改后的代碼只有在用戶第一次點擊Register或者之前點擊過Unregister的情況下,才允許注冊。

清除dba_change_notification_regs記錄

上面我們用了OracleDependency.RemoveRegistration方法來注銷某一個注冊,但是如果App還沒來得及注銷就崩潰退出,這種情況下沒有手工清除dba_change_notification_regs記錄的方法,不過正常情況下,當你更新相應的數據表(本例中的tab_cn)並commit后,Oracle會自動清除記錄,因為Oracle已經監測到這些注冊已經失效了,但是有時候並不會立即完全清除,遇到過有延時的,Oracle似乎是一批一批地清除。

多個App注冊同一端口

前面我們提到了,同一個App中,我們可以進行多次注冊,但對於不同的App,如果都向同一端口(本例中的49500)進行注冊,則會發生ORA-24912: Listener thread failed. Listen failed異常。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM