在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中包含變化資料對應的RowIddep.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異常。