简介基于Skynet框架的MySQL与Redis游戏服务器源码定位为面向游戏后端开发者的一站式高并发服务器参考实现解决轻量级服务器在网关管理、服务分发与数据存储上的整合问题。压缩包共22个文件整体约619KB核心逻辑集中在10个Lua脚本中另外包含Python测试客户端、Shell一键启动脚本、Proto协议定义及Markdown说明文档目录结构清晰便于按模块阅读和二次开发。目前已有132人学习下载。源码提供了双节点配置、网关gateway、agentmgr与nodemgr服务管理、主入口main.lua等关键模块并展示了Lua服务与MySQL、Redis数据库的实际交互方式涉及连接池、异步消息处理、负载均衡和热点数据缓存等常见性能优化手段覆盖了从客户端请求到后端存储的完整链路。对于想要快速上手Skynet框架、或需要一套可直接运行的中小型游戏服务器代码作参照的开发者这份源码具有较好的学习和复用价值。1. 基于skynet框架的mysql与redis游戏服务器源码打开之前先想清楚它解决什么问题搜到“基于skynet框架的mysql与redis游戏服务器源码.zip”这个词条的人多半刚拿到一份工程压缩包想搞清楚里面是什么、能不能跑、值不值得看。我的结论可能跟多数教程相反这类源码包最值钱的部分不在玩法逻辑而在数据层怎么跟skynet的Actor模型协作。skynet是C语言写的服务端框架业务逻辑跑在Lua虚拟机里服务之间靠消息通信——在哪个环节连MySQL、哪一行代码动Redis、哪个协程一堵导致全服卡顿这些只有真正上线翻过车的人才讲得清。这篇笔记就沿着“框架原理 → 跑通工程 → MySQL接入 → Redis接入 → 避坑 → 上线验证”的顺序把一套典型的skynet游戏服务器数据层方案拆开来讲。适合刚拿到源码包无从下手的开发者也适合已经跑起来但数据层反复出问题的人。读完你能照着复现启动步骤、理解数据层设计惯例并避开那些日志里看不出来的隐性坑。2. 先吃透Actor模型为什么游戏服务器要把每个模块拆成独立小世界2.1 服务、消息队列与回调skynet的并发模型怎么理解skynet并发模型的核心不是线程而是“服务”。每个服务是一个独立执行单元有自己的消息队列和Lua虚拟机状态服务之间不直接共享内存只通过消息通信。框架底层用少量工作线程驱动所有服务的消息队列某个服务的回调函数被触发时消息队列里的消息才会进入执行流程。这个设计和Erlang的Actor模型同源落到游戏服务器上的直接好处是一个玩家逻辑崩溃不会拖垮别的服务数据库访问和网关转发天然隔离。我一般让新手做一个小实验来建立体感在入口Lua文件里打印当前服务的地址和自己的消息来源。比如在登录服务里dubug一条消息看source字段对应哪个服务发的。源码包里的login/main.lua通常长这样-- service/login/main.lua 入口示意 local skynet require skynet skynet.start(function() skynet.dispatch(lua, function(session, source, cmd, ...) -- session 是这次请求的会话号source 是来源服务地址 if cmd ping then skynet.ret(skynet.pack(pong)) elseif cmd login then local ok, err handle_login(...) skynet.ret(skynet.pack(ok, err)) end end) end)这段代码说明三件事skynet.start里的函数是服务的启动入口skynet.dispatch注册了消息处理器skynet.ret(skynet.pack(...))是对同步请求的回复。这里最容易踩的坑是如果某个分支忘了调用skynet.ret调用方协程会一直挂起等待回复表现为线上偶发性卡顿日志还看不到错误。2.2 同步call与异步send该等的结果才等不该等的别等skynet给上层提供了两种投递消息的方式skynet.call是同步请求发送消息后挂起当前协程等目标服务回复才继续skynet.send是异步通知发完就继续往下走。很多刚接触skynet的人会在这两种方式之间选错把整个服务拖慢。我的经验法则很简单拿不到结果就无法继续的流程用call比如登录验证、查询角色数据、扣钻石纯通知性质的操作用send比如广播在线列表、写操作日志、通知玩家下线。常见翻车现场是某个玩家上线流程里连续同步调了三次数据库服务每次等一个RTT最坏情况下登录耗时就翻了三倍。更合理的做法是把多条查询合并成一条消息发给db服务让db服务一次执行多条SQL后用一次ret返回网络开销从三次降到一次。2.3 登录、agent、db、cache四层拓扑数据访问为什么必须收口典型的skynet游戏服务器拓扑围绕四类服务展开gateserver负责TCP连接和编解码收客户端消息后投递给对应的agent服务agent服务每个在线玩家一个承载玩家的业务逻辑db服务统一封装MySQL连接池cache服务统一封装Redis连接池。登录流程是gateserver收到登录请求 → 转发给login服务 → login验证通过后用skynet.newservice(agent)拉起一个agent → 后续所有客户端消息由agent处理。这里最关键的设计决策是所有数据库访问必须收口到db服务和cache服务而不是让每个agent自己连MySQL。原因有两点第一是连接数失控几千个玩家在线就意味着几千个MySQL连接数据库直接被打满第二是事务隔离困难多个协程共用一个连接时事务的begin和commit可能被其他请求穿插。收口之后db服务内部可以串行处理事务、用连接池复用连接业务层只暴露简单的数据接口。理解了这个拓扑再看源码包里的service目录你会瞬间明白每个文件夹是干什么的。3. 把源码包跑通编译、配置与启动最小服3.1 环境准备Linux、编译依赖与mysql/redis容器skynet官方支持Linux和macOS不支持Windows。如果你手头只有Windows机器别在本地裸奔折腾直接用WSL2或者开一台云主机省下的时间足够把后面章节读完了。编译前需要确认gcc、make、readline开发头文件已经安装Debian/Ubuntu系的命令是# 安装编译依赖 sudo apt update sudo apt install -y build-essential libreadline-dev源码包一般自带skynet框架目录进到skynet目录直接执行make linux就能编译出可执行文件不需要额外装Lua解释器skynet内置了Lua版本。MySQL和Redis我一般用Docker起比在本机折腾安装路径和初始化脚本快得多# 用 docker 起 MySQL 8.0字符集指定 utf8mb4 docker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDdevpass \ -e MYSQL_DATABASEgame \ mysql:8.0 --character-set-serverutf8mb4 # 用 docker 起 Redis 7开启持久化和内存上限 docker run -d --name redis7 -p 6379:6379 \ redis:7 --appendonly yes --maxmemory 1gb --maxmemory-policy allkeys-lruMySQL的--character-set-serverutf8mb4是必填项否则默认字符集存中文容易出乱码Redis的--maxmemory-policy allkeys-lru是为了防止某个key写爆把整台机器的内存吃光游戏服务器里缓存数据量大这个参数建议一开始就配好。如果你用MySQL 5.7注意认证插件和8.0不一样新客户端连接5.7通常会报caching_sha2_password相关错误解决方案是给账号改成mysql_native_password这是老版本最常见的兼容坑。3.2 源码包目录识别与skynet编译拿到一个zip先别急着跑先用一分钟清点目录。一个结构完整的skynet源码工程通常包含这些部分目录或文件作用怎么确认它正常skynet/框架本体里面能找到Makefile和src目录service/业务服务代码每个服务一个子目录内部有main.lualualib/Lua公共库业务代码引用的公共模块luaclib/C扩展编译产物编译后生成.so文件config启动配置文件文本文件能被skynet命令行读取这一步的核心目的不是背目录而是确认源码包的完整性。经常有人下载的包缺了luaclib里的.so文件导致启动时报cannot open share object file这就是因为包作者没把编译产物打进去。遇到这种情况不用慌进skynet目录重新编译一次就能生成缺失的.so。编译命令cd skynet make linux编译成功后当前目录会多出一个skynet可执行文件。如果报错提示缺lua头文件说明本机没有安装lua相关依赖实际是编译依赖没装全回到3.1把readline等依赖装上再试。macOS用户把make linux换成make macos即可。3.3 最小config配置与首次启动skynet启动时需要一个文本配置文件它决定了线程数、服务入口、Lua脚本搜索路径等关键参数。一个最简配置长这样-- config 文件skynet启动时用 ./skynet config 加载 thread 8 harbor 1 start main luaservice ./service/?.lua;./game/?.lua lua_path ./lualib/?.lua;./game/?.lua lua_cpath ./luaclib/?.so logpath ./log参数含义按优先级来讲thread决定框架工作线程数量不是越大越好一般设为云主机物理核数设太大会因为上下文切换反而变慢start是入口服务的文件名对应service/main.lualuaservice是服务查找路径问号会被服务名替换lua_path和lua_cpath分别指定Lua脚本和C扩展的搜索路径logpath决定日志落盘目录。配置文件本质上是一份Lua代码所以你可以用skynet.getenv(thread)在Lua里读取这些值。首次启动的命令是./skynet config看到类似start service和Welcome to skynet的输出就说明框架起来了。如果提示找不到main服务优先检查luaservice路径里的目录名和实际目录是否一致如果端口被占用说明有残留进程pkill skynet后重试。想进一步验证服务是否正常注册可以在config里加一行console 9527然后用telnet 127.0.0.1 9527连上控制台输入list就能看到当前所有服务的地址列表。这一步是整个源码包跑通的里程碑后面所有数据层调试都建立在这之上。4. MySQL与Redis接入数据层代码怎么组织才不翻车4.1 db服务与连接池为什么每一路MySQL查询都要排队借连接skynet的Lua服务是单线程事件循环协程可以并发跑但底层只有一个执行流。如果每个业务协程直接创建MySQL连接连接数会随着玩家量线性膨胀而且多个协程共用同一个连接时协议帧会互相穿插查询结果张冠李戴。所以db服务的核心是连接池所有连接启动时创建好业务请求到了以后从池子里借一个连接查询完归还。连接池的核心逻辑可以用这么一段伪代码理解-- service/db/main.lua 连接池关键路径示意 local conn_pool {} local function acquire() -- 有空闲连接直接弹出没有就新建 local conn table.remove(conn_pool) if not conn then conn mysql.connect({host 127.0.0.1, port 3306, database game, user root, password devpass, charset utf8mb4}) end return conn end local function release(conn) -- 用完后归还让其他请求复用 table.insert(conn_pool, conn) end逻辑说明acquire优先复用池子里空闲连接避免每次查询都走TCP握手release把用过的连接放回池子。池子初始大小一般配10到20个就够别贪多MySQL的max_connections默认151连接数超过了数据库自己先崩。这里有个隐藏问题release必须放在所有分支都能执行到的地方如果业务代码里某个分支提前return了连接就泄漏出池子表现为连接数只增不减。常见做法是把查询逻辑包一层pcall不管成功失败都强制归还。4.2 事务在skynet里的正确打开方式游戏服务器里扣钻石、发邮件、跨服排行结算这些操作都涉及事务。MySQL事务有个硬性约束必须在同一个连接上执行begin、多条SQL和commit或rollback。放到skynet里就意味着事务逻辑不能横跨多条服务消息必须完整地写在一个db服务的消息处理函数里。我见过最多的事务翻车方式是这样的agent服务先call db服务执行begin再call一次执行第一条SQL再call一次提交。三次call之间如果第二次失败了这个连接上的事务状态就悬在那里。更糟的是如果期间另一个请求从池子里借到了同一个连接就会把上一个事务的脏状态接过去。所以正确做法是把整个事务封装成一个完整的消息-- 事务处理示意扣钻石 写流水要么都成要么都回滚 function handle_transaction(session, source, cmd, ...) local conn acquire() local ok, err conn:query(begin) if not ok then release(conn) skynet.ret(skynet.pack(false, err)) return end local ok1 conn:query(update player set diamond diamond - 10 where id 10086) local ok2 conn:query(insert into diamond_log(player_id, delta, ts) values (10086, -10, now())) if ok1 and ok2 then conn:query(commit) skynet.ret(skynet.pack(true)) else conn:query(rollback) skynet.ret(skynet.pack(false, transaction failed)) end release(conn) end参数说明begin开启事务后所有SQL都在这个连接上执行ok1和ok2只要有一个失败就回滚防止钻石扣了流水没写。注意事务内的查询要控制在很短的时间内事务期间连接被独占长事务意味着一整路连接长时间不可用高并发下其他请求全部排队。事务超时也要兜底比如在执行commit前检查总耗时超过200毫秒直接回滚并记录错误日志。4.3 cache服务与Redis数据类型hash、zset、string怎么选Redis在游戏服务器里不负责最终一致性它扛的是高频读和临时状态。cache服务统一管理Redis连接业务服务不直接连Redis避免连接数失控。选数据类型时我的习惯是玩家可变属性用hash一天一个keyfield是属性名排行榜用zsetmember是玩家IDscore是分数会话token用string加expire跨服通知用list做简单队列。排行榜是Redis用得最经典也最容易做错的场景。MySQL里order by score desc limit 20在小数据量下没问题但玩家量过了十万深分页会让数据库扫描大量行慢查询直接把db服务队列堵死。换成Redis zset后-- cache 服务处理排行查询示意 -- key: rank:day_20250101 local function handle_rank(session, source, cmd, key, top_n) local list redis.command(zrevrange, key, 0, top_n - 1, withscores) skynet.ret(skynet.pack(list)) end参数说明zrevrange按score从高到低取成员0是起始索引top_n - 1是结束索引withscores让结果带上分数。这个命令时间复杂度是O(log(N)M)M是返回条数性能远优于数据库深翻页。但要注意两个边界一是key会过期每日榜单前一天的数据要提前半小时预热否则玩家零点一刷新直接打到数据库二是分数相同时zset按member字典序排玩家的实际名次可能跟预期有偏差业务上一般把分数放大到整数再带上时间戳做tiebreaker。4.4 缓存一致性延迟双删、穿透与雪崩治理玩家数据既要落MySQL又要进Redis缓存一致性是绕不开的问题。我的方案是写操作先更新MySQL成功后删除Redis里的旧缓存读操作命中缓存直接返回没命中就查MySQL并回填Redis。为什么不先写缓存因为Redis挂了不影响MySQL但反过来MySQL成功Redis缓存更新失败会留下脏数据。-- 玩家改名流程示意先写库再删缓存 local ok db_call(update_player_name, player_id, new_name) if ok then cache_del(player: .. player_id) end逻辑说明db_call先确保MySQL写入成功然后删除缓存key下次读取时回源数据库拿新值。这个方案有个时间窗问题删除缓存之前可能有旧读请求把脏数据回填。为了把这个窗口压到最小常见做法是延迟双删删除后等几百毫秒再删一次把回填的脏数据清掉。这个延迟时间要大于最长查询时间我一般设500毫秒。缓存穿透和雪崩也要提前治理。穿透指恶意请求反复查一个不存在的key每次都打到数据库解法是把空结果也缓存起来过期时间设短一点雪崩指大量key同时过期导致数据库负载飙高解法是给过期时间加一个随机偏移量让过期时间均匀分布击穿指某个热点key过期瞬间被大量请求同时回源解法是让回源逻辑加互斥锁只允许一个请求去查数据库其他人等锁释放后直接读新缓存。5. 避坑skynet接入MySQL与Redis最容易翻车的五件事5.1 现象游戏没几个人在线MySQL连接数却满了登录服刚启动半小时MySQL的show processlist刷出来几百个Sleep连接数据库直接拒绝新连接。原因基本指向db服务的连接池泄漏某个分支没有归还连接或者协程异常退出时没有走release。这种问题日志很难看出来因为MySQL不会主动断掉泄漏的连接只会默默地躺着。解决方法是两层兜底。第一层在release时检查连接有没有残余事务状态发现有就显式rollback第二层框架侧给db服务加定期清理任务扫描连接池里空闲时间超过wait_timeout的连接并重建。MySQL侧把wait_timeout从默认8小时调到2小时也能防止连接长期占着不还。经验是每次排查连接泄漏先怀疑所有skynet.ret之前的分支这类代码最容易漏归还。5.2 现象一个玩家查排行榜全服都卡了某玩家点开排行榜agent服务发了一条查询消息给db服务然后所有玩家都开始卡顿CPU没满日志也没错误。这是典型的慢查询阻塞问题db服务是单线程执行消息的一条深分页的order by score desc limit 1000000, 20让它跑了好几秒排在后面的其他玩家查询全部排队。解决思路是别让数据库扛热点读。排行榜用Redis zset承接数据库只做归档如果一定要用MySQL把深分页改成游标分页用where score 上一页最后分数的方式翻页避免大offset扫描。更关键的是给db服务加超时保护单条查询超过1秒直接取消并把执行计划记录到错误日志让问题在发生时立刻暴露而不是等到全服卡死才排查。5.3 现象Redis偶发超时日志报command timed out业务跑着跑着日志里出现command timed outRedis服务端负载不高网络也没断但就是偶发超时。原因通常是cache服务里多个协程共用了同一个Redis连接某个慢命令阻塞了连接其他请求全部排队等超时。另一个常见原因是业务代码里不小心执行了keys *这类O(N)命令Redis是单线程一个keys *能让整个实例卡几秒。解决方法是把所有Redis访问收口到cache服务并且每个请求从连接池借连接用完全部归还给连接加socket超时参数比如连接池内设置timeout 1000毫秒超过直接报错而不是无限等业务侧禁止keys *和hgetall大key改用scan分批遍历。想验证Redis网络状况可以用redis-cli --latency连着观察一段时间如果平均值超过1毫秒就要检查宿主机负载了。5.4 现象玩家下线后数据回档日志里没有任何错误玩家下午改了个签名晚上上线发现签名变回早上的值日志干净得像没发生过一样。这类丢数据问题最恶心因为它不是崩溃而是静默丢弃。原因通常出在缓存和数据库的不一致流程上agent服务先把新签名写进Redis然后异步通知db服务写MySQLagent在写库完成前就退出了消息丢了。解决方法是扭转缓存和数据库的写入顺序先写MySQL成功后删缓存读时回源Redis永远只当缓存不当唯一数据源。agent退出前要走一个优雅关停流程把未落库的变更同步flush到db服务db服务侧再加一个周期快照任务把Redis里最近的变更量定时回写数据库做兜底。5.5 现象压测时内存涨不停服务越跑越慢压测跑到第30分钟内存占用持续上涨玩家操作越来越慢最后服务卡死。先看服务列表如果某个agent服务消息队列长度在增长说明有消息没人处理再用skynet.memory一类工具统计Lua内存如果涨得很快大概率是协程泄漏——某个skynet.call一直没等到回复协程挂起队列里引用全部持在session映射表里永远不释放。解决手法是给所有跨服务call加超时控制不要裸调skynet.call封装一层带超时的调用函数超时后释放session并返回错误定时打印服务消息队列长度和协程数量涨了就知道哪个服务出问题。我见过太多线上事故是因为一个skynet.ret漏写导致的协程堆积这种问题压测时反而不容易暴露小流量下根本看不出来。6. 从跑通到上线数据链路验证、压测与一个小习惯6.1 上线前先走一遍完整链路验证服务起了、表建了、Redis也通了不代表链路是对的。我会用一个模拟客户端脚本跑一条最小核心链路登录请求发到gateserverlogin服务验证账号agent服务读取玩家数据如果缓存没有就从MySQL回源改一个字段后落库。每步都在MySQL里select验证行数据在Redis里hgetall验证缓存key是否存在。这条链路跑通才说明框架、数据库、缓存三者是真的协同工作而不是各跑各的。6.2 压测从登录风暴开始压测不用一上来就追求复杂玩法先做登录风暴就够了。写一个脚本模拟大量客户端同时登录观察三个指标登录接口的P99耗时、MySQL连接数曲线、Redis命令耗时。连接数曲线陡增后不掉说明连接池泄漏P99在并发200时就开始抖动说明db服务队列开始堆积。压测时把日志级别调到debug能看到每一条SQL和Redis命令的耗时定位慢请求比事后猜快得多。6.3 给db和cache服务加一层可观测性我个人的习惯是每个数据服务上线前必加两个计数器请求总数和耗时分布。不用引额外监控系统在服务里用一个表记录分桶耗时就行比如小于10毫秒、小于100毫秒、大于100毫秒各计一个数。配合skynet的日志系统每天看一眼请求耗时分布就能在玩家投诉之前发现数据层劣化。这套源码包能不能直接上线不取决于玩法做得多炫而取决于数据层在压力下扛不扛得住。这些年看skynet项目我总结出的判断标准其实就一条先看数据层怎么组织。一个把MySQL和Redis收口好、有超时有兜底有监控的服务结构玩法再简单也坏不到哪去反过来说如果数据访问散落在各个agent里那这份源码包的价值就要大打折扣。希望这篇笔记能帮你在拿到这类源码包时少走我当年走过的弯路也希望你跑通之后愿意把数据层的这些细节真正重视起来——毕竟线上最贵的从来不是服务器而是那些半夜爬起来排查问题的觉。本文还有配套的精品资源点击获取