在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異常。