Suggestion

不要为了重用而重用

存储Bean的定义和协议Bean的定义最好不要为了重用而特意定义成一份当存储和协议的 结构【确实】一样时,允许使用同一个Bean定义。一般情况下,两者最好使用独立的Bean 定义。建议协议可以包含存储Bean,但不要反过来使用。

尽量把逻辑服务器设计成无状态的

尽量把所有的数据都定义到数据库中,让zeze管理所有数据。即使是仅用于一个逻辑服务 器的数据也可以存储到后台数据库。这带来的好处是数据量不会局限于内存容量(因为 cache会自动管理),后台数据库可以看成容量几乎无限;热更新服务器等事情也会变得简 单;请求派发(通过linkd)也会简单。

定时器:Timeout & Timer

定时器本质上是程序执行状态,这个破坏了逻辑服务器的无状态性。在大规模集群的情况下, 定时器的实现需要解决两个问题:

  • 一个定时器应该只在一个逻辑服务器检测。多个检测实例在最终处理事务时忽略重 复是可行,但存在浪费。
  • 调度定时器的逻辑服务器死掉以后,怎么发现和重新分配到其他逻辑服务器上。

一般建议:

  1. 定时器数据持久化时建议存储到期(或者下一次时间)的绝对时间,不要存储1小 时这种相对时间。这种方式可以利用底层已有的调度注册一次,不用轮询。
  2. 定时器实际调度时尽量应该和在线用户相关,不要在全部用户上实现。也就是说用 户上线时,把他需要的定时器进行调度,此时可以判断绝对时间,把已经超时的处 理掉。
  3. 某些情况下定时器可以交给客户端实现,服务器只校验。比如对于现时使用(不是 装备也不会影响buf)的物品,服务器就可以不做实际调度,仅在使用物品的时候 检查绝对到期时间。如果用户作弊或者时间不一致,以服务器为准。
  4. 对于系统级别的定时器,最好都不要轮询。这种定时器看需求了,真的很多,会有 相当的负载。实现时尽量注意。 需求分类分析:
  5. 对于用户相关定时器。用户登录时选定一台服务器,所有相关定时器都注册到这台 服务器。如果这台服务器关闭,用户需要重新登录并选择新的一台服务器,再次进 行相关注册。
  6. 对于系统级别(比如定时活动)的定时器。所有的服务器都同时进行定时器判断, 忽略服务器之间的时间差,这个问题不大。考虑到不间断运行,这种定时器如果从 配置中读取,最好支持运行期重新加载。可以考虑把这种定时器配置存到后台数据 库。
  7. 其他类别。具体问题具体分析了。

尽可能使用Rpc

Events 的建议

  1. 如果事件需要和当前事务一起提交回滚: 直接调用实现者的方法。这种模式不建议使用动态 订阅的模式,最好就不使用管理类,直接把需要执行的Handle调用写在触发点。这样能很 直观的看出来总共有哪些handle。
  2. 如果事件需要和当前事务一起提交,但是event-handle允许失败:使用嵌套事务方式执行。
  3. 如果事件和当前事务没有直接关联(或者仅仅传递一下参数):只用Zeze.Util.Task.Run执行。 传递参数的时候注意不能把Table内的Bean的引用(beankey的引用可以)直接传过去。 一般来说这种情况是不必要的,因为新的事务可以直接查询,此时只需要传递Table.Key即 可。
  4. events派发的时候一个handle失败是否影响其他handle的派发。一般建议不要扩大影响, 也就是说每个handle派发采用同步调用(直接invoke)需要try。Zeze.Util.Task.Run 已经 处理了错误,所以就是独立,不影响的。

事务的划分

这是并发编程里面最根本也最重要的问题。

  • 最基本的划分规则应该根据需求来决定操作是否放在一个事务中。
  • 一般框架中有Event的模式,此时要注意Event的执行是否需嵌套在触发的事务中执行。建 议是除非这是需求决定的(参见上一条),应该启动Event派发放到另外的事务中。 虽然说事务的划分应该根据需求来定,但很多时候,提需求的人也不一定说得清。服务器都 是收到一个请求开始处理数据,这样每个请求的处理就可以看作一个事务。这样就不用费太 多脑细胞去考虑划分的问题。 但是要下面几个例子要注意。
    1. 当请求需要对队伍中的多个用户发放奖励时,一般来说一个用户的奖励发放失败不应该影响 其他用户。此时就需要分到多个事务,或者使用嵌套事务。
    2. 当对所有好友(家族成员,帮派成员)进行遍历处理时,也要注意。总的来说就是需要遍历 (广播)之类的操作都需要注意一下。比如Events的触发很多时候也是一种遍历(广播)。
    3. 并发优化:如果请求操作很复杂,访问很多数据,冲突比较严重的时候,需要仔细考虑需求, 进行事务划分细化。
    4. 广播需要一个事务例子:创建角色的时候,需要调用所有需要初始化的模块进行处理,此时 不管需要初始多少个模块,都需要在一个事务内。

TableCache.Capacity

一个事务中操作的同一个表的记录数超过Capacity,这个事务处理时间又超出 CacheNewLruHotPeriod时间(超过后就可能会被Clean),这导致事务最后lock_and_check 时发现记录已经被Clean,然后重做,最终有可能永远完成不了。所以!注意!这个问题可 以在记录内记录一个标记,保证新装载的记录至少用过一次才会被Clean,保证至少完成一 次事务。但为了这个问题做这些修改,感觉不值得,就不考虑了。

AllowDirtyWhenAllRead SelectCopy SelectDirty

  1. AllowDirtyWhenAllRead 当事务中所有的操作都是读操作并且事务级别为这个,那么事务将 不进行原子性检查,直接成功,不会发生重做。具有很高的并发性。非原子性的例子:事务 Writer修改两个变量V1,V2(最简单的,来自同一个Bean的两个变量,包括来自两个记录 或者来自两个表);事务DirtyRead先读取V1,再读取V2,那么读到的V1可能事务Writer 修改前的,读到的V2可能是事务Writer修改后的;也就是说两个变量没有原子化。这个级 别一般用于用户仅仅查询数据用来显示,并不关心数据之间的原子性,也没有关联两个变量 的逻辑判断,可以大大提高并发性。
  2. SelectCopy 在记录读锁内获得记录的拷贝,如果上面例子的两个V1,V2都在一个记录内, 那么原子性得到保证。但是V1,V2在两个记录内(或者两个表),仍然没有原子保证。这个 方法可以在事务外使用。Zezex/Game/Login/Onlines 给在线用户发送消息时,可以使用这 个方法安全的在事务外执行,因为Online.Status需要的两个变量(LinkName,LinkSid)都在一 个记录内,不会发生读到一个修改后的LinkName,而LinkSid又是旧的问题。
  3. SelectDirty 一般用于事务外,直接返回数据引用,记录锁外直接读取数据。和 AllowDirtyWhenAllRead一样,没有原子性保证。
  4. AllowDirtyWhenAllRead SelectDirty 使用时,读取record.var以后,再次读取,值可能发生 了变化,所以对同一个var,最好仅读取一次。当然也为了效率考虑,一个变量如果后面需 要重用,自己先保存一下。AllowDirtyWhenAllRead 现在除了可以在NewProcedure时设置, 也配置到了Protocol中,可以后期优化并发时加上配置,先不用关心。【注意:最好保持同 一个变量仅读取一次】

CheckpointMode.Table 的并行优化

在这个模式下,多个事务访问的记录当存在交叉时,会被关联到一起进行Checkpoint。关 联越分散,并行度越高。按自然的方式划分事务,一般具有足够好的并行度。但还是需要注 意某些全局模块访问。比如有个全局统计数据,非常多的事务都需要读写这个数据进行逻辑 判断,那么这些事务都会被关联起来,降低Checkpoint并行度。这时候,提高并行度就需 要进行额外处理。根据自己的事务划分需求,看看是否能把读写分到另外的事务中执行,本 事务根据结果进行处理。Checkpoint并行度属于优化,开发初期可以不用关心。

事务中不能访问大量的记录

一个事务不能修改太多的记录;一个事务不能读取太多的记录(比写可以多些)。当循环处 理的时候,要注意循环的量级。并且需要注意这个循环是否一定需要在一个事务内。参见事 务划分。

服务器开发读写问题

一个操作会引起大批量的修改事务时要非常注意,出现这种情况时,看看操作流程能不能转 换成少量的写+大量的读取。比如微博的博主发布了一条消息,马上推送消息到关注者的队 列中就可能会引起大量的修改事务,改成查询关注对象是否有更新并拉取,这样写只需要写 自己的发布队列,问题就转换成大量的查询。因为查询是读,可以充分利用上cache机制, 从整体来看是比大量修改事务好的。当然具体问题具体分析,可能不是所有的大量写问题都 可以做这个转换。开发同学需要自己注意这个问题。