news 2026/10/9 16:41:55

skynet游戏服务端源码解析:MySQL与Redis分工及避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
skynet游戏服务端源码解析:MySQL与Redis分工及避坑指南

简介:这份资源是面向游戏服务器开发者的完整源码包,基于轻量级高并发框架Skynet构建,重点解决游戏后端与MySQL、Redis两类数据库的协同交互问题。源码中实现了数据库访问层,将游戏逻辑产生的数据操作转换为SQL语句执行,并设计了缓存管理机制,把热点数据存入Redis以减轻数据库压力、加快读取速度,同时涉及连接池、异步IO处理与负载均衡等性能优化思路,适合具备一定Lua与网络编程基础、希望深入理解游戏服务器架构的开发者参考学习。压缩包共22个文件,约619KB,以lua脚本为主体,辅以py、pyc、proto、pb等文件,涵盖服务模块、配置节点、客户端与协议定义等内容,目录结构清晰。目前已有132人学习下载,可帮助读者梳理Skynet服务划分、数据库交互与缓存设计的实现脉络,并从中借鉴监控日志与安全防护的工程思路。

1. 从一份 skynet 游戏服务端源码说起:MySQL 与 Redis 到底怎么分工

如果你正在找一个能跑起来的游戏服务端骨架,而不是那种只有目录结构的空壳,这份基于 skynet 框架、同时接了 MySQL 和 Redis 的源码包值得拆一拆。skynet 是轻量级 actor 模型的服务端框架,每个服务是一个独立 Lua 虚拟机,服务之间靠消息通信,天然适合做网关、战斗、排行榜这类需要横向拆分的模块。这份源码把持久化层拆成两路:MySQL 存账号、角色、背包这类必须落盘且要事务保证的数据;Redis 扛排行榜、在线状态、跨服缓存这类高频读写、允许短暂不一致的数据。适合谁?正在搭第一个游戏服务端、或者手里有 skynet 但持久化层一直没理顺的开发者。下面按「先跑通、再拆模块、最后避坑」的顺序走一遍。

2. 环境搭建与首次启动:从零把服务跑起来

2.1 依赖清单与版本选择

skynet 对系统库有硬依赖,MySQL 和 Redis 的客户端库又各自有坑,先把版本对齐能省掉一半的编译报错。我一般会固定下面这套组合,不追最新版,因为 skynet 的 C 模块对 Lua 版本和编译选项比较敏感。

组件建议版本说明
操作系统Linux x86_64skynet 在 Linux 下编译最顺,macOS 需要额外处理动态库路径
Lua5.4skynet 自带 lua 源码,但第三方 C 模块要按 5.4 编译
skynet随源码包附带不要单独去拉最新版,源码包里的版本和业务代码是配套的
MySQL5.7 或 8.08.0 默认认证插件变了,老客户端连不上要改
Redis5.0 以上用到 stream 或新命令才需要 6.x,普通缓存 5.0 够
gcc / make系统自带编译 skynet 和 C 服务用

版本选完,先确认系统里有没有这几个开发库,缺了会在 make 阶段报cannot find -lmysqlclient这类错。

# 检查 MySQL 和 Redis 客户端开发库是否就位 ldconfig -p | grep mysqlclient ldconfig -p | grep hiredis # 如果没有,按发行版装(以 Debian 系为例) apt-get install -y libmysqlclient-dev libhiredis-dev

这两条命令的作用是确认动态链接库能被系统找到。ldconfig -p列出当前缓存里的所有共享库,grep 过滤出目标。如果输出为空,说明开发库没装,编译时链接阶段一定失败。装完库之后不需要重启,但要让 ldconfig 重新扫描一次,部分发行版装包时自动做了。

2.2 编译 skynet 与启动顺序

skynet 的编译分两步:先编框架本身,再编业务用到的 C 服务。源码包里通常有一个makeall.sh或者顶层 Makefile,但直接跑之前先看一眼 3rd 目录下有没有需要单独编的库。

# 进入源码根目录 cd skynet-server # 编译 skynet 核心(-j 后面跟 CPU 核数,加快编译) make linux -j4 # 如果源码包把 C 服务单独放了目录,进对应目录再 make cd service && make

make linux是 skynet 的标准编译目标,它会编译出skynet可执行文件和一批基础 C 服务。-j4是并行编译,核数越多越快,但报错信息会交错,第一次编译建议不加-j,方便定位错误。编译完成后根目录会出现skynet可执行文件,这是整个服务端的入口。

启动顺序有讲究:MySQL 和 Redis 必须先于 skynet 就绪,否则 skynet 里的连接服务会在初始化阶段直接失败退出。我习惯写一个启动脚本把顺序固定下来。

# 先确认 MySQL 和 Redis 在跑 systemctl status mysql systemctl status redis # 再启动 skynet,配置文件路径按源码包实际位置改 ./skynet config/config.lua

config/config.lua是 skynet 的启动配置,里面定义了thread数量、bootstrap入口服务、logger路径等。thread一般设成 CPU 核数,设太大反而因为上下文切换掉性能。bootstrap指向第一个被启动的服务,通常是main服务,由它再去拉起网关、数据库代理等模块。启动后如果看到日志里打印出各服务启动成功的记录,说明骨架跑通了。

2.3 数据库连接配置怎么改

源码包里数据库配置一般集中在一个 Lua 文件里,常见命名是config/db.lua或config/mysql.lua。改之前先确认 MySQL 里已经建好了对应的库和表,表结构通常在sql/目录下有一个.sql文件。

-- config/db.lua 示例结构 return { mysql = { host = "127.0.0.1", port = 3306, database = "game_db", user = "game_user", password = "your_password", charset = "utf8mb4", max_conn = 8, -- 连接池上限 timeout = 5000, -- 毫秒 }, redis = { host = "127.0.0.1", port = 6379, db = 0, auth = "", -- 没设密码就留空 pool_size = 4, }, }

max_conn是 MySQL 连接池上限,游戏服务端一般不需要太大,8 到 16 足够,因为 skynet 的数据库代理服务是串行处理请求的,连接开多了也是排队。timeout设 5000 毫秒是保守值,跨机房部署要适当放大。Redis 的db索引按业务分,比如 0 号库存会话、1 号库存排行榜,避免不同业务互相干扰。auth留空表示没设密码,生产环境一定要设。

提示:改完配置不要急着全量启动,先用一个最小测试服务连一下两个库,确认账号密码和网络都通,再启动完整业务。

3. MySQL 持久化层:账号、角色与背包的落盘逻辑

3.1 为什么用连接池而不是每次新建连接

游戏服务端对数据库的请求特点是「高频、短小」——一次登录要查账号、查角色、更新最后登录时间,可能几百毫秒内就有好几个查询。如果每个查询都新建一条 MySQL 连接,光 TCP 握手和认证就吃掉大部分时间,QPS 根本上不去。连接池的做法是启动时建好固定数量的连接,请求来了从池里取,用完还回去,省掉反复建连的开销。

源码包里通常有一个mysql_pool.lua或类似文件,核心逻辑是维护一个空闲连接队列。取连接时如果队列为空且未达上限就新建,达到上限就等待。这里有个容易翻车的点:等待没有超时的话,一旦有连接泄漏(借了没还),整个池会被慢慢耗尽,表现为服务运行一段时间后所有数据库请求卡死。

-- 连接池取连接的核心逻辑(简化示意) function pool:acquire() local conn = table.remove(self.idle) if conn then return conn end if self.count < self.max_conn then self.count = self.count + 1 return self:create_conn() end -- 池满,等待归还,带超时 return self:wait_for_idle(self.timeout) end function pool:release(conn) if conn.broken then self.count = self.count - 1 return end table.insert(self.idle, conn) end

acquire先从空闲队列拿,拿不到且没到上限就新建,到了上限就等。release时如果连接已损坏(比如查询超时被服务端断开),要把它从计数里减掉,不能放回空闲队列,否则下次取出来还是坏的。这个「坏连接检测」是很多简易连接池漏掉的一步,也是线上偶发查询失败的常见原因。

3.2 角色数据的读写分离与事务边界

角色数据里,有些操作必须在一个事务里完成,比如「扣元宝 + 加道具」,两步要么都成功要么都回滚。源码包里一般会把这类操作封装成一个存储过程或者在 Lua 层用事务包起来。

-- 扣元宝加道具,放在一个事务里 local conn = mysql_pool:acquire() conn:query("BEGIN") local ok1 = conn:query(string.format( "UPDATE role SET gold = gold - %d WHERE rid = %d AND gold >= %d", cost, rid, cost)) if not ok1 or conn:affected_rows() == 0 then conn:query("ROLLBACK") mysql_pool:release(conn) return false, "gold_not_enough" end local ok2 = conn:query(string.format( "INSERT INTO bag (rid, item_id, count) VALUES (%d, %d, %d) " .. "ON DUPLICATE KEY UPDATE count = count + %d", rid, item_id, count, count)) if not ok2 then conn:query("ROLLBACK") mysql_pool:release(conn) return false, "insert_failed" end conn:query("COMMIT") mysql_pool:release(conn) return true

这段逻辑的关键在UPDATE语句里的AND gold >= cost条件。把余额判断放进 SQL 的 WHERE 里,而不是先查再判断再更新,能避免并发下的超扣问题——两个请求同时读到余额足够,各自扣一次,结果扣成负数。放进 WHERE 后,数据库的行锁保证只有一个能更新成功,另一个affected_rows为 0,直接回滚。ON DUPLICATE KEY UPDATE是 MySQL 的语法,道具已存在就累加数量,不存在就插入,省掉一次查询。

3.3 表结构设计与索引注意点

源码包的sql/目录里通常有建表语句,直接导入即可,但有几个字段设计值得留意。角色表的主键一般是rid(角色 ID),账号表主键是account,背包表用(rid, item_id)做联合唯一索引。如果背包表只建了自增主键没建联合唯一索引,ON DUPLICATE KEY UPDATE就不生效,会插入重复道具。

-- 背包表的关键索引 CREATE TABLE `bag` ( `id` bigint NOT NULL AUTO_INCREMENT, `rid` bigint NOT NULL, `item_id` int NOT NULL, `count` int NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_rid_item` (`rid`, `item_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

uk_rid_item这个联合唯一索引是ON DUPLICATE KEY UPDATE能工作的前提。没有它,每次加道具都会插新行,背包里同一个道具出现多条记录,读取时还要做聚合,越往后越慢。另外count字段用int而不是varchar,避免字符串比较带来的隐式转换和索引失效。

注意:导入建表语句前先确认 MySQL 的sql_mode,如果开了STRICT_TRANS_TABLES,插入超长字符串会直接报错而不是截断,这在不同环境迁移时经常导致「本地能跑线上报错」。

4. Redis 缓存层:排行榜、会话与跨服数据

4.1 排行榜用有序集合而不是数据库排序

排行榜是 Redis 在游戏服务端里最典型的用法。如果用 MySQL 做,每次刷新排行榜都要ORDER BY score DESC LIMIT 100,数据量一大就慢,而且高频刷新会把数据库拖垮。Redis 的ZSET(有序集合)天生就是干这个的,插入和更新是 O(log N),取前 N 名是 O(log N + M)。

-- 更新玩家分数并获取排名 local redis = redis_pool:acquire() -- ZADD 更新分数,分数变了排名自动调整 redis:zadd("rank:level", score, "role:" .. rid) -- ZREVRANGE 取前 100 名,带分数 local top = redis:zrevrange("rank:level", 0, 99, "WITHSCORES") -- ZREVRANK 查某个玩家当前排名(从 0 开始,展示时加 1) local rank = redis:zrevrank("rank:level", "role:" .. rid) redis_pool:release(redis)

ZADD的分数用等级或者战力值,成员用role:rid这种带前缀的字符串,方便区分不同业务的数据。ZREVRANGE从高到低取,WITHSCORES让返回结果带上分数,省一次查询。ZREVRANK返回的是从 0 开始的排名,前端展示要加 1。这里有个细节:如果玩家分数没变,重复ZADD不会产生新成员,只是更新分数,所以可以放心地在每次分数变化时调用。

4.2 会话与在线状态:过期时间是关键

玩家登录后,服务端需要记录「这个账号当前在哪个网关、哪个角色在线」,这类数据放 Redis 并设置过期时间,比放内存更可靠——网关重启后状态还在,玩家不会莫名掉线。源码包里一般用SETEX或SET带EX参数来写。

-- 记录在线状态,30 分钟过期 local key = "online:" .. account redis:setex(key, 1800, gateway_id) -- 心跳时续期 redis:expire(key, 1800) -- 查询某账号在哪个网关 local gw = redis:get(key)

setex的第二个参数是秒数,1800 表示 30 分钟。心跳续期用expire重置过期时间,这样只要玩家还在线,key 就不会消失;玩家异常掉线没发心跳,30 分钟后 key 自动过期,状态自然清理,不需要额外的清理任务。这个「过期即下线」的设计比手动维护在线列表省事得多,也避免了服务崩溃后残留脏数据。

4.3 缓存与数据库的一致性处理

Redis 里的数据和 MySQL 里的数据难免有短暂不一致,比如玩家改了昵称,MySQL 更新成功但 Redis 更新失败。源码包里常见的处理策略是「先写库,再删缓存」,而不是「先写库,再更新缓存」。

-- 更新昵称:先写 MySQL,再删 Redis 缓存 local ok = mysql:query(string.format( "UPDATE role SET name = '%s' WHERE rid = %d", new_name, rid)) if ok then redis:del("role:info:" .. rid) -- 删缓存,下次读时重建 end

为什么是删而不是更新?因为更新缓存需要把完整数据重新组装一遍,如果组装逻辑和读取逻辑不一致,缓存里就会留下错误数据。删掉之后,下次读取时走「缓存未命中 → 查库 → 写缓存」的标准流程,数据一定是库里的最新值。这个策略的代价是下一次读会穿透到数据库,但游戏场景里昵称修改频率很低,完全可以接受。

提示:删缓存也可能失败,如果对一致性要求高,可以在删失败后把 key 丢进一个重试队列,由后台服务异步补偿。

5. 避坑与排查:那些让服务半夜挂掉的细节

5.1 连接池耗尽导致所有请求卡死

现象:服务运行几小时后,所有涉及数据库的操作全部超时,日志里没有明显报错,只是请求一直不返回。

原因:某条代码路径借了连接但没归还,通常是查询中途抛异常,release没被执行到。连接池的空闲队列逐渐见底,新请求全部卡在等待上。

解决:把acquire和release用pcall包起来,确保异常时也能归还。更稳妥的做法是在连接池里加一个「借出时间戳」,后台定时扫描超过阈值未归还的连接,强制回收并打日志,这样即使有泄漏也能自愈。

5.2 MySQL 8.0 认证插件不兼容

现象:本地用 MySQL 5.7 一切正常,换到 MySQL 8.0 后连接直接报Authentication plugin 'caching_sha2_password' cannot be loaded。

原因:MySQL 8.0 默认认证插件从mysql_native_password换成了caching_sha2_password,老版本的客户端库不认识。

解决:要么升级客户端库,要么把账号的认证插件改回去:ALTER USER 'game_user'@'%' IDENTIFIED WITH mysql_native_password BY 'password';。改完FLUSH PRIVILEGES生效。生产环境建议直接升级客户端库,改插件只是临时方案。

5.3 Redis 大 key 拖慢整个实例

现象:排行榜相关操作偶尔卡顿,Redis 慢查询日志里出现ZRANGE耗时几百毫秒。

原因:排行榜的 ZSET 成员数过多,比如把所有玩家都塞进一个 key,几十万成员时ZRANGE虽然复杂度不高,但网络传输和序列化开销大。

解决:按区服或时间段拆分 key,比如rank:level:s1、rank:level:s2,每个 key 只存本服玩家。取全服排名时用ZUNIONSTORE合并,或者干脆只展示本服排名。另外定期清理不活跃玩家,减少成员数。

5.4 时区不一致导致时间字段错乱

现象:数据库里存的时间比实际时间差 8 小时,或者跨时区部署时登录时间对不上。

原因:MySQL 服务端时区、客户端连接时区、Lua 里os.time()的时区三者不一致。

解决:统一用 UTC 时间戳存储,展示时再转本地时区。连接 MySQL 时显式设置SET time_zone = '+00:00',Lua 里用os.time()拿到的就是 UTC 秒数。这样无论服务器部署在哪里,存进去的时间都是可比的。

5.5 skynet 服务阻塞导致消息堆积

现象:某个服务(比如战斗计算)处理变慢,发给它的消息在队列里越堆越多,最终内存暴涨。

原因:skynet 的服务是单线程处理消息的,如果某个消息的处理逻辑里有同步阻塞操作(比如同步查数据库),整个服务的消息队列就会卡住。

解决:把阻塞操作拆到独立的服务里,用异步消息回调的方式处理。比如数据库查询交给专门的db服务,业务服务发请求后继续处理其他消息,等db服务回包再继续。这是 skynet 的核心用法,也是新手最容易违反的一条。

6. 进阶技巧:用压测和日志把问题提前暴露

源码包跑通只是第一步,真正上线前得知道它能扛多少并发。我一般会写一个简单的压测脚本,模拟 N 个玩家同时登录、查角色、写背包,观察 QPS 和延迟。skynet 自带一个skynet.abort和调试控制台,但压测更适合用外部工具。

# 用 wrk 压测网关的登录接口(假设网关监听 8888) wrk -t4 -c100 -d30s --latency \ -s login.lua \ http://127.0.0.1:8888/login

-t4是 4 个线程,-c100是 100 个并发连接,-d30s压 30 秒,--latency输出延迟分布。login.lua是自定义脚本,构造登录请求的 body。重点看Latency的 P99 值,如果 P99 超过 200 毫秒,说明数据库或 Redis 那边有瓶颈,要回去查慢查询。

日志方面,skynet 的logger服务会把日志写到文件,但默认级别可能不够细。我习惯在数据库代理服务里加一条「慢查询日志」,超过 100 毫秒的查询把 SQL 和耗时打出来。

-- 在数据库代理服务里包一层耗时统计 local start = skynet.now() local result = conn:query(sql) local cost = skynet.now() - start if cost > 100 then skynet.error(string.format("slow query: %dms, sql=%s", cost, sql)) end

skynet.now()返回的是厘秒(1/100 秒),所以 100 对应 1 秒?不对,skynet 的now单位是厘秒,100 厘秒等于 1 秒。要统计 100 毫秒应该用 10。这个单位换算我踩过坑,一开始按毫秒算,结果慢查询日志一条都不打,排查半天才发现是单位问题。从那以后我每次用skynet.now()做耗时统计,都强制在代码注释里写清单位,并且用skynet.now() - start的结果和os.clock()对一遍,确认量级没错。

压测和慢日志配合,基本能在上线前把大部分性能问题暴露出来。剩下的就是根据压测结果调连接池大小、Redis 超时、skynet 线程数这些参数,反复几轮,直到 P99 稳定在可接受范围。这套流程走下来,这份源码包就不只是「能跑」,而是「敢用」了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 16:40:53

OA办公系统数据库设计:从RBAC权限到审批流的表结构全解析

简介&#xff1a;《OA办公系统大数据库设计.doc》是一份针对OA办公自动化管理系统的数据库设计说明书&#xff0c;面向系统设计、开发、验收、评审与测试人员。文档采用MSSQL SERVER 2008 R2&#xff0c;数据库名为OASYSDB/OA系统数据库&#xff0c;旨在将数据分析结果整理成计…

作者头像 李华
网站建设 2026/10/9 16:39:27

对偶生成对抗网络去雾实战:从天空翻车到PyTorch落地

简介&#xff1a;本资源为基于PyTorch实现的对偶生成对抗网络图像去雾项目&#xff0c;面向计算机相关专业正在做毕业设计的学生&#xff0c;以及需要项目实战练习的学习者&#xff0c;也可作为课程设计或期末大作业参考。项目包含完整Python源码、训练好的模型权重与文档说明&…

作者头像 李华
网站建设 2026/10/9 16:38:49

FPGA板卡硬件结构与升级原理全解析

1. FPGA板卡不是“黑盒子”&#xff0c;而是可编程的硬件积木场FPGA板卡这个词&#xff0c;最近在硬件开发、边缘计算和工业控制圈子被反复提起&#xff0c;但很多人一听到“FPGA”&#xff0c;第一反应还是“太硬核”“门槛高”“得会Verilog”——其实这是个典型的认知偏差。…

作者头像 李华
网站建设 2026/10/9 16:36:53

IEC 62541-1:2025 RLV 解读:OPC UA 信息建模与地址空间核心概念

简介&#xff1a;IEC 62541-1:2025 RLV 是OPC统一架构&#xff08;OPC UA&#xff09;系列规范的首个部分&#xff0c;面向工业自动化、物联网与工业互联网领域的工程师、系统架构师及技术决策者&#xff0c;为解决跨厂商设备与系统互操作提供标准化参考。资源为单份PDF电子原版…

作者头像 李华
网站建设 2026/10/9 16:22:58

影刀RPA新手教程:网页截图与区域截图——证据留档的用法

影刀RPA新手教程&#xff1a;网页截图与区域截图——证据留档的用法 流程跑挂了想复盘&#xff0c;日志里只有一行报错文字&#xff0c;页面当时长什么样完全不知道&#xff0c;这种抓瞎的感觉我经历过太多次。后来我给每条正式流程都加了截图留档&#xff1a;出错时截图、关键…

作者头像 李华