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

     

     

     

  • 相关阅读:
    浮点数小数点后开始非零数字的起始位置
    关于接口测试
    性能测试模型之曲线拐点模型
    2018春招实习笔试面试总结(PHP)
    mysql删除表中的记录
    浅析单点登录
    MySQL两种引擎的比较
    Redis初探(windows/linux安装)
    剑指offer试题(PHP篇三)
    剑指offer试题(PHP篇二)
  • 原文地址:https://www.cnblogs.com/morvenhuang/p/2673831.html
Copyright © 2020-2023  润新知