Skip to content

路线 B|C++ Linux 游戏服务端 ​

B4|Database & Cache:MySQL、Redis 与数据一致性 ​

适用基础: B3 目标: 能判断数据该放进程、缓存还是数据库,并处理事务、过期和故障。


一、先定义数据事实 ​

text
数据库:长期事实和可恢复状态
Redis:低延迟缓存、临时状态、队列/排行榜等
进程内:本实例局部热数据,重启丢失

缓存不是事实来源。每种数据要写明 owner、更新路径、失效策略和恢复方式。

二、MySQL 表、索引与查询计划 ​

表设计先确定实体、唯一约束、状态和版本,再围绕真实查询设计联合索引。EXPLAIN 要看访问类型、候选/实际索引、估算行数、过滤和排序;索引最左匹配、选择性、回表和分页方式都会改变结果。索引能加速读,也会增加写入、锁和空间成本。

三、事务、隔离与锁 ​

事务提供原子性、隔离性和持久性,但隔离级别决定并发读能看到什么。扣费、发奖和背包更新需要明确事务边界、行锁范围、锁顺序和重试条件;死锁是并发图形成环,应用应记录 SQL/事务上下文并按幂等语义重试,不能简单无限重试。

MVCC 与快照 ​

InnoDB 通过版本信息和 undo 数据让普通一致性读看到事务快照,锁定读则参与当前读和加锁。MVCC 减少读写阻塞,但长事务会延长旧版本保留、扩大 undo 和复制延迟。游戏服务应让事务短小,不能在事务中等待网络或玩家输入。

唯一约束是最后防线 ​

应用层先检查“名字是否存在”无法阻止并发插入;数据库唯一索引才能提供最终原子约束。幂等订单、结算批次和领取记录都应有业务唯一键,然后把重复键错误映射为“已经处理”。

四、Redis 结构、TTL 与内存模型 ​

String、Hash、List、Set、Sorted Set 各有访问语义;选择结构就是选择更新和查询复杂度。TTL 只是过期策略,不是持久化保证;还要理解淘汰策略、持久化、复制延迟和大 key。Pipeline 减少往返但会扩大单批延迟和客户端/服务端内存。

Redis 单命令原子不代表多命令业务原子。需要组合读改写时可使用 Lua、事务或基于版本的比较,但仍要考虑主从切换和持久化语义。分布式锁必须有唯一 owner、租约、超时和 fencing token;只用 SETNX 后无条件删除可能误删别人的锁。

五、缓存模式 ​

text
Cache-aside:读 miss 查库后写缓存
Write-through:写入缓存并同步后端
Write-behind:异步落库,风险更高

对玩家关键状态,通常选择可恢复数据库为事实,缓存只做加速,并用版本/时间戳处理旧值覆盖。

缓存穿透是请求不存在数据,击穿是热点 key 同时失效,雪崩是大量 key 同时失效或缓存整体不可用。它们的解决策略不同:负缓存/Bloom Filter、single-flight、TTL 抖动、限流和降级不能混成一个“加锁”答案。

六、玩家存档的一致性模型 ​

玩家存档通常以数据库为事实来源,缓存只保存可重建副本。读路径要处理 miss、旧值和回源失败;写路径要定义先库后缓存、版本检查、失效和补偿。并发保存时使用版本号或乐观锁,防止慢请求用旧快照覆盖新状态。缓存成功回调只代表缓存操作成功,不代表持久化完成。

七、连接池与慢查询 ​

连接池大小应结合 DB 能力、请求并发和事务时长测量;池满时要排队上限和超时。记录 SQL 模板、耗时、影响行数和 trace ID,不记录敏感数据。

连接池不是越大越好。过多并发会增加数据库线程、锁竞争和缓存抖动;过小则把等待转移到应用队列。应从数据库可承受并发、请求时限和事务占用时间推导,并把“等待连接”和“执行 SQL”分别计时。

八、练习与答案 ​

1. 缓存击穿如何处理? ​

热点 miss 时使用 single-flight/短锁让一个请求回源,其余等待或返回降级值;同时设置合理 TTL 和预热。

2. 为什么 Redis 成功不代表最终一致? ​

数据库写入可能尚未完成,缓存也可能被旧请求覆盖;必须定义写入顺序、版本和失败补偿。

九、阶段输出 ​

提交数据字典、索引和 Explain 记录、缓存策略、故障演练结果及数据恢复说明。