
简介基于Skynet框架构建的游戏服务器源码面向有一定后端基础的游戏开发者展示了如何用轻量级高并发框架整合MySQL关系型存储与Redis缓存解决服务器性能与数据库交互瓶颈。压缩包共22个文件主体为Lua逻辑脚本辅以Python客户端脚本、.proto/.pb协议定义、节点配置及启动脚本整体仅619KB结构紧凑便于快速定位网关、代理、场景、登录等核心模块。目前已有132人学习下载适合用于学习Skynet消息机制、服务节点划分以及MySQL访问层与Redis缓存管理在真实项目中的落地方式。源码覆盖从客户端请求分发、负载均衡到数据持久化的完整链路可作为中小型游戏服务器开发的参考模板也可作为扩展功能与二次改造的基础底座。1. 别只看登录器这套源码值钱的是 mysql 与 redis 的分工拿到“基于skynet框架的mysql与redis游戏服务器源码.zip”这类包第一反应通常是改个 IP、启动服务端这恰恰是翻车的开始。这类源码真正值钱的不是那套能走的登录协议而是它把 skynet 的 actor 并发模型、mysql 的强一致落盘、redis 的高吞吐缓存切成了一条可扩展的数据链路。它能解决的是典型 socket 长连接游戏服务端问题几十万在线连接下怎么不卡用户操作玩家背包、排行榜、跨服会话这类冷热数据怎么存才不丢又不炸。适合有一定 Lua 基础、想拿开源服务端做独立产品或者正准备从单机 socket 迁到框架的同学。2. 先看懂并发模型再动手改配置skynet 是怎么驱动 mysql 与 redis 的这类包解压后目录结构一般按四块组织skynet 框架本体、服务端 lua 业务脚本、sql 初始化脚本、部署说明。先别急着进service/改代码把数据层的主干读懂后面所有调参才有依据。skynet 是云风写的 CLua actor 框架业务逻辑全写在 lua 里mysql 和 redis 只是数据层的两个出口。如果你一进来就找“启动入口”会看到一堆.lua文件和一个skynet可执行文件真正决定架构的不是入口而是服务之间怎么发消息、谁在管数据。2.1 节点、服务与消息循环几十万连接靠什么撑住skynet 的核心理念是 actor 模型每个独立服务service跑在自己的 lua state 里服务之间不共享内存只能通过消息队列通讯。调度器把消息投递给对应服务服务处理完再发回响应。这个模型对游戏服务端特别合适因为它天然把“玩家”“场景”“数据库”隔离成互不干扰的进程内单元。常见的映射方式是每个登录玩家一个 agent 服务负责这个玩家的协议处理场景服务负责战斗广播和 AOIdb 服务和 cache 服务分别封装 mysql 与 redis 访问。玩家变多时就加 agent 服务场景复杂时就拆分场景服务数据库压力大时就给 db 服务扩容。下面是一个最基础的服务骨架你会在包的service/里反复看到这种写法local skynet require skynet skynet.start(function() skynet.error(agent service start, address , skynet.self()) end)逻辑说明skynet.start是服务的入口函数服务启动后进入自己的消息循环skynet.self()返回当前服务的地址其他服务用这个地址给它发消息。注意这里没有while true或任何阻塞调用因为 actor 模型不允许一个服务独占 CPU。参数说明服务间通讯分两种skynet.send是异步发消息不等回复skynet.call是阻塞等待返回。游戏逻辑里请求加载玩家数据必须用call因为后续代码依赖返回结果而推送公告、广播战斗事件用send即可。写业务时最容易犯的错是在 agent 服务里直接mysql.query这个阻塞调用会卡住整个 agent 的消息循环后面来的协议全部超时。正确做法是把数据库访问转发给 db 服务自己继续处理其他消息。2.2 mysql 与 redis 的分工为什么不是“全存 redis”也不是“全扔 mysql”不少新手拿到包后会问既然 redis 这么快玩家数据全放 redis 不就行了答案是不行redis 是内存数据结构服务器不具备完整的 ACID 事务和行级持久化能力掉电丢数据是常态。反过来mysql 能持久化但扛不住排行榜这种每秒几千次的原子自增操作。这套源码把两者切成互补关系数据类别中间件选型理由账号注册信息、角色属性mysql必须强一致落盘不能丢在线状态、会话 tokenredis高频读写临时性数据玩家背包热数据redis mysql在线时用 redis 快速存取定期回写 mysql竞技场/PVP 排行榜redis zset自带排序和区间查询O(logN) 写入跨服转服、活动开关锁redis setnx原子性获取锁配合过期时间防死锁全服邮件、长文本日志mysql复杂条件查询和持久化需求我一般判断一条数据的归属就两个问题这条数据丢了会不会让玩家投诉会就 mysql。这条数据每秒会被读写多少次超过百次就 redis。二者重叠的部分就是“热数据”也就是玩家在线期间频繁变动、离线后又要保留的字段这类数据走 redis 缓存加 mysql 定期落盘。2.3 在源码里定位关键服务登录、agent、db 与 cache拿到包后第一步不是读代码而是用命令把服务依赖摸清楚。这类包常见路径是service/放业务服务lualib/放公共库sql/放建表脚本bin/放编译产物。用 grep 可以快速确认 mysql 和 redis 在哪层被创建grep -rn mysql.create\|db.mysql --include*.lua service/ | head -20 grep -rn redis.create\|db.redis --include*.lua service/ | head -20逻辑说明第一条命令找 mysql 封装在哪个服务里创建第二条找 redis。正常情况你会看到db.lua或cache.lua这类集中封装文件而不是每个业务文件里散落一堆数据库连接。参数说明看到连接创建代码后注意连接的参数来源。多数包会把 mysql 的 host、port、user、password 和 redis 的 host、port、auth 统一放在 config 文件里用skynet.getenv读取。如果发现参数写死在 lua 里说明这个包的设计比较粗糙部署时改配置会特别痛苦。另外看skynet.call的目标服务名如果用的是数字地址而非服务名说明服务启动顺序是写死的后续加节点就要改顺序逻辑这是很多人迁移到多节点部署时才发现的问题。3. 在 Linux 上把服务端跑起来从装依赖到收到第一条协议跑通这种包最高效的路径是先在单台 Linux 上把 mysql、redis、skynet 三件事都拉起来再去看协议。不要一上来就想着多节点单机跑通能排除八成环境问题。3.1 编译 skynet 的最小依赖集与常见报错skynet 本体是 C 写的编译前需要装基础构建工具和两个关键开发库mysql 客户端库和 hiredis。前者是skynet.db.mysqlC 模块的依赖后者是 redis 客户端模块的依赖。Debian/Ubuntu 系执行apt-get update apt-get install -y build-essential git autoconf libreadline-dev \ default-libmysqlclient-dev libhiredis-dev cd skynet make linux逻辑说明libreadline-dev是 skynet 调试控制台依赖的缺失时编译能过但启动后敲不了调试命令libmysqlclient-dev和libhiredis-dev是数据层模块的硬依赖不装会出现luaopen_skynet_db_mysql not found这类运行时错误。参数说明make linux是 linux 平台标准目标macOS 用make macosx。如果源码包里带了第三方的 lua 版本编译前先看根目录 Makefile 的LUA_DIR和CFLAGS确认它用的是系统 lua 还是内置 lua。常见翻车是编译时报lua.h: No such file or directory说明 lua 头文件路径不对把LUA_DIR指到包内 lua 目录即可。3.2 初始化 mysql建库建表再处理认证插件源码包的sql/目录里通常有建表脚本但不少包只给了核心表业务表要自己补。下面是一份最常见的玩家数据模型包含角色主表和背包表字段设计基本能支撑登录、存档、背包三个核心环节CREATE DATABASE IF NOT EXISTS game_db DEFAULT CHARSET utf8mb4; USE game_db; CREATE TABLE IF NOT EXISTS player ( player_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, account VARCHAR(64) NOT NULL, name VARCHAR(32) NOT NULL DEFAULT newbie, level INT NOT NULL DEFAULT 1, gold BIGINT NOT NULL DEFAULT 0, vip TINYINT NOT NULL DEFAULT 0, last_login_time TIMESTAMP NULL DEFAULT NULL, last_logout_time TIMESTAMP NULL DEFAULT NULL, profile JSON NULL, PRIMARY KEY (player_id), UNIQUE KEY uk_account (account) ) ENGINEInnoDB; CREATE TABLE IF NOT EXISTS player_bag ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, player_id BIGINT UNSIGNED NOT NULL, item_id INT NOT NULL, count INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY idx_player_item (player_id, item_id) ) ENGINEInnoDB;逻辑说明player表用player_id做自增主键account加唯一索引保证一个账号只有一条主角色记录gold和vip用DEFAULT 0防止插入时漏字段profile字段用 JSON 类型存可变属性避免频繁 ALTER TABLE。player_bag用(player_id, item_id)联合索引支撑“查某个玩家的全部背包物品”和“查某一个物品数量”两类高频查询。参数说明建库务必指定utf8mb4否则角色名带中文表情符号时会报Incorrect string value。表引擎用 InnoDB别用 MyISAM游戏存档的并发更新场景下 MyISAM 表锁会直接把 db 服务拖死。建完表后创建连接账号CREATE USER skynet127.0.0.1 IDENTIFIED BY dev_pass; GRANT ALL PRIVILEGES ON game_db.* TO skynet127.0.0.1; FLUSH PRIVILEGES;如果 mysql 版本是 8.0 以上默认认证插件是caching_sha2_password老版本libmysqlclient握手时会失败报Authentication plugin caching_sha2_password cannot be loaded。开发环境可以用下面的语句暂时兼容生产环境建议换新版客户端驱动而不是降级认证ALTER USER skynet127.0.0.1 IDENTIFIED WITH mysql_native_password BY dev_pass;注意mysql_native_password在 mysql 8.4 里已标记废弃新项目直接使用支持 caching_sha2_password 的客户端库更省心。3.3 redis 准备先把访问安全与持久化配好redis 的安装比 mysql 省事但配置文件里几个默认值对游戏服务器不友好。我一般会改redis.conf里的四处bind 127.0.0.1 protected-mode yes port 6379 daemonize yes requirepass dev_redis_pass appendonly yes参数说明bind 127.0.0.1表示只允许本机访问如果你的 mysql 和 redis 与 skynet 不在同一台机器把 bind 改成内网 IP 并配合防火墙白名单protected-mode yes在无密码时会拒绝外部连接算一道保险requirepass是访问密码skynet 连接时需要在 config 里配置同样的 auth。appendonly yes开启 AOF 持久化redis 在游戏服里主要做缓存但排行榜和在线状态这类数据丢了也会出问题AOF 比 RDB 快照更能减少丢失窗口。写完后启动并验证redis-server /etc/redis/redis.conf redis-cli -a dev_redis_pass ping能返回PONG说明 redis 可用。注意redis-cli -a会在命令行里暴露密码生产环境建议用REDISCLI_AUTH环境变量代替。3.4 配置 skynet.config 并启动验证skynet 的配置是一段 lua 代码通常叫config或config.cfg。里面既要有框架本身的调度参数也要有数据层连接信息。下面是一份能用的最小配置thread 4 logger log/skynet.log logpath log/ cpath ./luaclib/?.so luaservice ./lualib/skynet/?.lua;./lualib/skynet/?/init.lua;./service/?.lua;./service/?/init.lua start main bootstrap snlua bootstrap mysql { host 127.0.0.1, port 3306, database game_db, user skynet, password dev_pass, max_packet_size 1024 * 1024, pool_size 4 } redis { host 127.0.0.1, port 6379, auth dev_redis_pass }逻辑说明thread 4是 skynet 调度线程数不是越大越好一般按 CPU 物理核心数设置数据层服务多时可以加到 8luaservice决定启动时去哪找服务脚本start main指定第一个服务是service/main.lua由它去启动后续服务mysql.pool_size是连接池大小下面章节展开讲。参数说明max_packet_size对应 mysql 的max_allowed_packet如果玩家存档里有较大的二进制数据默认 1MB 可能不够改成 4MB 或 8MB同时 mysql 服务端也要同步调整。注意配置文件里不要写中文注释skynet 对配置文件编码敏感容易解析失败。启动验证./skynet config正常会看到日志里出现服务启动顺序例如main service start、cache service bind port之类。接着分别在 mysql 和 redis 里确认数据已通mysql -h127.0.0.1 -uskynet -pdev_pass game_db -e SELECT COUNT(*) FROM player; redis-cli -a dev_redis_pass DBSIZE如果能查到player表存在且 redis 的 key 数量不为零说明框架、mysql、redis 三端已经打通。如果 redis 里 key 数量持续为零优先检查账服或登录服是否没起来而不是先怀疑 redis。4. 数据是怎么流动的从 redis 缓存回写到 mysql 的完整链路这一章是整个源码包最值得读的部分。登录时数据从哪来、在线时数据存在哪、下线时数据怎么回去这三条链路决定你后面加功能时改哪里。4.1 玩家登录与数据加载先读 redismiss 再查 mysql常见的实现是玩家登录后agent 服务先去 cache 服务要玩家数据cache 没有再去 db 服务要db 查完回填 redis。这样设计能显著降低 mysql 的读压力在线人数越高收益越明显。伪代码如下local skynet require skynet local cjson require cjson function PlayerAgent:load(player_id) local ok, data skynet.call(cache, lua, get_player, player_id) if ok and data then self.data cjson.decode(data) return end local db_player skynet.call(db, lua, query_player, player_id) if db_player then self.data db_player skynet.send(cache, lua, set_player, player_id, cjson.encode(db_player), 300) end end逻辑说明先走 redis 读缓存命中就直接解析使用不查库未命中才走 db 服务查 mysql。回填缓存时设置 300 秒过期时间避免玩家长期不在线时缓存里堆积冷数据。注意这里回填用的是skynet.send因为 agent 不需要等 cache 回复异步写入即可。参数说明TTL 设 300 秒是折中方案。玩家频繁登录的游戏可以设 600 秒签到类活动多的游戏建议 60 到 120 秒避免玩家改完签名后重登还看到旧值。另外查询 player 时必须用参数化查询或mysql.format直接拼接字符串会把 SQL 注入带进生产环境这种包里的 demo 代码普遍有这个毛病接手后第一件事就是替换。4.2 脏标记与延迟落盘redis 攒数据mysql 定期收玩家在线时金币、体力这类高频字段没必要每次都写 mysql常见做法是加一层脏标记。数据先写 redis由定时器把脏数据批量刷回 mysqllocal SAVE_INTERVAL 30 local dirty {} local function mark_dirty(player_id) dirty[player_id] true end skynet.timer(SAVE_INTERVAL, function() local batch dirty dirty {} for pid in pairs(batch) do local data skynet.call(cache, lua, get_player, pid) if data then skynet.send(db, lua, save_player, pid, data) end end end)逻辑说明mark_dirty由业务逻辑调用玩家金币变化时记一个脏标记不立即写库。定时器每 30 秒把当前脏集合取出逐一向 db 服务发送保存请求。注意这里先把batch取出来再清空dirty否则玩家在回写过程中修改数据会丢更新。参数说明SAVE_INTERVAL是落盘间隔设 30 秒比较均衡追求掉线恢复速度就改 10 秒但 mysql 写入 QPS 会涨三倍。更稳的做法是加“下线兜底”玩家登录时如果 redis 里有旧数据先触发一次强制保存再踢下线防止定时器还没来得及跑就掉线的窗口期丢数据。4.3 redis 数据结构与业务映射排行榜、锁、PVE 战斗redis 的优势不只在缓存它的数据结构本身就对应游戏里的常见玩法抽象数据结构典型玩法命令示例注意点String金币、体力、会话 tokenSET / GET / INCRBY注意原子自增用 INCRBY不要读改写Hash玩家属性集合HSET / HGETALL适合一次性拉取整包属性ZSet排行榜、匹配分ZADD / ZREVRANGEscore 用整数避免浮点精度问题Set在线列表、公会成员SADD / SISMEMBER适合集合运算List邮件队列LPUSH / BRPOP阻塞式弹出可用于消息队列排行榜是游戏服务器里最容易写错的部分。示例ZADD pvp_rank 9527 player_10001 ZADD pvp_rank 8810 player_10002 ZREVRANGE pvp_rank 0 9 WITHSCORES逻辑说明ZADD写入战力分数ZREVRANGE按分数从高到低取前 10 名。这里的关键是把 score 存成整数很多人直接存带小数点的战力值分数一多就会出现9526.9999这种误差导致排名跳动。常见做法是战力统一乘以 10000 转成整数再入库展示层再除以 10000。另一个高频场景是跨服和活动竞态比如同时在线领取同一个限时奖励。用 redis 分布式锁保护临界区local key KEYS[1] local val ARGV[1] local ttl tonumber(ARGV[2]) if redis.call(SET, key, val, NX, PX, ttl) then return 1 end return 0逻辑说明这段脚本用SET key value NX PX ttl实现原子性加锁NX表示 key 不存在时才写入PX设置毫秒级过期时间。调用方拿到返回值 1 表示抢锁成功0 表示锁已被持有执行业务后主动 DEL 释放锁。用 Lua 脚本是为了让“检查 key、写 key、设过期”这三步在 redis 服务端原子执行避免并发下两个请求同时拿到锁。参数说明锁的 TTL 要大于临界区最大执行时间否则锁提前过期导致资源仍被并发访问。一个务实的习惯是把 TTL 设成业务耗时的 5 倍并在业务结束时用 val 校验是不是自己的锁再删防止误删别人新拿到的锁。4.4 mysql 连接池与慢请求隔离db 服务不能成为全局瓶颈很多包把 mysql 查询直接写在 agent 服务里这是架构上最大的隐患。agent 一多每个 agent 各建一条 mysql 连接连接数轻松破百mysql 默认max_connections只有 151线上就直接报Too many connections。规范做法是让 db 服务统一持有连接池其他服务通过消息请求发送 SQLlocal mysql require skynet.db.mysql local pool {} for i 1, pool_size do pool[i] mysql.create({ host 127.0.0.1, port 3306, database game_db, user skynet, password dev_pass, max_packet_size 1024 * 1024 }) end local idx 1 function get_conn() local conn pool[idx] idx idx % pool_size 1 return conn end逻辑说明pool_size条连接启动时全部建好之后查询从池里轮询取连接用idx做简单的取模轮转。这样整个 skynet 进程对 mysql 的连接数就是固定的不会随玩家数上涨。如果业务里有跨多行的事务注意在mysql.create时配置transaction start确保事务和事务之间不共用连接。参数说明pool_size一般设为thread数的 1.5 到 2 倍比如 4 线程就建 6 到 8 条连接。再往上加收益衰减明显因为 skynet 的消息循环在同一时刻能同时执行的查询数量受限于调度线程数。连接池不是越大越好大了反而增加 mysql 端线程切换成本。注意db 服务内部不要串行执行多个慢查询一个ORDER BY RAND()或全表COUNT(*)会把后面所有玩家数据请求堵住。遇到慢日志里有这类语句第一时间拆分到独立服务或改成 redis 计数器。5. 五个高频踩坑点这类源码包最容易翻车的地方前面把正向链路跑通后下面这些坑是你大概率会遇到的每一条都是实打实的血泪经验。5.1 现象连接 mysql 报 error 2002 (HY000)cant connect through socket /tmp/mysql.sock原因skynet 的 mysql 模块默认走 TCP但某些包在代码里写了socket /tmp/mysql.sock参数。实际部署时 mysql 的 socket 路径可能是/var/run/mysqld/mysqld.sock路径对不上就连不上。另外如果 mysql 服务没起来或 bind 配置仅监听 localhost也会报同样错误。解决先确认 mysql 在跑且端口正常systemctl status mysql ss -lntp | grep 3306然后统一改用 TCP 方式连接即配置里只写host 127.0.0.1和port 3306把socket参数删掉。如果确实需要走 socket查看 mysql 实际路径mysql -e SHOW VARIABLES LIKE socket;把查到的路径填到代码里。5.2 现象导入 sql 脚本成功但 skynet 启动时连数据库报缓存认证插件失败原因mysql 8.0 默认的caching_sha2_password认证插件与部分 skynet 自带的旧版libmysqlclient不兼容。报错信息一般是Authentication plugin caching_sha2_password cannot be loaded或Reading from the stream has failed。解决将连接用户改为兼容的认证插件ALTER USER skynet127.0.0.1 IDENTIFIED WITH mysql_native_password BY dev_pass; FLUSH PRIVILEGES;注意这不是长久之策mysql 8.4 开始mysql_native_password默认禁用。更干净的方案是重新编译 skynet链接新版 MySQL Client Library或者让包内 mysql 模块走纯 Lua 动态库方案。开发环境用ALTER USER最快生产环境必须走新驱动。5.3 现象排行榜名次乱跳玩家战力相同但顺序随机原因redis 的ZSet按 score 排序当 score 是浮点数时精度损失会导致 123.456 和 123.455 被当成同一个值或排序错位。另一个常见误操作是先ZSCORE取回分数加完后再ZADD两步之间玩家并发改分产生覆盖。解决score 全部转成整数存储比如战力乘以 10000需要支持小数展示时展示层再除回来。积分变更一律用原子命令ZINCRBY pvp_rank 10 player_10001ZINCRBY在服务端原子完成加减不需要先读后写。如果排行榜还要求同分时按时间先后排序可以用“分数 * 1e10 (某个时间戳基准值 - 注册时间)”的复合 score 编码。5.4 现象角色改名或充值到账后重新登录看到的还是旧数据原因数据加载链路只做了“读 redismiss 查 mysql回填 redis”但业务修改后没有让缓存失效。玩家改名后如果更新的是 mysqlredis 里还是旧值下一次登录 redis 命中直接返回旧数据。解决凡是修改核心展示字段的接口写完数据后必须同步刷新缓存。两个姿势一是直接DEL缓存 key让下次登录重新加载二是更新 mysql 的同时也更新 redis 里的对应 hash 字段。我习惯用 DEL简单可靠配合 4.1 的 TTL 冷数据机制缓存重建成本很低。排查这类问题可以用 RedisInsight 这类可视化客户端连上 redis直接看玩家的 key 里的值是不是最新几秒就能确认是缓存没失效还是写库失败。5.5 现象多节点部署后同一角色在 A 节点下线B 节点的数据反而被覆盖原因这是原包最常见的单机假设问题。玩家在 A 节点登录agent 持有玩家数据网络抖动后重连到 B 节点。B 节点重新加载数据A 节点的 agent 还活着定时器继续把旧数据写回 mysql把 B 节点的新数据冲掉。解决在 redis 里保存玩家会话绑定关系key 为session:{account}value 为节点地址和 agent 地址并设置过期时间。玩家在 B 节点登录时先查这个 key如果已有会话先通知 A 节点强制下线并保存再允许 B 节点登录。同时在玩家数据表加一个version字段回写时用UPDATE ... WHERE version ?更新成功后版本号加一旧节点回写时版本不匹配就丢弃。这层逻辑在单机跑的时候不触发上多节点之前必须补上。6. 从单节点压测到多节点mysql 与 redis 的扩展边界在哪6.1 用一个压测脚本找到数据层拐点跑通业务后别急着加玩法先压一把数据层。下面这个 Bash 脚本模拟并发登录观察哪一层先到瓶颈for i in $(seq 1 200); do redis-cli -a dev_redis_pass INCR login_count /dev/null mysql -h127.0.0.1 -uskynet -pdev_pass game_db \ -e INSERT INTO player_log(account, action) VALUES(bench_$i, login); done wait逻辑说明脚本同时向 redis 发自增命令、向 mysql 插日志两端压力同时上来。跑完后看两个指标redis 的INFO stats里的instantaneous_ops_per_sec和 mysql 的SHOW PROCESSLIST里的线程数。参数说明如果 mysql 线程数先冲破 100说明连接池不够或慢查询拖住了连接如果 redis 的ops/sec到 5 万后 CPU 打满说明单机 redis 到极限。这时再看 skynet 日志里有没有timeout报错没有就说明框架层还能扛瓶颈在存储层。6.2 三个边界参数的观测习惯我维护这种架构时固定看三个参数mysql 慢查询日志设long_query_time 1超过 1 秒的 SQL 全部记录拿到就优化索引或换 redis。redis 内存碎片率用INFO memory里的mem_fragmentation_ratio观测超过 1.5 说明碎片严重考虑重启或调整jemalloc配置。skynet 的thread不是越高越好压测时把thread从 4 加到 8QPS 不涨反而掉说明锁竞争已占主导问题出在服务划分而不是线程数。到单节点撑不住的时候先后撤数据层再撤服务层mysql 按区服分库redis 按主键 hash 拆实例都不要一上来就拆服务。我自己每次搭这种包都坚持先定回写协议再写功能先开慢日志再压测这套顺序跑顺了单区扛几千在线不玄学。希望这一篇能帮你把基于 skynet 的 mysql 与 redis 数据链路理清楚少走几步弯路。本文还有配套的精品资源点击获取