5.4 服务器开发


  • 5.4.1 代码生成——xmlgen

    • 操作

      limax.jar包集成了代码生成工具。


      java –jar limax.jar xmlgen –java server.xml                                   
      

      按照server.xml描述的模型在当前路径下生成代码,详细使用帮助可以执行java –jar limax.jar xmlgen获得。

      一般来说,构建一个项目的xml描述使用类似如下的组织方式:

      example.share.xml


      <?xml version="1.0" encoding="utf-8"?> 
      <namespace name="share" pvid="100">
      <!--服务器客户端需要支持的bean,protocol,rpc,view描述
      rpc不适合应用项目使用,下面以bean,protocol,view作为例子--> 
      
          <bean name="MyBean">
              <enum name="e0" value="0"/>
              <enum name="e1" value="1"/>
              <variable name="var0" type="int"/>
          </bean> 
          <protocol name="MyProtocol" type="101" maxsize="4">
              <variable name="var0" type="MyBean"/>
          </protocol>
          <view name="MyGlobalView" lifecycle="global">
              <variable name="var0" type="MyCbean"/>
          </view>  
          <view name="MySessionView" lifecycle="session">
              <variable name="var0" type="example..MyXbean"/>
              <bind name="bind0" table="mytable"/>
              <bind name="bind1" table="mytable">
                  <ref name="var0"/>
              </bind>
              <control name="control">
                  <variable name="var0" type="int"/>
              </control>    
          </view> 
          <view name="MyTemporaryView" lifecycle="temporary">
              <variable name="var0" type="int"/>
              <subscribe name="_var0" ref="MySessionView.var0"/>
          </view>      
      </namespace>                                  
      

      example.zdb.xml


      <?xml version="1.0" encoding="utf-8"?> 
      <zdb>
      <!--zdb使用的cbean,xbean,table描述--> 
      
          <cbean name="MyCbean">
              <variable name="var0" type="int"/>
          </cbean> 
          <xbean name="MyXbean">
              <variable name="var0" type="int"/>
              <variable name="slist" type="vector" value="string"/>
          </xbean>
          <table name="mytable" key="long" value="MyXbean" autoIncrement="true"/>     
      </zdb>                                  
      

      example.server.xml


      <?xml version="1.0" encoding="utf-8"?> 
      <project name=”example” xmlns:xi="http://www.w3.org/2001/XInclude">
          <xi:include href="example.share.xml"/>
          <xi:include href="example.zdb.xml"/>
          <state name="Server"> 
              <namespace ref="share" />
          </state>
          <service name="ExampleServer" useGlobalId="true" useZdb="true">
              <manager name="ExampleServer" type="provider" initstate="Server" port="10100"/>
          </service>         
      </project>                                  
      

      example.client.xml


      <?xml version="1.0" encoding="utf-8"?> 
      <project name="example" xmlns:xi="http://www.w3.org/2001/XInclude">
          <xi:include href="example.share.xml"/>
          <xi:include href="example.zdb.xml"/>
          <state name="Client"> 
              <namespace ref="share" />
          </state>
          <service name="ExampleClient">
              <manager name="ExampleClient" type="client" initstate="Client" port="10000"/>
          </service>         
      </project>                                  
      

      通过xml的XInclude组织描述文件,划分出服务器,客户端的共享部分和zdb部分。项目比较复杂的情况下,应该考虑从功能模块的角度使用XInclude作更合理的细分。

      必要的时候,服务器xml与客户端xml允许合并成一个,xmlgen加入-service参数可以指定只生成某一个service的代码。不推荐这样使用。

      脚本环境的客户端不需要依据客户端xml生成代码,应该通过指定xmlgen参数在生成服务器的同时生成对应脚本语言的代码模板。

      按照xml描述生成的服务器源代码,存放在当前目录下的两个子目录内,src目录与gen目录。src目录为源码目录,下面的文件按照应用需求修改编辑,这个目录应该提交到版本控制系统内;gen目录不需要作版本控制,每次执行xmlgen都会重建,任何修改都会丢失。另外,当前目录下生成了service-XXX.xml文件,XXX为xml描述中的service name,运行服务器需要的配置生成在这里,部署到具体运行环境的时候可以按照运营需求适当调整。一个描述文件下有多个service的情况下,service-XXX.xml生成多个。

      新建eclipse Java项目,Location指定当前目录。

      Package Explorer中,右键编辑项目属性

      Java Builder Path – Source下,加入src与gen目录

      Java Builder Path – Projects 下,加入对limax的引用,

      或者Java Builder Path – Libraries下,加入limax.jar。

      这时就能清晰看见项目组织结构。


    • 名字空间映射

      把xml描述中的名字空间映射到java名字空间下。

      protocol, rpc, view命名规则:

      项目名,service名, (protocol, rpc, view)所在的名字空间名, (protocol, rpc, view)名。

      例如,上面定义的MyGlobalView被命名为example.ExampleServer.share.MyGlobalView.

      特别的,gen目录下能看到 example.ExampleServer.states 这样一个名字空间,这个名字空间下放置了service通过manager节点引用的所有state节点相关的生成代码。为了避免混乱,项目的最外层名字空间不允许命名为states,否则生成阶段报错。

      bean,monitorset命名规则:

      项目名,(bean,monitorset)所在的名字空间名,(bean,monitorset)名。

      例如,上面定义的MyBean,被命名为example.share.MyBean

      对于bean而言,bean与protocol, rpc, view一同定义在xml的namespace节点下,还是为之采用不同命名规则的原因在于bean可以被多个服务中的protocol,rpc,view引用,这种方案可以减少重复代码的生成。

      对于monitorset而言,这种命名方式容易映射到期望的jmx domain。

      cbean,xbean,table命名规则:

      cbean放置在cbean名字空间下。

      xbean放置在xbean名字空间下。

      table放置在table名字空间下,table的名字被修订为首字母大写。

      使用cbean,xbean与table时,不要import这些名字空间,应该直接使用xbean.MyXbean,table.Mytable,这样就能清晰看出代码使用了zdb的某个元件,便于维护。


  • 5.4.2 View

    代码生成以后,只需要编辑src目录下的几个文件,MyProtocol不作讨论。

    观察MyGlobalView,MySessionView,MyTemporaryView三个段代码。

    三段代码分别继承自gen目录下的_MyGlobalView, _MySessionView, _MyTemporaryView,父类里的公有方法,即包含了操作view所需要的代码。

    • View对象的获得与维护

      获取全局View


      MyGlobalView gview = MyGlobalView.getInstance();                                 
      

      全局View全局一个,获取不需要参数。


    • 手工同步全局View


      gview.syncToClient(sessionid);                                 
      

      同步全局view到客户端


      gview.syncToClient(sessionid, "var0");                                
      

      同步全局View的var0字段到客户端


      gview.syncToClient(Arrays.asList(sessionid1, sessionid2));                                
      

      广播全局view到指定的客户端集


      gview.syncToClient(Arrays.asList(sessionid1, sessionid2), "var0");                               
      

      广播全局view得var0字段到指定的客户端集/p>

      获取会话View


      MySessionView sview = MySessionView.getInstance(sessionid);                               
      

      这里需要sessionid参数,因为会话View在会话建立时以sessionid为key分类自动创建。


      创建临时View


      MyTemporaryView tview = MyTemporaryView.createInstance();                              
      

      销毁临时View


      tview.destroyInstance();                            
      

      获取临时View


      MyTemporaryView tview = MyTemporaryView.getInstance(sessionid, instanceid);                           
      

      每个临时View维护了一个Membership,该Membership中的sessionid,才能用作这里的sessionid参数,临时View创建以后 tview.getInstanceIndex(),可以获取第二个参数所需的instanceid。


      加入临时View


      tview.getMembership().add(sessionid);                          
      

      执行成功以后临时View代码中的onAttached方法被调用,见MyTemporaryView的生成代码,这里应该填写加入成功后应该执行的动作。

      执行失败以后临时View代码中的onAttachAbort方法被调用,见MyTemporaryView的生成代码,AbortReason指出了失败原因,比如加入sessionid过程中,sessionid对应用户离线了。


      离开临时View


      tview.getMembership().remove(sessionid, reason);                         
      

      执行成功以后临时View代码中的onDetached方法被调用,见MyTemporaryView的生成代码,reason将传递给onDetached; reason必须为非负值,负值内部使用,指出系统原因,比如整个view关闭,导致了用户detach。

      执行失败以后临时View代码中的onDetachAbort方法被调用。

      这里要明确, onDetachAbort是对Membership.remove动作失败的响应, 销毁临时View导致的离开,系统以负值调用onDetached。如果销毁临时View的动作出现在Membership.remove之前, 会出现这样的状况,首先onDetached以负值reason被调用,然后onDetachAbort以VIEWCLOSED为reason被调用,如果需要精确处理离开的行为,这种情况应该要考虑到。


    • View对象上的操作

      View对象上3种操作,分别与xml描述中view节点下三个子节点variable, bind, control一一对应。

      variable上的操作


      <variable name="var0" type="int"/>                        
      

      生成 setVar0(Integer),_setVar0(Integer)

      提供两个版本set方法与事务支持相关。事务环境下,使用set版本,事务成功后set动作被真正执行,事务失败set动作不执行,_set版本set动作立即执行与事务成功失败无关;非事务环境下,两个版本行为一致。以null为参数执行set,意味着删除该字段。


      bind上的操作


      <table name="mytable" key="long" value="MyXbean" />
      <bind name="bind0" table="mytable" />                        
      

      生成 bindMytable(long) 方法,这里的参数类型long即是mytable的key的类型long。

      例如,view.bindMytable(100);执行以后,一旦mytable表的key=100这一行的数据,也就是MyXbean类型的xbean在事务成功以后检测到改变,改变的数据自动设置到view的bind0字段上。


      control上的操作


      <control name="control">
          <variable name="var0" type="int" />
      </control>                            
      

      生成如下方法


      protected static final class control implements limax.codec.Marshal, Comparable<control> { 
          public int var0;
          ......
      }
      protected void onControl(_MySessionView.control param, long sessionid) { }                          
      

      需要在onControl内填写control的响应代码。

      特别的,每个View的生成代码中,均有


      protected void onMessage(String message, long sessionid) { }                          
      

      填写消息处理代码,脚本客户端的情况下,脚本系统难以有效构造类型化数据,只能使用默认的onMessage传递字符串消息,应用应该定义自己的串数据格式规范,服务器端按规范解析message字符串。


  • 5.4.3 ZDB

    • 存储过程

      zdb通过存储过程实现数据库的事务支持,需要使用的包是limax.zdb.Procedure,过程返回true事务提交,返回false事务回滚。有3种基本执行方式,execute,submit,call。

      execute:异步执行存储过程


      Procedure.execute(()->{
          过程代码
          返回true或者false
      });                          
      

      上面的方法不关心返回结果,是最简单常用的方法。


      Procedure.execute(()->{
          过程代码
          返回true或者false
      }, (procedure, result)->{
          if (!result.isSuccess()) {
              System.err.println("Procedure " + procedure + "  fail");
              if(result.getException() != null) 
                  result.getException().printStackTrace(System.err);
              else
                  System.err.println(“procedure return false;”);
          }
      });
      

      上面的方法通过第二个lambda表达式参数获得过程的返回值,返回值提供了成功失败的信息,如果失败可以检测是否由异常导致。

      submit: 异步执行存储过程,返回 Future<Procedure.Result>


      Future<Procedure.Result>future = Procedure.submit(()->{
          过程代码
          返回true或者false
      });                          
      

      可以通过future.get(),等待过程结束,通过get可以获得执行结果。

      call: 同步执行存储过程,返回Procedure.Result.


      Result result = Procedure.call(()->{
          过程代码
          返回true或者false
      });                          
      

      这里需要特别注意,submit不允许在存储过程内调用,否则抛出IllegalStateException异常。存储过程嵌套调用,必须使用call,存储过程的嵌套导致事务嵌套,内层事务失败回滚不影响外层事务;存储过程内部使用execute将启动新的过程,不能实现嵌套。


    • 异常规范

      Zdb框架划分两类异常:

      用户异常,派生自java.lang.Exception

      框架异常XError,派生自java.lang.Error,由框架和生成代码使用。

      用户在实现存储过程时允许抛出或者捕获用户异常。禁止捕获Error,Throwable。

      存储过程执行时,抛出用户异常,导致当前事务回滚,效果如同在过程中return false,并且将异常设置到过程返回结果中。

      存储过程执行时,抛出XError,导致事务回滚到最外层,直接设置最外层过程的返回结果。

      submit方式下,如果存储过程执行时抛出异常至最外层,应该捕获java.util.concurrent. ExecutionException e,e.getCause(),获取该异常。

      正确设置过程的日志级别,可以将框架捕获到的异常记录下来。


    • 标准表操作

      table的value有两种类型,一是xbean类型,二是常量类型(cbean以及除了binary之外的简单类型),这两种类的操作稍有区别,分别介绍。

      value 为xbean类型的表操作:


      xbean = table.Tablename.insert(key);                          
      

      根据key插入一条记录,记录存在插入失败,返回null,否则返回xbean,这之后可以填写xbean内容,提交以后内容被加入数据库。


      key = table.Tablename.newKey();                          
      

      允许自增量的表,可以用这个操作获取新的key,再作插入操作。


      pair = table.Tablename.insert();                          
      

      允许自增量的表支持该操作,功能相当于able. Tablename.insert(table.Tablename.newKey());,返回的pair为limax.util.Pair类型,pair.getKey(),获取key,pair.getValue()获取xbean,接下来填写xbean内容。除非自增量配置在使用以后,又进行了错误的重配置,返回的xbean不可能为null。


      xbean = table.Tablename.update(key);                          
      

      获取与key关联的xbean记录,获得的xbean进行修改以后,事务提交后更新到数据库。


      xbean = table.Tablename.select(key);                          
      

      获取与key关联的xbean记录,获得的xbean只能读,一旦修改将抛出异常导致事务失败。


      result = table.Tablename.delete(key);                          
      

      删除与key关联的记录,返回类型为boolean,成功true,失败false。

      value为常量类型的表操作:(以cbean为例)


      cbean = table.Tablename.insert(key, cbean);                         
      

      除非记录存在,返回原来的cbean,否则null。


      pair = table.Tablename.insert(cbean);                         
      

      除了多个cbean参数与value为xbean的自增量表类似。

      update操作返回常量本身,但是无法修改。与select的差别在于update获取了写锁,而select只能获取读锁。

      select,remove操作与value为xbean的情况相同。


    • 非标准表操作


      table.Tablename.get().getCache().walk((key, value) -> {
          //readonly action
      });                        
      

      遍历表cache,遍历过程逐个读锁定记录,提供的记录只读。既可用于磁盘表也可用于内存表。


      table.Tablename.get().walk((key, value) -> {
          //readonly action
      });                        
      

      遍历低层数据库内容,内存数据库中未checkpoint的记录不会访问到,zdb周期性将提交数据checkpoint到低层。虚拟机参数limax.zdb.Checkpoint.SCHED_PERIOD控制checkpoint检测的最小周期,默认100ms,具体的Checkpoint周期由运行时的xml参数配置。

      因为看到的低层数据库内容,所以与zdb锁无关系。这种walk只能用于磁盘表。

      对于这2种walk,结果的顺序不保证,不需要在事务上下文中执行。


    • 事务隔离度

      默认隔离度为level2,非常必要的情况下可以使用level3,这种情况下事务被串行执行,效率低。


      Transaction.setIsolationLevel(Transaction.Isolation.LEVEL3);                       
      

      将当前线程事务隔离度设置为level3,也就是说,凡是通过当前线程启动的事务,均拥有level3隔离度。


      Transaction.setIsolationLevel(Transaction.Isolation.LEVEL2);                      
      

      隔离度修改回level2。


    • zdb的锁为行锁,默认支持隔离度level2,实现holdlock,确保可重复读,直到事务结束。select操作获得读锁,insert,update,delete操作获得写锁。执行select之后,如果在同一key上update,先前的读锁被释放,再进行写锁定,这意味着读锁被升级为写锁;反过来锁不会被降级,原因在于:修改记录后如果把锁降级为读锁,那么别的事务就有可能读取到刚修改过的数据,事务隔离度就降级为level0了,发生了脏读,往往容易造成逻辑错误。这意味着,


      xbean = table.Tablename.select(key);
      table.Tablename.update(key);                      
      

      这一序列执行完成以后,xbean允许修改,即便再次table.Tablename.select(key),该xbean还是能够修改。经验上,判断一个key不存在再插入这一典型事务与关系数据上的操作非常类似:


      if (table.Tablename.select(key) == null)
          table.Tablename.insert(key);                      
      

      往往被认为是一个不好的操作,因为有可能在select读锁释放以后insert写锁定之前,另外的事务用这个key执行了insert,导致这里的insert失败,直接违反了上面语句序列的初衷。合理的写法应该是:


      xbean = table.Tablename.update(key)
      if(xbean == null)
          table.Tablename.insert(key);                      
      

      复杂的事务可能导致死锁,zdb支持死锁检测。死锁检测周期,重试次数,重试退避的最大时间均可配置。死锁检测周期至少1秒。死锁被作为异常记录到过程返回结果中,可以通过日志配置记录下来。乐观模式下的设计,不需要考虑锁问题。需要解决的情况下检查日志,发现死锁热点,再考虑使用显式的预先锁定解决。

      通过预先一次性锁定事务将涉及到的表的行,可以有效解决死锁问题。需要使用的包是limax.zdb.Transaction.LockContext

      例如:

      同时写锁定表ta,tb的行row1,row2,可以写为:


      Transaction.getLockContext().wAdd(row1, row2, table.Ta.get(), table.Tb.get()).lock();                      
      

      同时写锁定表ta,tb的行row1,row2以及读锁定表tc的行row3,可以写为:


      Transaction.getLockContext().wAdd(row1, row2, table.Ta.get(),table.Tb.get()).rAdd(row3, table.Tc.get()).lock();                     
      

      一个add操作请求锁定,参数中所有列举出来的表的所有列举出来的行,有非常大的灵活性,可以一一列出,也可以通过容器给出,顺序也没有限制。

      例如:


      wAdd(table.Ta.get(),table.Tb.get(), row1, row2);
      wAdd(new Object[]{row1, row2}, new Object[]{table.Ta.get(),table.Tb.get()});
      wAdd(new Object[]{table.Ta.get(),table.Tb.get(), row1, row2});
      wAdd(Arrays.asList(table.Ta.get(), row1), Arrays.asList(table.Tb.get(), row2));                     
      

      均是等价的。

      实际上,容器(Collection)类型或者数组类型的参数值,被全部递归解析出来,区分出表类型对象构造锁定的表集,非表类型对象构造行集。


    • xbean访问

      Xbean只能在事务环境下访问,确保访问时拥有相应的锁,避免并发冲突,生成不正确的序列化数据,使得错误蔓延到下次数据库读取,影响服务正常运行。实现上,Zdb运行配置中,属性zdbVerify,控制相应的锁检测,默认zdbVerify=true,每次Xbean访问均检测锁有效性,如果访问缺少锁,则抛出limax.zdb.XLockLackedError。除非服务器经过严格的覆盖测试,不要为了少量的性能提升将zdbVerify设置为false,特别是闭包的使用很容易将Xbean带出锁范围之外。只读操作获得的xbean比如通过select,walk表cache,只能进行读访问,写访问将抛出limax.zdb.XLockLackedError。需要定期检查服务器运行日志,一旦发现limax.zdb.XLockLackedError,则根据对应的栈信息修改代码,确保并发安全性。


      <xbean name="MyXbean">
          <variable name="var0" type="int"/>
          <variable name="slist" type="vector" value="string"/>
      </xbean> 
      
      MyXbean xbean = new xbean.MyXbean ();                   
      

      xbean对象使用get,set存取简单类型数据。


      int x = xbean.getVar0();
      xbean.setVar0(100);                  
      

      xbean对象访问容器类型时,直接get获得容器进行修改


      xbean.getSlist().add("abc");
      xbean.getSlist().remove(0);                  
      

      Xbean在组织结构上,把记录看作根节点,层层连接起来,形成一棵树。作为一棵树上节点的xbean不能连接到另一棵树上

      例如:


      Xbean1 x1 = table.Table1.select(key1);
      Xbean2 x2 = table.Table2.update(key2);
      x2.getArray().add(x1); //这里假设Xbean2的array字段定义为Xbean1的vector                  
      

      是禁止的,这种情况下将抛出异常报告Xbean管理错误,结束事务。上面的例子应该通过拷贝数据解决问题,例如:


      x2.getArray().add(new Xbean1(x1))                 
      

  • 5.4.4 Monitor

    这里简单解释Monitor的使用。

    limax.xml定义了TransactionMonitor,于是生成了代码limax.zdb.TransactionMonitor,提供了6个方法:


    public static void increment_runned(String procedureName);
    public static void increment_runned(String procedureName, long _delta_);
    public static void increment_false(String procedureName);
    public static void increment_false(String procedureName, long _delta_);
    public static void increment_exception(String procedureName);
    public static void increment_exception(String procedureName, long _delta_);                 
    

    两个方法提供给运行环境下的采集应用使用。


    public static String buildObjectNameQueryString(String procedureName);
    public interface Collector;                
    

上一页 下一页