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;