db服设计

整体框架

目前使用的服务端框架:

小区路由服在启服时,会监听一个端口,其他服会做为客户端连接路由服,连接是单向的,只有其他服去连接路由服。但小区路由服会去连接跨服路由服,最终形成一个星型结构。

game 服,commonsvr 公共服等服(简称客户端),在选择哪个路由 router 时,有以下几种方式做选择:

  1. 用本服务的唯一 nodeid % 路由个数。
  2. 用字符串转md5,然后再 % 路由个数(这类用的比较少)。
  3. 用玩家uid,或者公会id % 路由个数。

game 服节点,或者其他节点需要更新数据到数据库,那么是怎么选择 db 服的呢?

假设现在有 db 节点3个,game 服想要对数据库操作,那么就想要对 db 服传送数据,我们怎么选择那个 db 服呢,我们是在 game 节点只有当前存活的所有的路由服信息,没有 db 服信息。那么 game 服可以通过一个负载因子,先选择一个路由节点,然后,把消息(包括:更新或查询等具体数据内存,负载因子,目标节点类型)传递到这个路由服。其中本次消息的目标节点类型是 db。在消息发送到路由服后,路由服再根据目标节点类型,以及负载因子选择一个 db 服,然后将具体数据发送给选择好后的 db 服,db 服最后再将 数据发送到 mysql 进行落地。

路由服在选择db服时,同样是通过 客户端层 传递过来的负载因子id(nodeid,string,uid等)% db 服个数,然后与某个 db服进行通信。每次通讯都会重新计算一次。

数字取模操作会很快,性能上也还好,第二个如果要缓存这些映射关系,对于 uid,公会id这类,是会无限增加的,内存也会跟着无限增长,对于字符串负载因子,因为现在已经很少用了,性能开销不大。如果觉得大量的md5计算会耗时的话,个人感觉可以只在业务层,做字符串负载id映射,既 balance_seed[字符串] = md5(字符串),缓存大小最好也控制下,使用缓存淘汰策略。

通过负载id取模的方式,保证了每次业务操作,都只选择唯一条线路和 db 服打交道。保证数据插入,更新,读取的一致性。而且,我们在做游戏开发时,有个很重要的原则,既某类业务只会绑定到数据库的某张表里的某个字段,或某几个字段。而且某个时刻,只有一个服去操作这个字段里的数据。

比如,玩家实体是在 game server  创建的。为了负载,game server 可以有多个。game server1 有玩家1,game server2 有玩家2,玩家1 和玩家2 虽然都在 mysql 里的 role 同一张表中,但当前时刻,只会有一个 game server 去读取,更新 role 表的 玩家1 数据。不会有其他进程,去读写 role 表的玩家1 数据。

总结:做服务端开发时,尽可能设计成,只有一个进程操作数据库表里的某个字段数据,固定连接线路,然后串行操作(读,写,更新),不同进程可以操作同一个表的不同字段数。简单来说,就是多个不相干的数据操作是并发,相关的数据是串行的。这样的并发设计,大大减少了开发者的心智负担,也减少了很多意想不到的bug。

设计注意点:

当玩家掉线重连,或者其他情况下,切换到另一个 game server 登陆时,原先旧的 game server 一定要保存玩家数据入库后,才能在新的 game server 加载数据。不然,有可能新的 game server 开始写入 role 数据,而旧的 game server 把旧的 role 数据覆盖过去,造成玩家新数据丢失。所以,一定要尽可能保证数据在落地前,只有一个进程在操作这份数据。

db服设计

一个db服会起多个 网络线程,多个 worker 工作线程,以及多个连接数据库线程,处理不同的 sql 数据业务。如下图:

db服在socket线程接受到路由发来的 sql 请求时,通过 sql表名+ 主键key 拼接的字符串,取md5码,得到一个唯一的数字,然后再和 db 线程数取模,发到对应的 db 线程处理这个 sql 请求。这样,就可以做到不同的sql请求,在不同的 db 线程处理,实现并发。

db线程处理不同的sql请求

我们可以起多个db线程,每个db线程都会去连接同一个 mysql,以及一个 redis。也就是说,一个区,只会有一个mysql 和一个 redis。这种设计比较简单,也足够应付大多数类型的游戏。

在一个db线程中,处理数据更新,插入,查询等流程时,会不会因为同时操作redis,mysql失败,导致的数据不一致呢。我们来分析下

比如,我们需要对一个 key 进行查询,更新,插入操作。

查询:

我们在获取 key 数据时,可以先查看 key 是否在黑名单中(下面更新有介绍),如果在,直接访问 mysql,如果不在,就直接访问 redis,如果 redis 没有缓存数据,就去 mysql 中查询,将查询结果返回给业务层,同时也更新到 redis,并设置过期时间1h。下次查询,就可以直接从 redis 中获取,加快数据获取速度。如果是在 mysql 层查询失败,我们就直接给业务层发送一个失败的消息。

更新:

在 db 线程接收到更新请求时,我们可以同时对 redis,mysql 发送更新操作。然后对 key 记录起来。例如:

map[key] = {redisResule = 0, mysqlResule = 0}

如果 redis 更新成功,我们标记为1,如果更新失败,我们标记为-1,同理,mysql 也是。当 redis 和 mysql  都更新成功,标记为1时,然后就可以给业务层发送一个成功消息。

如果 redis 更新失败了,但 mysql 更新成功了,我们也给业务层发送一个成功消息。但会把这个 key 放到一个黑名单中,下次如果有读操作,发现  key 在黑名单时,就不去 redis 读缓存数据,而是直接去 mysql 获取,并把 mysql 成功获取到的数据再次覆盖到 redis 中,设置过期时间1h。如果 redis 更新成功,就从黑名单中移除。

如果是 mysql 先返回成功,那么我们可以先给业务层发送一个成功的消息,而后如果 redis 返回失败,将这个 key 加入黑名单。

插入:

插入和更新类似,同样需要把 key 记录起来,redis 和 mysql 插入都成功时,给业务层返回一个成功的消息。失败就记录到黑名单中,下次操作,一定是走 mysql 数据库,保证拿到的数据都是最新的。

存在的问题:

如果 redis 有缓存 key 数据,在有更新,查询两个顺序请求时,我们先执行更新,再立马执行查询请求,如果更新 mysql 是成功的,redis 是失败的,但 redis 还没来得及返回添加到黑名单中,此时,执行的查询请求,会因为黑名单中没有这个 key,而直接去查询 redis,那么查询得到的结果就是缓存旧数据,导致业务层获取到的和 mysql 不一致。

针对这种情况,我们可以为 db 线程设计 n个队列,然后对每个 key 哈希取模,选择其中的一个队列,然后把 key 的操作请求放到队列中,只有队列中第一个 key 请求执行完后,pop 出去,才去执行第2个 key 的请求。这样即使对同一个 key 的更新,查询,我们都会被按顺序放到同一个队列中。只有更新出结果了,要么成功,要么失败,才会执行下一个 key 的请求。如果更新失败了,key 会被放到黑名单中,此时,再去执行队列中的第2个查询操作,会先去读黑名单,有在其中,就直接访问 mysql,而不会去访问 redis 了,业务层也会拿到和 myql 一致的数据。

这种队列设计,相当于把同一个 key 不同操作串行起来,好处是 能解决 redis 和 mysql 数据不一致的问题,实现简单,不足的地方,是有一定的操作延迟,因为不同的 key 放在同一个队列中,会被阻塞。除非,设计成按 key 来实现队列,一个 key 就对应创建一个队列,不过这种设计,感觉不太合理,占用的内存会高,而且性能也不一定能比固定 n 个队列强多少。

引入了阻塞队列后,如果队头的请求,数据库一直没有返回,可以额外开个线程定时去检测队列情况,并做日志记录,方便后面查询阻塞原因。

posted @   墨色山水  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端
点击右上角即可分享
微信分享提示