Organizations

20 results for 架构
  • 日志分表分页 因为公司数据日志打点十分频繁, 并且涵盖有多个条件筛选的情况, 并且采用了数据分表的出来数据写入. 更加主要问题是每日单表数据递增速度在最低 50G 以上, 并且如果用户量越高递增的数据流越高, 还有更大的问题就是数据格式不稳定. 内部上报数据是 JSON 格式数据, 高版本 MySQL 支持的 JSON 数据结构选用这种类型方便对后续数据过滤匹配筛选 而日志上报数据后台需要可以通过 年月日时分秒 这种条件精确筛选入库时间, 后续还要支持其他复杂条件精确检索. 这里以简单打点日志表 step_log 做说明, 后台进程会采用分表入库 step_log_20241201,step_log_20241202,.... 入库, 那么后续需要检索分页数据就需要费时处理下, 可以假设表结构字段如下: player_id: 玩家ID sid: 服务器ID time: 入库时间戳 action: 行为id val: 上报数据 JSON, 以 {} 数据格式为主 需要说明这里的 val 数据内部 JSON 内部数据会按照频繁度, 从里面升级为表字段; 比如表内部带有 { "channel":1001 } 字段是运营迫切需要的, 那么后续会将 JSON 内字段 晋升 为表字段, 追加上索引之后能够更快加速表数据检索的速度. 之后数据分页也需要利用 UNION ALL 来做表合并, 因为日志表一般数据结构是固定, 所以可以直接将所有表连接查询: # 假设用户需要查询 step_log_20241201 ~ step_log_20241207 的表数据 # 首选需要判断表是否存在, 去掉那些不存在的表 SHOW TABLES LIKE 'step_log_20241201' # 之后通过检索出存在合表数据总量 SELECT COUNT(1) AS `total` FROM step_log_20241201 UNION ALL SELECT COUNT(1) AS `total` FROM step_log_20241202; # 其他表追加 UNION ALL .
    架构 Created Thu, 23 Jan 2025 23:44:48 +0800
  • 强关联架构 之前说过强关联数据的复杂情况( 涵盖有交叉切库和多表关联的关系 ), 当时想着早晚会遇到这种需求, 没想到这么快就遇到了. 这里涉及到的有以下关联: 游戏服务器组(ServerGroup): 将大量服务器分组, 每个组关联多个服务器放置在同个数据库, 该数据库对应组ID 游戏服务器(Server): 游戏服务器, 放置设计游戏关联信息的 渠道来源(Channel): 来源渠道, 比如快手|抖音|微信或者商店对应来源渠道 后台管理员(User): 后台管理员, 不同管理员关联不同渠道|不同服务器|不同权限等 后台权限(Auth): 访问权限, 管理员的权限各有不同 这里强烈这种需求尽可能 不要用脚本语言去实现, 特别是弱类型语言维护坑太多了; 复杂关联数据本身就很绕了, 还要处理数据类型传递验证且没有常驻数据连接池访问的问题. 弱类型后续维护坑太大了, 主要是弱类型写着最后传递过来的类型分不清是 int|string|null 哪种, 不注意的时候传递类型直接被修改都不清楚情况. 这里关联数据的方式先说明, 这里授权采用传统 RBAC 建模, 单个用户关联多个角色, 角色有: 用户登录后台(User 检索数据) 判断用户是最高管理员或者普通管理员( 只有最高管理员默认拥有所有权限 ) 获取关联角色|分组( Group|Role 检索关联 ) 通过角色|分组关联获取权限( Auth 检索菜单和接口权限 ) 通过角色|分组关联获取渠道( Channel 检索关联渠道 ) 通过角色|分组关联获取服务器( Server 检索关联服务器 ) 后续所有数据都要去 ServerId IN (1,2,3) AND Channel(1,2,3,4) 这里追加条件, 单单要建立这里面关联就需要以下表: # 管理员表, 最基础简单的账户表 CREATE TABLE IF NOT EXISTS `user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号名', `nickname` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '昵称', `salt` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '盐值', `password` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '哈希密码', `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权Token', `created_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `created_uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建管理员', `created_ip` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建IP', `updated_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `updated_uid` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新管理员', `updated_ip` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '更新IP', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='管理员账户表'; # 管理员对应分组, 给后台划分角色类型, 比如策划|主管|客服等角色 CREATE TABLE IF NOT EXISTS `user_group` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色组名称', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='管理员角色组'; # 用户权限表 CREATE TABLE IF NOT EXISTS `user_auth` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `pid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '等于0标识父级节点, 大于0是关联的父节点', `ty` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '菜单类型, 0 默认为菜单页面, 1 为API隐藏接口类型', `name` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '权限名', `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限路径', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='管理员权限表'; # 对应渠道来源, 比如快手|抖音|微信|H5等 CREATE TABLE IF NOT EXISTS `channel` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `channel_id` bigint(20) unsigned NOT NULL COMMENT '第三方渠道定义', `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '渠道名称', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='渠道分类'; # 服务器列表 CREATE TABLE IF NOT EXISTS `server` ( `id` bigint(20) unsigned NOT NULL COMMENT '服务器ID, 需要运维定义', `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '服务器名称', `created_at` int(10) unsigned NOT NULL COMMENT '创建时间', `created_uid` bigint(20) unsigned NOT NULL COMMENT '创建管理员', `created_ip` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建IP地址', `updated_at` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', `updated_uid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '更新管理员', `updated_ip` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '更新IP地址', PRIMARY KEY (`id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='服务器列表'; 这就是基础的信息表, 需要留意基础表数据千万 不要硬删除, 否则会出现关联数据关联错误, 而是采用字段软删除才能保证数据关联; 之后关联的表数据用于 role|group 这些分组ID来检索关系:
    架构 Created Thu, 16 Jan 2025 21:14:39 +0800
  • 多重互相关联 这里实际上工作当中涉及到的数据库多重关联问题, 而且这是目前所有系统系统当中大概率会接触到的问题, 假设目前后台需要设计游戏邮件表处理游戏邮件发放, 这里初版表结构如下: CREATE TABLE `game_mail_list` ( `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `server_id` JSON NOT NULL COMMENT '发送的服务器id', `player_id` JSON NOT NULL COMMENT '发送的玩家id', `type` TINYINT(3) UNSIGNED NOT NULL COMMENT '邮件类型', `title` VARCHAR(255) NOT NULL COMMENT '邮件标题' COLLATE 'utf8mb4_unicode_ci', `content` LONGTEXT COMMENT '邮件内容' COLLATE 'utf8mb4_unicode_ci', `item` LONGTEXT COMMENT '附件内容', `create_time` INT(10) UNSIGNED NOT NULL COMMENT '创建时间', `push_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '推送时间', `start_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '展示开始时间', `end_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '展示结束时间', `delete_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '软删除时间', `update_uid` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '操作者', `check_status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '审核状态: 0 代表等待审核, 1代表审核通过,2代表审核拒绝', `check_uid` BIGINT(20) UNSIGNED NOT NULL DEFAULT '0' COMMENT '审核处理人', `check_time` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '审核时间', PRIMARY KEY (`id`) USING BTREE ) COMMENT ='玩家邮件列表' COLLATE = 'utf8mb4_unicode_ci' ENGINE = InnoDB ROW_FORMAT = DYNAMIC; 十分简单的表, 直接记录后台创建对应 服务器ID+玩家ID 即可, 基本上满足前期推送游戏邮件需求.
    架构 Created Thu, 09 Jan 2025 23:29:17 +0800
  • 数据中台 数据中台( Data Centered Platform ) 是现代化管理数据方案, 在常规后台和数据库之中追加层数据处理层; 也就是废除常规 Web <-> DB 架构演变成 Web <-> Center <-> DB 中间做数据清理. 这样相比常规架构有以下好处: 隔离子业务直接访问数据库, 保证数据安全性 统一数据获取入口, 打通业务平台直接让多套系统统一 避免重复的数据筛选工作, 直接请求通用中心获取数据 数据中间层方便数据高效筛选和获取 减少运维管理成本, 多套系统和数据库集中在单套分布式服务器 项目架构演变路程: 项目立项: 单套后台系统和单数据库提供服务, 没有太复杂的角色权限, 运维|运营 共用管理中心 项目发展: 数据量暴涨并且单库单表开始无法承载, 需要分库分表和后台执行常驻任务清洗数据 项目成熟: 角色细分, 运营|运维|渠道商|客服 等业务拆分出来做复杂权限操作和查询, 还需要大量数据筛选统计 这样构建的好处就是避免业务碎片化, 比如需要知道客服业务需要去登录 XXX客户中心, 需要渠道业务需要去登录 YYY渠道中心, 诸如此类多系统在新人理解业务时候带来思维凌乱的问题, 每个系统可能业务相同但是有好几份代码. 比如 A 中心本身代码有查询用户数据功能, B 中心也有类似业务但是实现的是另外代码业务, 后面扩展 C 中心也类似 当然也不是提倡什么业务一开始就直接采用这种复杂架构方式, 因为本身设计这种架构对于开发人员来说需要很深厚的技术功底, 同时也要对线上业务有比较深入接触才能知道对应架构痛点. 对于项目初期盲目采用数据中台架构会导致项目更加混乱, 项目初期数据量级本身没达到要这样处理的地步 项目迈入成熟阶段比较明显的特征就是获取效率越来越低下, 且业务开始有数据主体关联( 渠道 - 用户 开始建立强关联 ).
    架构 Created Tue, 24 Dec 2024 22:28:42 +0800
  • 大数据处理 这里说明下当前的面临的环境困局: 单表可能超过2~3G/day, 主要玩家打点数据 多表需要关联查询, 内部带有强关联( A表依靠B|C表的数据 ) 数据要求长持久性, 数据需要运营存底且需要保持最少三个月处理 已经实行分库分表, 但还是没办法提高数据查询效率 在游戏当中会出现频繁记录打点信息, 用于追踪玩家操作从而还原出玩家具体的操作流程; 特别游戏准备开服时期更加需要玩家打点信息, 如果买量推广效果好的时候单日递增打点数据是十分庞大的. 同时按照运营需求的情况, 也许需要统计玩家对于游戏活动参与度, 方便运营和策划分析总结下次构建出活动. 游戏运营的数据库相比其他更加复杂( 当然比不过更加频繁的 实时IM 方面 ), 其中涉及到: 分服: 游戏服务交叉很复杂, 可能涉及到 单服务器分叉多个子服 情况 存盘: 游戏相关数据需要保持至少一年, 且数据量及其庞大 检索: 关联数据查询十分频繁, 统计日活跃|月活跃|季度活跃交叉查询特别频繁 如果没有概念, 那么以简单案例说明: # 运营需要做月度玩家盘点, 要求如下: # 1. 需要查询30天玩家留存, 支持以下维度查询 # 1.1 起始日期( start_time, end_time ) # 1.2 可以包含 全部| 充值玩家 |不充值玩家( is_recharge ) # 1.3 可以筛选指定充值金额区间的玩家统计 ( start_recharge, end_recharge ) # 1.4 支持对应服务器Id查询( server_id ) 以此说明表的大概格式如下:
    架构 Created Fri, 20 Dec 2024 23:04:23 +0800
  • 游戏窗口比例 这里列出常规的游戏当中用到的像素比例, 方便开发的时候套用屏幕比例加载. 这里列取出些外部网站设置标准比例参考: QQ游戏大厅第三方比例 COCOS手机比例 常规手机比例 Gamer比例 因为现在宽屏2K/4K显示器的普及, 屏幕能够设置的比例也越来越高, 这里提供下比较主流的PC比例: 1280 x 720 1366 x 768 1440 x 810 1600 x 900 1920 x 1080 以上就是比较标准的常规PC比例, 而移动端竖屏则大部分采用以下比例: 640 x 1136, 兼容最好 750 x 1334 1242 x 2208 1125 x 2436, 支持全面屏 基本上设计采用以上参考即可, 这里提供 JSON 配置文件方便本地读取加载, 首先是 PC 配置: [ { "w": 1280, "h": 720 }, { "w": 1366, "h": 768 }, { "w": 1440, "h": 810 }, { "w": 1600, "h": 900 }, { "w": 1920, "h": 1080 } ] 而手机竖屏端配置则如下:
    架构 Created Sun, 16 Jun 2024 15:46:32 +0800
  • 协议响应设计 无论是 Web|Protbuf 服务端响应给客户端都基本需要对协议进行关联响应, 比如 Web 响应给数据(内部的 status 用于客户端索引错误处理): // 客户端请求转发给内部让其转发到指定proto对象 { // 标识请求的请求控制器 "proto": 10059, "args": { "username": "meteorcat", "password": "meteorcat", } } // 服务端响应给客户端响应协议 { "status": 10037, "message": "failed by username", "data": {} } 而 GoogleProtobuf 也虽然不需要带上 status, 但是需要在客户端|服务端采用 KEY-VALUE 方式做哈希索引: --- 这里做类似Lua挂载查询 --- 常规都是以哈希表方式让其客户端|服务端启动时候加载 protocols = {} protocols[10023] = LoginRequestProto.New() protocols[10024] = LoginResponseProto.New() --- 后续直接调用其内部句柄对象解析协议即可 所以协议关联是很重要的, 否则后续请求协议数据多起来特别庞大也不好管理, 所以需要全局的协议管理器方便注入处理; 但是注册协议时候最大问题就是协议分类, 功能分类譬如 玩家分类(User-Login|Register)|公告分类(Notice-List|Readed), 这种时候就需要对协议做分类方便类目管理, 这推荐采用整除法做协议管理分组: --- 正规的协议采用 `xxxyyy` 格式的 int 类型 --- 比如设定用户信息类 100, 而他分类当中可以 获取信息(1)/修改昵称(2) --- 剩下设定场景游戏类 200, 而其分类当中可以 加入房间(1)/离开房间(2) local user_group = 100 local user_get_info = 1 local user_change_nickname = 2 -- 打包成int local user_mask = user_group * 1000 -- 转换成 100,000 local user_get_info_proto = user_mask + user_get_info -- 打包成 100,001, 后三位用于识别子分类 local user_change_nickname_proto = user_mask + user_change_nickname -- 打包成 100,002 -- 解包出类型和具体协议 local dec_mask = tonumber(user_get_info_proto / 1000) -- 整除掉标识, 获取到分类 100 local dec_id = user_get_info_proto - (dec_mask * 1000) -- 提取出的具体协议值 1 这种方法直接利用简单的加减乘除处理, 在性能上面占据优势且跨平台处理也方便移植, 所以包装协议可以直接处理好封装成哈希表处理.
    架构 Created Thu, 09 May 2024 18:46:37 +0800
  • 数据库设计小技巧 对于常规的表结构推荐预留以下字段: create_uid: 创建后台管理员ID update_uid: 更新数据管理员ID delete_uid: 删除数据管理员ID create_time: 数据创建时间 update_time: 数据更新时间 delete_time: 删除删除时间 这几个是设计表当中最好追加的字段, 因为常常需要溯源和软删除处理, 这里举几个常用样例: 需求A: 用户创建的数据要求自己进行可见和修改, 用户只能修改自己所有的那部分数据. 需求B: 数据只允许被 软删除 而不能直接 硬删除, 会导致丢失关联的数据库数据. 需求C: 订单数据表需要按照最后玩家支付时间倒叙排序查看( 订单流程分为: 创建订单, 等待回调, 支付完成, 发货完成 ) 这里目前工作当中最常见的场景, 里面都需要充分调度到内部的条件数据: # 只允许查询|修改到自己创建的数据 SELECT * FROM `order_info` WHERE `create_uid` = owner_uid # 去除查询出来的已经被标识删除的数据 SELECT * FROM `order_info` WHERE `delete_uid` <= 0 这几个字段在功能表上推荐先追加上, 因为本身都是数值类型所以相对占用也不是那么大.
    架构 Created Thu, 18 Apr 2024 18:14:13 +0800
  • 空间坐标测距检索 当时项目提出的具体项目需求, 后续实现之后感觉挺有趣的就把其单独剔除出来总结起来; 具体需求就是商家在我们自己商户平台 注册并绑定当前地址, 然后用户需要在平台检索最近的注册商家. 这里实现方式其实也十分简单, 只需要在注册的时候使用 MySQL|MariaDB 的 Geometry 类型保存位置经纬度. 注意: MySQL版本必须在 5.7 之后才能支持该类型. Geometry 类型是专门用于空间存储的类型, 具体支持以下细分类型: Point(110.3 44.0): 点, 保存坐标点位置, 也就是位置标识经纬度 LineString(80.07 23.45, 99.23 34.56): 线, 两个点联系的直线, 用于标识两点直线 Polygon((93.30 35.301 90.332 30.341)): 面, 多点连接采用的多边形面 MultiPoint: 多点, 用于多个点记录, 一般是复杂的多点位置记录 MultiLineString: 多线, 用于多条路线记录, 一般是复杂的多路线记录 MultiPolygon: 多面, 用于多个面对象, 一般是复杂的多面记录 GeometryCollection: 集合, 这是很高级的经纬度混合集合, 实际上我个人很少去用到 回归需求上面, 实际上要求客户端|移动端在商户平台需要对接第三方SDK平台来提交坐标系, 这里以 百度地图开放平台SDK 做样例; 假设安卓客户端已经接入 百度安卓定位SDK, 如下官方文档说明: /** * 实现定位回调 */ public class MyLocationListener extends BDAbstractLocationListener { @Override public void onReceiveLocation(BDLocation location) { //此处的BDLocation为定位结果信息类,通过它的各种get方法可获取定位相关的全部结果 //以下只列举部分获取经纬度相关(常用)的结果信息 //更多结果信息获取说明,请参照类参考中BDLocation类中的说明 //获取纬度信息 - 关键参数 double latitude = location.
    架构 Created Wed, 17 Apr 2024 19:55:51 +0800
  • 数据打点量化 数据统计是日常工作常见业务, 最常见的就是需要汇总买量玩家的登录/充值/流失等, 需要了解游玩对象玩家的转化率. 除了游戏相关, 日常大数据也需要统计, 比如统计玩家购买物品统计购买的种类/购买频率通过大数据统计出购买喜好等 这种打点统计用几种方式: 第三方推送: 走 Web 推送 本地内存队列: 走 Redis/Mongo 队列推送 本地日志推送: 这种方式是我最近看到, 通过服务端写入本地日志文件, 之后第三方接入通过监控日志文件在后台异步推送, 这几种是目前接触过的, 每种都有其优势. 第三方推送 首先是 第三方Web推送 , 在日常比较有两种方式: 客户端推送: 将负载移交到客户端和第三方交互, 但是数据准确性可能会被人篡改, 比如抓包查看到就能自己去随便推送, 这种方式数据准确性实际上很难说, 而且曾经发现过第三方自己刷自己接口增加自身买量数量的问题. 服务端推送: 这种对于相对响应速度要求不高, 因为在中间数据传输过很多层解析, 打点推送会走 dns(解析域名) -> resolve(IP最速解析) -> https(证书验证) -> shunt(后端负载均衡), 可以看到服务端推送如果大数据的时候大数据转发时候可能会超时卡住从而让整个服务停摆等待推送完成. 这里以前踩过坑, 第三方打点推送不要完全信任数据可以到达, 可能第三方服务某些时候需要停机维护等, 如果大数量打点推送的时候日志异常足够撑爆本地. 数据推送绝对不要采用同步方式, 而且如果是游戏服务端千万不要在内部服务耦合混进去, 无论同步异步在游戏服务端内部走 Web 推送都是很 “蠢” 的设计, 本身就是为了低延迟高并发的服务不要集成这种打点方式. 本地内存队列 这种比较常见也是相对来说兼顾低延迟高并发的方式, 直接采用内存监听队列方式, 本地直接内网推送节省打点过程的解析浪费和不走本地日志文件IO提升响应速度(注意采用长连接推送). 这种方式主要问题就是对于大数据打点的情况, 可能本地服务器内存可能直接一下子就被打满了. 一般队列除了生产者也需要消费者把数据提取出来放置到关系型数据库来进行数据落地. 本地日志推送 这种方式是最近看到某个第三方采用类似 elk 方式, 让游戏服务端将日志按照规则写入到本地日志文件 xxx.log 之中, 然后接入 SDK 会不断异步把日志推送到第三方那边过去.
    架构 Created Sat, 30 Sep 2023 16:32:56 +0800
Previous