最近《计算机程序设计艺术》看多了,每次写完代码之后,总会习惯估算一下指令级的开销。导致每次写代码都是性能导向,违反了很多设计准则。因此打算重新看一下《UNIX编程艺术》,来拉一下已经严重倾斜的天平。
刚看了二页,又想起一个困扰我多年的问题,而没想到的是这次我似乎想到了解决办法。
在写服务器程序时,将数据持久化到数据库,是一个必不可少的操作。它需要将一个结构体进行打包,然后通过网络发给数据库,数据库再进行,并将操作结果通过网网络返回给应用程序。虽然可以通过这样或那样的手段,降低这些步骤之间的延迟,但是其本身带来的开销还是远大于一次函数调用。
因此在写代码时,通常无法忽略持久化所带来的开销,需要尽可能的合并对同一对象的持久化。
然而合并过程往往没有那么美好,这会会频繁打断原本我们完美的抽象,让我们编写逻辑时总是需要随时背起一个思想包袱。
下面举个简单的例子。
在这个例子里,我们会有两个模块,一个是Hero模块对外提供对某个hero对象的操作(这里仅提供加攻击力和加经验),另外一个是Team模块,调用Hero模块提供的接口对一批hero进行操作(这里假设所有操作都是按队伍操作的)。
//Hero模块 struct hero { int heroid; int attack; //攻击力 int exp; //当前经验 int level; //当前等级 }; std::unordered_map<int, struct hero> heros; static void persistent(int heroid) { auto &h = heros[heroid]; save_to_db(h); } void add_exp(int heroid, int exp) { auto &h = heros[heroid]; h.exp += exp; if (h.exp > exp_of_next_level) ++h.level; persistent(heroid); } void add_attack(int heroid, int val) { auto &h = heros[heroid]; h.attack += val; persistent(heroid); } //Team模块 void team_award() { add_exp(1, 50); add_attack(1, 60); }
上面的代码咋一看,很完美啊,高内聚,低耦合,API之间提供的功能也很正交。
但是它有一个致命问题,忽略了persistent函数所带来的开销,这个函数与普通函数是不同的。上面的代码一共会对同一个hero对象执行两次persistent操作。也就是整个开销扩大了200%,并且在我们编写业务逻辑时往往不止调用2个接口这么少。因此需要将team_award函数中将所有对heroid为1对象的persistent操作进行合并。
合并方法有多种,这里仅列出我最常用的一种(仅适用于1~3个同类型操作之间的合并,如果操作多而杂,这样做就非常不妥了),就是将add_exp和add_attack进行合并,提供如下函数,并在team_award函数中调用。
void add_exp_attack(int heroid, int exp, int attack) { auto &h = heros[heroid]; h.exp += exp; if (h.exp > exp_of_next_level) ++h.level; h.attack += attack; persistent(heroid); }
这时代码开始有点’bad taste’了,很显然这是违反了‘正交性’原则的。在某些地方我们需要编写诸如`add_exp_attack(1, 0, 10)`类似的代码。然而我却一直没有办法,一直沿用至今。
直到今天我再次打开《UNIX编程艺术》时,我忽然发现了服务器程序的一个规律。那就是,服务器逻辑总是由‘客户端请求’和‘定时器超时事件’驱动的。
那么请处理完一个‘客户端请求’或‘定时器超时事件’之后应该就是最佳的持久化时机。
因此,只要我们在处理这两类事件需要持久化时设立Dirty标记,再由框架在每次处理完‘客户端请求’和‘定时器超时事件’之后根据Dirty标记去持久化所有改动的数据。困扰我数年的持久化合并问题就这么完美的解决了。
我们需要增加一个persistent模块,改动后的伪码大概如下:
//persistent模块 typedef (persistent_cb_t)(int key, int ud); struct dirty { persistent_cb_t *cb; int ud; }; std::unordered_map<int, dirty> persistent_cb; void persistent_pend(int key, persistent_cb_t *cb, int ud) { auto &d = persistent_cb[key]; d.cb = cb; d.ud = ud; } void persistent_clear() { for (auto &iter:persistent_cb) { auto &d = iter.second; d.cb(iter.first, d.ud); } persistent_cb.clear(); } //Hero模块 struct hero { int heroid; int attack; //攻击力 int exp; //当前经验 int level; //当前等级 }; std::unordered_map<int, struct hero> heros; static void persistent(int heroid, int ud) { auto &h = heros[heroid]; save_to_db(h); } void add_exp(int heroid, int exp) { auto &h = heros[heroid]; h.exp += exp; if (h.exp > exp_of_next_level) ++h.level; persistent_pend(heroid, persistent, 0); } void add_attack(int heroid, int val) { auto &h = heros[heroid]; h.attack += val; persistent_pend(heroid, persistent, 0); } //Team模块 void team_award() { add_exp(1, 50); add_attack(1, 60); } //Socket请求处理模块 void socket_dispatch(int cmd, packet *req) { switch(cmd) { case 1: team_award(); } persistent_clear(); } //Timer超时事件处理模块 void timer_expire() { //调用所有超时回调 persistent_clear(); }
由此我们完美解决了,代码抽象和合并持久化之间的矛盾。