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。


上一页 下一页