尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

游戏服务器积分系统设计:ET/Skynet框架下的存储方案对比与实践

游戏服务器积分系统设计:ET/Skynet框架下的存储方案对比与实践 1. 项目概述在游戏服务器开发中积分系统的设计看似简单实则暗藏玄机。最近在基于ET/Skynet框架重构我们的卡牌对战游戏时团队就积分该存为道具还是直接存数值这个问题争论不休。作为技术负责人我不得不深入思考这两种方案在架构设计、性能表现和代码实现上的差异。这个选择不仅关系到数据库结构设计更影响着整个游戏经济系统的扩展性和维护成本。经过两周的论证和原型验证我们最终形成了一套完整的解决方案。本文将分享从架构权衡到代码落地的全过程思考特别适合正在使用ET/Skynet框架开发游戏后端的中高级开发者参考。2. 核心需求解析2.1 业务场景分析在我们的卡牌对战游戏中积分系统主要服务于三个场景排位赛积分实时变化的竞技场排名依据活动积分限时活动的进度追踪兑换积分游戏内商城的通用货币每种积分在变更频率、数据一致性和业务逻辑复杂度上都有显著差异。比如排位赛积分需要高频更新且对实时性要求极高而兑换积分则需要严格的原子操作保证不会出现超发。2.2 技术约束条件基于ET/Skynet框架的特性我们需要特别考虑分布式架构下的数据一致性热更新支持千万级用户的数据存储效率与现有道具系统的兼容性这些约束条件直接影响了我们的技术选型。例如ET框架的Entity Component系统天然适合道具化的设计而Skynet的actor模型则更擅长处理数值型的频繁更新。3. 架构方案对比3.1 道具化存储方案将积分设计为特殊道具存储在玩家的背包系统中。每个积分类型对应一个道具ID数量字段表示当前值。优势复用现有道具系统的基础设施方便实现复杂的获取/消耗逻辑天然支持事务操作易于扩展新的积分类型劣势高频更新时的性能开销批量操作时需要加载整个背包排行榜查询效率较低// ET框架下的道具化积分示例 public class ItemComponent : Entity { public Dictionarylong, Item Items new(); public void AddScore(int scoreType, int value) { var item Items[scoreType]; item.Count value; // 触发道具变更事件... } }3.2 数值化存储方案将积分作为玩家实体的直接属性在数据库中建立专门的字段存储。优势读写性能极高简化排行榜实现减少不必要的组件加载内存占用更小劣势业务逻辑与核心系统耦合新增积分类型需要修改数据结构事务控制需要额外开发-- Skynet下的数值化积分示例 function handler.add_score(player, score_type, delta) local player_data player.data player_data.scores[score_type] (player_data.scores[score_type] or 0) delta -- 直接更新数据库字段 db.update(players, {_idplayer.id}, { [$inc] {[scores...score_type] delta} }) end3.3 混合存储方案经过压力测试和业务评估我们最终采用了混合方案高频更新的竞技积分使用数值化存储需要复杂逻辑的活动积分采用道具化存储商城兑换积分使用独立的事务型存储这种折中方案在ET/Skynet混合架构中的实现关键点在于通过ET的Entity系统管理道具化积分利用Skynet的actor处理高频数值更新使用MongoDB的事务支持保证兑换积分的原子性4. 核心实现细节4.1 数据模型设计classDiagram class Player { long playerId Dictionaryint,int scores ItemComponent items } class ItemComponent { Dictionarylong,Item items } class Item { long itemId int count int configId } Player 1 *-- 1 ItemComponent ItemComponent 1 *-- * Item4.2 ET框架中的道具积分实现在ET框架中我们扩展了ItemSystem来支持积分特性public static class ScoreItemSystem { [ObjectSystem] public class ScoreItemAwakeSystem : AwakeSystemItem, int { public override void Awake(Item self, int scoreType) { self.AddComponentScoreComponent, int(scoreType); } } public static void AddScore(this Item self, int delta) { if (!self.IsScoreItem()) return; var scoreComp self.GetComponentScoreComponent(); scoreComp.Add(delta); // 触发ET事件总线通知 Game.EventSystem.Publish(new EventType.ScoreChange(){ Player self.GetParentItemComponent().GetParentPlayer(), ScoreType scoreComp.ScoreType, Delta delta, NewValue scoreComp.Value }); } }4.3 Skynet中的数值积分服务在Skynet中我们实现了独立的score_servicelocal skynet require skynet local mongo require skynet.db.mongo local score_service {} function score_service.update_rank_score(player_id, score_type, delta) local player get_player(player_id) local old player.scores[score_type] or 0 local new old delta -- 使用mongodb的原子操作 local ok db.player:update( {_id player_id}, {[$inc] {[scores...score_type] delta}}, {upserttrue} ) if ok then player.scores[score_type] new -- 更新排行榜 skynet.send(rank_service, lua, update, score_type, player_id, new) end return ok end skynet.start(function() db mongo.client({ host config.mongodb_host, port config.mongodb_port, }).db(game_db) skynet.dispatch(lua, function(_, _, cmd, ...) local f assert(score_service[cmd]) skynet.ret(skynet.pack(f(...))) end) end)5. 性能优化实践5.1 缓存策略设计针对不同积分类型采用不同的缓存策略积分类型缓存策略刷新频率失效条件竞技积分全内存实时更新下线时持久化活动积分LRU缓存5分钟道具变更时兑换积分不缓存--5.2 批量更新优化对于道具化积分的批量操作我们实现了特殊的批量接口public async ETTask BatchAddScores(long playerId, Dictionaryint, int scoreChanges) { using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.Item, playerId)) { var player GetPlayer(playerId); var itemComp player.GetComponentItemComponent(); foreach (var change in scoreChanges) { var item itemComp.GetScoreItem(change.Key); item?.AddScore(change.Value); } // 合并数据库操作 await dbProxy.BulkUpdateItems(playerId, scoreChanges); } }6. 事务处理方案6.1 跨服务事务控制当涉及多个服务的积分操作时我们采用Saga模式保证最终一致性创建分布式事务ID记录操作日志执行各服务操作定时补偿检查function score_service.begin_transaction() local tx_id generate_tx_id() local tx_log { _id tx_id, status pending, operations {}, create_time os.time() } db.transaction_log:insert(tx_log) return tx_id end function score_service.commit_transaction(tx_id) -- 验证所有操作是否完成 local ok check_operations(tx_id) if ok then db.transaction_log:update( {_id tx_id}, {[$set] {status committed}} ) end return ok end6.2 失败恢复机制我们设计了三级恢复策略实时重试网络问题导致的失败立即重试3次定时扫描每分钟检查pending状态的事务人工干预超过24小时未完成的事务报警7. 踩坑与经验总结7.1 内存泄漏问题在早期实现中我们忽略了ET框架中EventSystem的订阅管理导致积分变更事件监听器不断累积。解决方案是// 在Dispose时取消监听 public override void Dispose() { Game.EventSystem.RemoveListener(this); base.Dispose(); }7.2 排行榜数据不一致数值化积分在跨服场景下出现了排行榜延迟问题。最终我们引入了双写缓冲机制先在内存中更新异步写入排行榜定时全量同步7.3 热更新兼容性道具化积分的配置变更需要特别处理热更新。我们的做法是保留旧积分类型的只读访问新积分使用新的道具ID提供自动转换接口8. 监控与调优8.1 关键指标监控我们在Grafana中配置了以下监控项积分更新延迟百分位事务成功率各类型积分QPS缓存命中率8.2 性能调优结果经过3轮优化后关键指标对比指标初始方案优化后竞技积分更新延迟120ms15ms活动积分TPS8003500兑换积分事务成功率98.5%99.99%9. 扩展性设计9.1 新积分类型接入通过配置驱动的方式支持快速新增{ score_type: 101, storage_type: numeric, min_value: 0, max_value: 9999, persist_freq: realtime }9.2 跨游戏积分互通通过抽象积分服务接口实现public interface IScoreService { ETTaskint GetScore(long playerId, int scoreType); ETTaskbool TransferScore(long from, long to, int scoreType, int amount); }10. 最终架构全景图我们的混合架构最终实现了高频操作5万QPS的竞技积分更新复杂逻辑支持50种活动积分规则强一致性兑换积分零差错水平扩展支持500万DAU这套方案已经在我们的三款游戏中稳定运行半年期间经历了三次大型活动考验。最大的收获是认识到没有银弹方案只有适合当前业务发展阶段的技术决策。
返回列表