外观
路线 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 记录、缓存策略、故障演练结果及数据恢复说明。
