2.4.2 ZDB部分
-
大厅房间配置
<xbean name="ChatHallInfo"> <variable name="name" type="string" /> <variable name="rooms" type="list" value="long" /> </xbean> <table autoIncrement="true" name="chathalls" key="long" value="ChatHallInfo" /> <xbean name="ChatRoomInfo"> <variable name="name" type="string" /> <variable name="hallid" type="long" /> </xbean> <table autoIncrement="true" name="chatrooms" key="long" value="ChatRoomInfo" /> <table name="hallnamecache" key="string" value="long" persistence="MEMORY" />
从上面的描述可以看出:
1. chathalls表为自增量id表,描述了大厅配置,值字段xbean.ChatHallInfo有两个属性,大厅名字和大厅内roomid列表。
2. chatroom表为自增量id表,描述了房间配置,值字段xbean.ChatRoomInfo有两个属性,房间名字和所处的大厅的hallid。
3. hallnamecache表是内存表,建立大厅名到hallid的映射。
-
配置获取
SessionManager.java:
public void onManagerInitialized(Manager manager, Config config) { this.manager = (ServerManager) manager; table.Chathalls.get().walk((k, v) -> { Procedure.call(() -> { table.Hallnamecache.insert(v.getName(), k); return true; }); return true; }); if (table.Hallnamecache.get().getCacheSize() == 0) ......................... else { setViewHallsFromCache(); ......................... } }
首先通过在chathalls表上walk,以大厅名为key,hallid为value,初始化内存表hallnamecache。如果hallnamecache有数据,则调用setViewHallsFromCache进一步初始化大厅配置。
static void setViewHallsFromCache() { final ArrayList<RoomChatHallInfo> halls = new ArrayList<>(); table.Hallnamecache .get() .getCache() .walk((name, hid) -> { final Procedure.Result r = Procedure.call(ProcedureHelper.nameProcedure("setViewHallsFromCache", () -> { xbean.ChatHallInfo chi = table.Chathalls.select(hid); halls.add(new RoomChatHallInfo( chi.getName(), hid, chi.getRooms() .stream() .map(rid -> { RoomInfo.mapKey(rid, hid,rid); return new ViewChatRoomInfo(table.Chatrooms.select(rid).getName(),rid); }) .collect(() -> new ArrayList<ViewChatRoomInfo>(),List::add, List::addAll))); return true; })); if (!r.isSuccess()) { Trace.fatal("get all chatroom failed!", r.getException()); System.exit(-1); } }); CommonInfo.getInstance().setHalls(halls); lastSetHallsTimeStamp = System.currentTimeMillis(); }
这里的执行过程是,遍历hallnamecache,用hallid在chathalls表中逐个查询xbean.ChatHallInfo,然后遍历其中的rooms数组获得roomid(rid), 然后用rid在chatroom表中获取房间名, 最后生成了全部配置存储在halls数组中。
这里需要理解一个问题,为什么遍历hallnamecache表,而不直接遍历chathalls表。原因在于zdb提供两种walk,一种遍历表的内存cache(setViewHallsFromCache中的walk), 另一种遍历底层数据库(onManagerInitialized中的walk)。底层数据库表存储的是checkpoint之后的数据, 只有内存cache数据和底层数据的并集才是全部的数据。对于内存表, 只有cache数据, 没有底层数据, 所以内存表中的数据可以一次性完整遍历。
按照上面的说法,又能够提出一个问题,为什么不直接提供一个walk,把两种walk的结果结合起来。简单的解释就是为了尽量减少锁定。举个例子说明,cache数据比底层数据新,先输出cache中(k0,v0),既然输出了就要记录下来,避免遍历底层数据库时产生重复,遍历底层数据库时,发现记录(k0,v?),这时候既然k0相关记录已经输出了,(k0,v?)就只能忽略,问题是,如果不在输出(k0,v0)时锁定k0记录,我们就不能假设(k0,v?)比(k0,v0)旧,如果(k0,v?)比(k0,v0)新而被忽略掉,那么这样的结果比脏读还差。所以要解决这个问题实际上就只能锁定整个cache,输出cache记录,然后遍历底层数据库。如果锁定了整个cache,其它所有使用这张表的事务全部会被阻塞,严重影响性能。事实上,zdb提供的walk为了事务性能考虑,都是脏读,本来使用上就应该谨慎,尽量少用,结合两种walk意义不大。
-
配置修改
用删除room举例说明
Main.java:
cmdmap.put("deleteroom", (sessionid, params) -> { if (params.length < 3 || !UserInfo.getInstance(sessionid).isCommandMode()) return; Procedure.execute(() -> { Long hallid = table.Hallnamecache.select(params[1]); if (null == hallid) { UserInfo.getInstance(sessionid)._setRecvedmessage( new ChatMessage("bad hall name", sessionid)); return false; } xbean.ChatHallInfo hallinfo = table.Chathalls.update(hallid); Long removeroomid = null; for (Long roomid : hallinfo.getRooms()) { xbean.ChatRoomInfo roominfo = table.Chatrooms.update(roomid); if (params[2].equals(roominfo.getName())) { removeroomid = roomid; break; } } if (null == removeroomid) { UserInfo.getInstance(sessionid)._setRecvedmessage( new ChatMessage("bad room name", sessionid)); return false; } GlobalId.delete(groupRoomNames, params[2]); table.Chatrooms.delete(removeroomid); hallinfo.getRooms().remove(removeroomid); xbean.RoomInfoCache roominfocache = table.Roominfocache.update(removeroomid); List<ChatRoom> roomlist = new ArrayList<>(); if (null != roominfocache) { roomlist.add(roominfocache.getRoomview()); table.Roominfocache.delete(removeroomid); } updateViewHallsFromCache(sessionid, "delete room succeed",roomlist); return true; }); });
根据大厅名在hallnamecache表里查找hallid,通过hallid在chathalls表里面查找所有房间的roomid,遍历这些roomid,在chatrooms表中匹配待删除房间名, 确定removeroomid, 之后在chatrooms表里删除removeroomid记录, 修改hallinfo表的hallid对应记录,删除removeroomid。这些就是删除房间过程中两个表上的操作。
最后updateViewHallsFromCache更新大厅房间配置。
Main.java:
private static void updateViewHallsFromCache(long sessionid, String message, List<ChatRoom> destroylist) { UserInfo.getInstance(sessionid).setRecvedmessage(new ChatMessage(message, sessionid)); ProcedureHelper.executeWhileCommit(() -> Engine .getApplicationExecutor().execute(() -> { SessionManager.setViewHallsFromCache(); destroylist.forEach(room -> room.destroyInstance()); })); }
在这里,事务提交以后,调度一个任务,执行上一节描述的setViewHallsFromCache刷新大厅房间配置。
这里注意几个存储过程的用法:
表上通过key查询value的时候select一般比较少用,除非之后没有在相应记录上执行任何修改,否则就应该使用update,原因详见limax手册,服务器开发/Zdb/锁, 一节。所以前面的代码也就hallnamecache上使用了一次select。
ProcedureHelper.executeWhileCommit, 安排一个任务在当前事务提交后执行,如果事务回滚则不执行。在这个执行环境下,事务引用的xbean的锁还没有释放,这些xbean可读,但是不能写,写操作可能触发异常,回滚整个事务。实际上这样的功能主要提供给View实现,实现事务提交后的数据发送功能。上面的代码,在事务提交后,通过Engine的应用执行器再次提交一个刷新配置的任务,这样setViewHallsFromCache执行的时候,之前事务相关的锁完全释放掉了,减少了锁占用时间,有利于提高性能。
-
用户信息
<xbean name="UserInfo"> <variable name="nickname" type="string" /> </xbean> <table name="userinfo" key="long" value="UserInfo" />
修改用户信息的情况在前面的View部分SessionView一节已经讲到,非常简单,这里就不再叙述了。
-
RoomInfoCache
<table name="roominfocache" key="long" value="any:chatviews.ChatRoom" persistence="MEMORY" />
这里主要示例了内存表使用包含any类型value的情况,roominfocache表将roomid映射到ChatRoom这个TemporaryView对象上。
UserInfo.java:
private void joinRoom(long roomid, long sessionid) { Procedure.execute(ProcedureHelper.nameProcedure("joinRoom", () -> { ChatRoom view = table.Roominfocache.update(roomid); if (null == view) { ......................................... view = ChatRoom.createInstance(); view.setInfo(new ViewChatRoomInfo(info.getName(), roomid)); view.setRoomId(roomid); table.Roominfocache.insert(roomid, view); } view.getMemberShip().add(sessionid); return true; })); }
用户进入聊天室的时候,直接通过roomid获得ChatRoom,如果不存在则创建新的ChatRoom,存入cache,存在则直接取出使用。
Main.java:
cmdmap.put("deleteroom", (sessionid, params) -> { if (params.length < 3 || !UserInfo.getInstance(sessionid).isCommandMode()) return; Procedure.execute(() -> { .................... ChatRoom view = table.Roominfocache.update(removeroomid); List<ChatRoom> roomlist = new ArrayList<>(); if (null != view) { roomlist.add(view); table.Roominfocache.delete(removeroomid); } updateViewHallsFromCache(sessionid, "delete room succeed",roomlist); .................... return true; }); });
删除大厅内房间时,用update查找是否有对应ChatRoom存在,存在则从roominfocache中删除。
这里需要明确如下几个问题,内存表相当于一个支持事务能力的map。any类型只能被内存表作为value直接或间接使用(通过xbean引用)。zdb不关心any对象的寿命,换句话说就是any对象不受事务成功失败的影响。上面的deleteroom的例子中,view被存储到roomlist中,交给updateViewHallsFromCache销毁。从事务完整性角度看,上面的joinRoom并不完美,可以写成:
view = ChatRoom.createInstance(); final ChatRoom _view = view; ProcedureHelper.executeWhileRollback(() -> _view.destroyInstance()); …………………
这里通过ProcedureHelper.executeWhileRollback方法,调度一个事务失败时执行的任务销毁创建出来的view。