ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Boot+WebSocket实战:福州永泰麻将小程序后端架构与开发

Spring Boot+WebSocket实战:福州永泰麻将小程序后端架构与开发 简介在前后端分离架构日益普及的今天实时通信与高并发处理成为小程序后端开发的核心挑战。以微信小程序生态下的棋牌游戏为例项目不仅需要理解Spring Boot等主流框架的工程化实践更要掌握WebSocket长连接、Redis缓存与分布式锁在牌局状态同步、断线重连等场景中的具体应用。通过合理的架构选型与模块划分可以有效保障游戏服务的稳定性与响应速度。本文以福州永泰麻将小程序后端为蓝本从技术选型到核心模块设计再到部署上线与问题排查系统梳理了地方棋牌类后端开发的完整链路为同类实时交互项目提供了可参考的实践方案。 接手这个“福州永泰麻将微信小程序后端.zip”项目时我先把它当成一个普通的地方棋牌App后端来处理结果越做越发现地方麻将和小程序端的组合水比想象中深得多。不只是写几个接口那么简单房间生命周期、牌局状态同步、玩家断线重连、积分结算对账每一条链路都得捋清楚。这篇文章我按照实际开发顺序来拆解从架构选型到核心模块再到部署上线和踩坑记录给准备做同类项目的人一份可以照着走的参考。1. 项目定位与整体架构设计1.1 核心需求解析这不是“另一个麻将App”福州永泰麻将本质上是地方规则麻将核心打法和大众麻将差不离但带了一批本地特色规则比如特殊牌型计分、金癞子的玩法、抢杠胡的收益规则等。用户拿到这个后端包真正需要的不只是一套“能跑起来”的代码而是“能扛住真实玩家在线对战”的完整服务端方案。微信小程序端和普通App端最大的区别在于微信生态的约束。后端必须配合小程序的登录体系wx.login换openid、域名白名单校验、HTTPS强制要求以及虚拟支付限制。这些因素直接影响架构决策。我拆完需求后确认了三个核心目标支持好友房模式和快速匹配模式房间内实时同步牌局状态积分制结算不涉及真实货币规避微信小程序虚拟支付限制一套后端同时服务小程序端和管理后台前后端彻底分离为什么强调前后端分离因为棋牌类项目迭代频繁前端要快速调UI和交互后端要稳定提供接口。如果像传统服务端渲染那样耦合在一起小程序发版审核又慢改个按钮等三天项目根本推不动。1.2 技术选型为什么是Spring Boot Redis WebSocket后端的语言选型选了Java加上Spring Boot 2.7这是当下国内前后端分离项目最稳妥的组合。Spring Boot生态成熟招人容易第三方库齐全尤其适合这种业务逻辑复杂的棋牌项目。热搜词里提到的ruoyi框架我也认真考虑过它确实能快速搭出管理后台但对于牌局这种强实时、高并发的场景反而多了一层学习和改造成本最终决定不用直接原生Spring Boot起项目。存储层用了MySQL 8.0加MyBatis-PlusRedis 6.x做缓存和分布式锁。MySQL负责持久化用户数据、牌局记录、积分流水Redis处理在线状态、房间临时数据、Token缓存。这个分工比较关键牌局进行过程中的手牌、出牌记录、操作队列全部放Redis速度快且天然支持过期时间一局结束后才落库到MySQL这样MySQL的写入压力小很多。实时通信选了WebSocket而不是轮询或SSE。原因很简单牌局是双向强交互场景玩家出牌、碰杠胡、聊天消息都要毫秒级推送。WebSocket长连接一次握手后全双工通信比轮询省流量也比SSE更适合双向通信。后端用Spring的WebSocket模块自己实现了一套轻量级消息路由没引入Netty初期并发量在几千人这个级别完全够用真到了需要Netty的地步再加也不迟。1.3 目录结构与模块划分项目按多模块Maven结构组织顶层拆成了四个子模块common公共工具类、统一返回体、异常处理、常量定义system用户体系、登录鉴权、系统配置game-core牌局核心逻辑包括洗牌、发牌、出牌判定、胡牌算法manage管理后台接口对局查询、用户管理、数据统计这里最需要注意的是game-core必须独立出来不能和system耦合。原因很现实牌局算法要反复测试独立模块可以直接写单元测试跑不用启动整个Web应用。我当时把胡牌判断、听牌计算、积分结算都写成了纯Java方法不依赖Spring容器测试效率高了一个量级。2. 核心业务模块拆解2.1 房间生命周期管理麻将房间是整个系统的核心容器我把它设计成五个状态等待中、游戏中、已暂停、已结束、已解散。每个状态间的流转都由后端严格校验决不能允许前端传一个状态过来就改。房间创建设计时最关键的是“房主”概念。房主拥有解散房间、踢人、修改房间设置等权限这些操作用Redis的Hash结构存储房间数据字段包括房间号、房主ID、玩家人数、当前局数、总局数、玩法参数等。这里有个坑要提前讲房间号不能用自增ID6位数字的房间号要支持玩家手动输入加入。我直接用随机数生成6位数字存Redis时用SETNX做唯一性校验冲突就重新生成。实测下来每天几万次建房操作冲突概率极低性能也没问题。房间解散的逻辑也值得说一下。房主解散时要通知所有房间内成员这个通知是全量推送等所有客户端确认后才能释放房间资源。我在房间对象里维护了一个readyDisband集合收集每个玩家的确认状态全员确认或者超时5分钟才真正销毁房间。这个机制避免了一个人误删房间导致所有人牌局中断的情况。2.2 玩家状态机与断线重连玩家在牌局中有多个状态空闲、匹配中、游戏中、旁观中、离线中。每个状态都是互斥的后端在状态变更前必须做校验。这块逻辑看着简单实际写起来很容易出bug尤其是断线重连。微信小程序和App不同小程序切后台超过一定时间WebSocket连接会被微信主动断开。玩家回来后再进入小程序需要重新建立连接并恢复到原牌局。我做了两件事玩家断线时通过WebSocket的close事件感知将玩家标记为离线但不清除牌局内数据玩家重新登录时查询Redis中的玩家状态若为离线中则自动恢复原房间关系推送“玩家XX回来了”的提示给其他三位断线重连还有一个隐藏问题玩家离线期间轮到他操作时要自动跳过还是等待我的方案是30秒等待超时系统自动摸牌出牌。如果不设超时四个人里有一个切走了整桌人全卡住体验极差。这个30秒参数现在看来也是比较合理的折中。2.3 积分结算与流水对账从标题里的“微信小程序”这个归属来看这类棋牌项目都走积分制不碰真实资金。但积分结算同样要对账不然玩家积分对不上投诉就来了。我设计了一套“局级流水总账”的双层结构每局结束后生成一条局级积分流水记录四位玩家的积分变动、牌型说明、胡牌类型。同时更新玩家总积分。所有计分规则都收敛到一个ScoreCalculator类里支持永泰麻将这些特殊规则的分值配置。新增规则时只改这个类不会影响其他模块。为了防止积分重复计算我在局级流水表加了唯一索引(room_id, round_no)配合Redis分布式锁做并发保护确保重复请求不会产生第二笔流水。这个唯一索引在线上救了我很多次群友们反复点击“再来一局”时偶尔会触发重复结算请求全靠这个兜底。3. 关键技术实现细节3.1 微信小程序登录与Token鉴权小程序端和后端的对接第一步就是登录。微信小程序通过wx.login()获取临时code后端拿着code调用微信接口换openid和session_key。这个环节有两个坑第一wx.login()返回的code只能用一次5分钟有效。后端拿到code后必须立即调用接口不能存库、不能延迟处理。我见过有些同事把code传进来先打了行日志结果调微信接口时报code已被使用。排错花了一个下午最后发现是多打了一行日志的原因。第二openid是用户唯一标识不能直接暴露给前端。我用openid换了一个自定义Token存储结构是token - openidToken有效期设为7天小程序每次请求都带上Token后端通过拦截器统一解析用户身份。这样即使Token被截获也无法反向推算出openid保护了用户数据安全。登录流程的时序可以简单描述为小程序调用wx.login()拿到code小程序请求后端POST /api/auth/login传code后端调用微信接口用code换openid session_key后端生成自定义TokenRedis中写入token - openid映射返回给小程序小程序后续请求都在header中带Authorization: Bearer token实操中还要考虑session_key的保存因为后续如果要做解密手机号等操作会用到。但永泰麻将这个项目用不到我就没在Redis里存session_key省了一点内存。需要用到的小程序项目记得把session_key也存起来过期时间跟微信官方对齐一般跟着code的有效期走建议存24小时。3.2 Redis在房间系统中的实战用法Redis在这个项目里不是点缀是刚需。核心诉求是“快”和“临时”。牌局进行中的数据读写频率极高比如每位玩家的手牌、当前操作玩家、剩余牌数等如果每次操作都走MySQL数据库会先扛不住。用Redis解决后单台8C16G的服务器撑几千人在线打牌几乎没什么压力。数据结构的选择房间信息Hashfield存房间号value存房间详情JSON牌桌状态Stringkey是game:room:{roomId}:statevalue是当前牌局的完整状态JSON每次操作后整体刷新玩家手牌Stringkey是game:room:{roomId}:player:{playerId}:handvalue是手牌列表JSON。这里用String不用List虽然List支持push/pop更方便但玩家整理手牌时需要排序和替换比如摸牌后插到指定位置JSON序列化整体更新反而更灵活在线状态Stringkey是user:online:{userId}value是1带过期时间每次心跳续期分布式锁Stringkey是lock:game:action:{roomId}value是请求ID防止并发操作牌局这里要说明一下为什么不直接用MQTT或自研消息队列。牌局操作的并发量并没有想象中那么大一个房间四个人操作频率就是每几秒一个人动作Redis的原子操作完全够用。引入MQ反而增加部署和运维的复杂度小项目没必要为了“高大上”牺牲简洁性。牌局状态用完整JSON整体刷新还有一个好处断线重连时直接把房间状态推给客户端客户端就能完整渲染当前牌桌画面。不需要设计一堆增量同步协议省了很多事。缺点是流量会大一点但一次状态同步撑死几KB完全可以接受。另外还有一个参数值得分享Redis连接池的配置。我用了Lettuce连接池初始连接数5最大连接数50等待超时3秒。本地压测过2000人同时在线的场景下Redis平均响应时间在1ms以内连接池不会成为瓶颈。如果后续用户量翻倍优先考虑Redis主从加哨兵而不是盲目调大连接池。3.3 WebSocket消息推送与会话管理WebSocket在Spring Boot里接入不难难点在会话管理。一个玩家可能开了多个页面也可能断线重连后旧连接还没被回收导致同一个玩家有多个WebSocket会话。如果广播消息时给所有会话都发一份客户端会收到重复消息。我写了一个WsSessionManager专门管理会话映射。核心数据结构是ConcurrentHashMapString, CopyOnWriteArraySetSessionkey是userIdvalue是会话集合。发消息时遍历集合推送并且捕获异常后主动移除无效会话。这里有几个细节值得说说每次推送消息都带一个msgIdUUID客户端做幂等处理重复消息自动丢弃心跳机制一定要做。前端每30秒发一次ping后端回pong超过90秒没收到ping就主动关闭连接并清理会话。不做心跳的话僵尸连接会越积越多最后把服务器连接数打满WebSocket的URL带上Token参数例如ws://domain/ws?tokenxxx但Token放在URL里容易被日志记录要提醒运维在Nginx层过滤掉Token参数消息类型我统一定义为JSON格式包含type和data两个字段。type定义成枚举值ROOM_JOIN、GAME_START、GAME_ACTION、GAME_RESULT、CHAT_MSG、SYS_NOTICE等。前端根据type分发处理后端根据type序列化data对象。这个协议设计保持了扩展性后续加新功能不用改老消息格式。3.4 洗牌算法与发牌逻辑洗牌算法我用的是经典的Fisher-Yates算法均匀打乱136张牌不带花牌的永泰麻将。这个算法是O(n)时间是公认的无偏洗牌方式。直接用的JavaCollections.shuffle()底层就是它特别偷懒但足够用。发牌逻辑的难点不是发牌本身而是如何保证玩家手牌有序展示。真实麻将桌上玩家摸完牌后会自己理牌小程序端也要模拟这个过程。我发牌后直接按花色和数值排序存到Redis里前端拿到直接渲染。碰杠之后手牌变化了重新排序并更新缓存。还有一个小细节是牌墙的生成。永泰麻将的牌墙是17墩34张牌每人面前一堵揭牌有顺序。我在后端模拟了一个牌墙数组按顺序出牌。打到最后如果牌墙只剩7张就进入流局这个规则也需要精确实现不然跟线下规则对不上。4. 难点问题与排查技巧实录4.1 并发重复请求的幂等处理上线第一天就遇到一个线上问题有玩家反映自己有几次出牌被系统判为“非法操作”但界面上明明点了牌。后来排查日志发现是小程序端网络延迟导致客户端重试同一个出牌请求发了两次。后端第一个请求处理成功后状态变了第二个请求带着同样的操作过来状态校验不通过所以被拒绝了。玩家看到的不是“网络错误”而是“非法操作”体验很差。解决方案分两层第一层前端做防重复点击按钮点击后置灰3秒。第二层后端做幂等校验。每次客户端操作都带一个requestId前端生成UUID后端收到请求先去Redis查idempotent:{requestId}。如果存在直接返回上一次的处理结果不存在则继续处理并把requestId写入Redis过期时间5分钟。这样即使前端重发请求后端也能识别并返回相同的响应。这个方案看起来简单但实际排坑时花了不少时间。因为“非法操作”的错误提示文案太笼统玩家和客服都看不懂。后来我把校验失败的提示细化成具体原因比如“手牌中没有这张牌”“还没轮到你操作”“牌局已结束”配合前端弹窗展示投诉量明显下降。这也算是一个产品层面的细节优化。4.2 跨域与安全校验前后端分离之后跨域是第一道坎。虽然微信小程序的wx.request不校验CORS但管理后台是浏览器访问必须处理跨域。我在后端写了一个全局CORS配置允许的来源限定为管理后台的域名或IP不允许通配符*。安全校验方面除了登录鉴权还有请求签名。所有写操作接口出牌、碰杠、胡、房间操作都要求带timestamp和sign。签名的规则是把请求参数按key排序拼接加上密钥做MD5。密钥只保存在服务端配置里小程序端写死在代码中但由于小程序的代码可以被反编译这个签名方案只能防君子不防小人。真要严格防护得用wx.request支持的自定义header配合后端验签还应该在微信后台配置服务器域名白名单并开启IP白名单。另一个容易被忽略的点是请求频率限制。我用Redis做了简单的滑动窗口计数器每个用户每秒钟最多请求20次超过就返回429 Too Many Requests。这个阈值是压测得出的正常玩家每秒操作最多3-5次20次足够还能拦住大部分粗暴的刷接口行为。4.3 微信小程序域名白名单和HTTPS微信小程序上线时有一个硬性要求所有请求的域名必须在小程序后台配置合法域名并且必须是HTTPS协议。这不是后端能独立解决的需要前后端配合。我在后端申请了SSL证书用的Lets Encrypt免费证书在Nginx层配置了443端口解析。后端服务本身监听8080端口Nginx做反向代理把/api路径转发到内网8080。这里要注意小程序后台配置的域名要填https://api.example.com而不是IP地址因为微信不认IP域名。WebSocket同样要过这一关。小程序端连接wss://api.example.com/ws才能连上需要额外在Nginx配置WebSocket升级头。如果不配Nginx会把WebSocket握手请求当成普通HTTP请求处理这会导致升级失败连接一直建立不起来。这个坑我踩过一次印象极深。上线前一定要检查微信小程序后台的“开发设置-服务器域名”是否配置完整request合法域名、socket合法域名、uploadFile合法域名、downloadFile合法域名各配各的少一个都跑不通。4.4 与“前端无法获取数据”相关的排查热搜词里有一条“前端无法获取数据”在前后端分离项目里太典型了。我曾经遇到小程序端请求后一片空白排查了很久最后定位到是后端返回的JSON里有个字段叫status和前端约定的code不一致前端解析不到数据就渲染空白。从那以后我制定了一个响应体规范所有接口返回{ code: 0, msg: success, data: {} }其中code0表示成功非0表示业务异常。前端统一走一层封装先判断code再取data。这个约定在后端用RestControllerAdvice做了统一包装新接口不需要每次手工构造响应体。还有一次前端说“拿不到用户信息”我查了日志发现请求压根没到后端。后来发现是前端把Token放错位置了放到了data里而不是header里后端拦截器自然取不到。这种问题在后端日志里表现为“没有请求进来”前端表现为“接口404或者无响应”。建议在前端调试时打开wx.request的success回调打印完整响应方便区分到底是网络问题、协议问题还是后端报错。5. 部署落地实战5.1 服务器部署方案和环境准备我用的是一台4C8G的阿里云ECS操作系统是Ubuntu 20.04。部署栈为Nginx 1.18、JDK 1.8、MySQL 8.0、Redis 6.x。这套配置支撑几千名注册用户、两三百人同时在线打牌负载均衡和性能都够用。如果用户量再上一个台阶优先考虑加一台Redis从库MySQL做读写分离而不是急着上微服务。初始化环境有几个必须注意的细节MySQL建库时一定要指定UTF-8字符集utf8mb4不然存emoji表情比如玩家昵称里的表情符号会报错。很多人在这被坑过看起来是代码报错其实是字符集问题Redis要设置maxmemory限制和maxmemory-policy allkeys-lru防止缓存数据占满机器内存打挂整台服务器JDK版本要跟Spring Boot版本匹配Spring Boot 2.7推荐JDK 8或11我用的是JDK 8稳定不需要升级部署方式我用的是Docker Compose编排服务。写一个docker-compose.yml把MySQL、Redis、后端应用一键启动比手动敲命令高效太多而且换服务器时特别好用。镜像构建用的多阶段构建先maven打包再打镜像镜像体积控制在100MB以内。5.2 Nginx配置前后端路由前后端分离项目部署时Nginx是核心入口。前端是小程序不需要静态托管但有管理后台管理后台用Vue打包成静态文件。Nginx配置两个location块server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # WebSocket location /ws { proxy_pass http://127.0.0.1:8080/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; } } server { listen 443 ssl; server_name admin.example.com; # 管理后台静态文件 root /var/www/admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } # 管理后台接口也走API服务器 location /api/ { proxy_pass http://127.0.0.1:8080/api/; } }如果手上暂时没有域名本地开发阶段要用IP调试的话微信开发者工具里可以勾选“不校验合法域名”但上线前必须关掉并用真实域名。5.3 Docker Compose一键发布我用Docker Compose把整套环境编排起来配置文件核心内容如下version: 3.8 services: mysql: image: mysql:8.0 container_name: mahjong-mysql restart: always environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: mahjong_db ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql redis: image: redis:6.2 container_name: mahjong-redis restart: always ports: - 6379:6379 command: redis-server --requirepass yourpassword --maxmemory 256mb --maxmemory-policy allkeys-lru volumes: - redis_data:/data backend: build: . container_name: mahjong-backend restart: always depends_on: - mysql - redis ports: - 8080:8080 environment: SPRING_PROFILES_ACTIVE: prod volumes: - ./logs:/logs volumes: mysql_data: redis_data:部署的操作步骤很简单在服务器上装好Docker和Docker Compose把项目代码拉下来执行docker compose up -d就完成了。这里有一个注意事项后端启动前要确保MySQL和Redis先启动所以depends_on配置一定要写。同时在后端配置文件里设置数据库连接和Redis密码这些敏感数据不写在代码里用环境变量注入。出包时只需要把后端的jar包或者Docker镜像交给运维不需要安装一套完整环境。这个方案在换服务器、迁移机房时极其省心。5.4 压测与性能调优上线前用JMeter做了基础压测。场景设计为300个并发用户同时登录、同时操作出牌、同时查询房间列表。压测结果登录接口TPS约1200平均响应时间80ms出牌接口TPS约900平均响应时间120ms房间列表接口TPS约1500平均响应时间50ms这个数据对当前规模完全够用。压测后优化了两个点第一MySQL连接池调大。Spring Boot默认的HikariCP最大连接数是10对于这种短平快接口并发一上来连接池就不够用了。我调到了maximum-pool-size: 30配合minimum-idle: 5性能提升明显。第二接口的响应体序列化用了Fastjson但Fastjson有版本漏洞后来换成了Jackson性能略降但安全更重要。Spring Boot自带Jackson没必要额外引入Fastjson。棋牌项目虽然不像金融项目那么敏感但用户手机号、openid这些数据一样不能泄露安全上省不得。6. 常见问题与排查速查表把项目推进过程中遇到的高频问题整理成一张表后面自己排查代码时对照着看效率高很多。问题描述可能原因排查方法解决方案小程序请求后端接口报“url not in domain list”小程序后台未配置合法域名登录微信公众平台检查服务器域名配置配置request/socket合法域名使用HTTPSWebSocket连接不断重连Nginx未配置WebSocket升级头查看Nginx日志验证Connection头在Nginx location中配置proxy_set_header Upgrade和Connection upgrade玩家登录后频繁掉线Token过期时间设置太短查看Redis中Token剩余过期时间调长Token过期时间前端增加静默续期逻辑心跳间隔适当调短积分流水重复前端重复提交结算请求检查应用日志看请求次数增加requestId幂等机制数据库表增加唯一索引玩家出牌被误判非法请求重试导致状态不一致查看Redis中牌局状态的变更记录前端按钮防重复点击后端幂等校验管理后台登录不跳转CORS未配置浏览器F12看控制台报错信息后端配置CORS允许来源不能全放开Redis连接数耗尽连接池配置过小查看Redisclient list调大连接池检查代码是否及时归还连接MySQL CPU使用率突然高涨慢查询未优化查询慢查询日志、explain执行计划给高频字段加索引比如room_id、player_id冷数据归档玩家进入房间后看不到任何牌房间状态JSON没有推送完整后端打印房间状态JSON对比前端解析逻辑统一前后端协议用API文档约束字段命名7. 扩展方向与个人经验这个项目做下来我对地方棋牌类小程序后端的理解比之前深了不少。如果后续要继续迭代有几个方向值得投入短剧和聊天功能现在是棋牌类小程序拉留存的有效手段。牌局间隙玩家需要一点轻娱乐内容在房间页嵌入一个类似聊天室的功能配合简单的表情包系统活跃度能明显提升。WebSocket消息路由已经在架构上留好了扩展点加消息类型不是难事。另外小程序的分包异步加载特性值得利用起来。游戏页面和普通页面拆到不同分包进入牌局时异步加载对应分包冷启动速度能快一截。这虽然偏前端但后端接口也要配合做按需加载比如只拉当前房间数据不把用户所有历史数据一次性吐给前端。最后想说一下团队协作的经验。项目开始前一定要先定接口文档工具用Apifox或者YApi都行但所有接口字段、响应格式、错误码都要提前定义清楚。前后端并行开发时前端可以用Mock数据先跑起来真正联调的时间能压缩到两三天。踩过几次坑之后我现在的习惯是写完每个核心接口都顺手写一个冒烟测试验证主流程通不通。棋牌项目不像普通管理系统出牌逻辑和结算逻辑出错玩家会直接炸毛。自动化测试虽然前期花点时间但对项目稳定性是实打实的保障。本文还有配套的精品资源点击获取
返回列表