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