- 打卡等级:魔龙套勇士
- 打卡总天数:130
- 打卡月天数:23
- 打卡总奖励:14868
- 最近打卡:2025-08-23 00:38:01
管理员
本站站长
- 积分
- 8609
|
确保异常场景下抽奖系统的数据一致性,核心是保障「消耗道具 - 生成奖励 - 记录日志」全链路的原子性(要么全成功,要么全失败),即使遭遇网络中断、服务器宕机、数据库故障等异常,也能避免 “扣券未发奖”“发奖未扣券”“重复发奖” 等数据不一致问题。以下是结合传奇游戏引擎特性的具体实现方案:
一、核心原则:构建 “原子化 + 可追溯” 的抽奖链路
异常场景下的数据一致性依赖于两个核心机制:
原子操作:抽奖的 “扣减道具”“发放奖励”“记录日志” 必须作为一个不可分割的整体,要么全部执行成功,要么全部回滚。
可追溯性:通过详细日志记录每一步操作,异常发生后可通过日志逆向恢复或正向补全数据。
二、分场景解决方案
1. 网络中断 / 客户端崩溃(最常见异常)
问题:玩家点击抽奖后,客户端与服务器断开连接,可能导致 “扣了券但未收到奖励”(服务器已扣券但未发奖)或 “未扣券却收到奖励”(服务器未扣券但客户端显示中奖)。
解决方案:
服务器端 “预扣 + 确认” 机制:
玩家发起抽奖时,服务器先执行 “预扣券”(如将抽奖券从 10 变为 9,并标记状态为 “待确认”),同时生成唯一抽奖 ID(如LuckyID=20250818123456)。
服务器完成奖励计算后,向客户端发送 “中奖结果”,并等待客户端返回 “确认收到” 的响应。
若收到客户端确认,服务器将 “预扣” 改为 “实际扣减”,发放奖励并记录日志;若未收到确认(如超时 5 秒),则自动回滚 “预扣券”(恢复为 10)。
引擎实现示例(M2 引擎脚本):
qbasic
[@LuckyDrawRequest]
#ACT
; 1. 生成唯一抽奖ID(时间戳+角色ID)
MOV S0 <$STR(TIME)>_<$USERID>
; 2. 预扣抽奖券,标记状态为“待确认”
TAKE 抽奖券 1
WRITEVAR GLOBAL LuckyStatus_<$S0> 0 ; 0=待确认,1=已完成
; 3. 计算奖励
CALL @CalcReward
MOV S1 <$STR(0)> ; 中奖物品
; 4. 发送结果给客户端,等待确认
SENDMSG 7 抽奖结果:<$S1>,请确认[LuckyID=$S0]
SETTIMER 5 @CheckClientConfirm 5000 ; 5秒后检查客户端确认
[@CheckClientConfirm]
#IF
CHECKVAR GLOBAL LuckyStatus_<$S0> 1 ; 客户端已确认
#ACT
GIVE <$S1> 1 ; 发奖
WRITEVAR GLOBAL LuckyStatus_<$S0> 1 ; 标记为已完成
RECORDLOG LuckyLog.txt <$S0>,<$USERID>,<$S1> ; 记录日志
#ELSE
; 未确认,回滚扣券
GIVE 抽奖券 1
WRITEVAR GLOBAL LuckyStatus_<$S0> 2 ; 2=已回滚
BREAK
客户端本地缓存与校验:
客户端收到 “中奖结果” 后,先缓存LuckyID和奖励信息,再向服务器发送 “确认”;若客户端崩溃重启,重新登录时主动向服务器查询未完成的LuckyID(通过角色 ID 关联),服务器根据LuckyStatus补全奖励或说明状态。
2. 服务器宕机 / 进程崩溃
问题:抽奖过程中服务器突然断电或进程崩溃,可能导致 “扣券和发奖只执行了一半”(如已扣券但未发奖,且无日志记录)。
解决方案:
事务日志(Write-Ahead Logging, WAL):
服务器执行抽奖前,先将 “扣券数量、目标奖励、角色 ID、时间戳” 写入不可变日志文件(如LuckyTransLog.txt),格式如下:
plaintext
[未完成]|LuckyID=20250818123456|RoleID=123|Cost=1|Reward=屠龙刀|Time=1692347696
日志写入成功后,再执行实际的扣券和发奖操作;操作完成后,将日志状态改为 “已完成”:
plaintext
[已完成]|LuckyID=20250818123456|RoleID=123|Cost=1|Reward=屠龙刀|Time=1692347696
服务器重启时,自动扫描LuckyTransLog.txt中的 “未完成” 记录,根据日志信息:
若已扣券但未发奖:补发奖励,更新日志为 “已完成”。
若未扣券也未发奖:忽略或回滚(无实际操作)。
若未扣券但发了奖(极端情况):从玩家背包回收奖励(需日志记录奖励唯一 ID)。
引擎适配:
M2 引擎:通过Envir\Logs目录的CustomLog功能实现 WAL,重启时执行@RecoveryLuckyData脚本扫描日志。
Blue 引擎:利用BlueEngine.ini中的AutoRecovery=1配置,自动触发日志恢复流程。
3. 数据库故障(如 MySQL 宕机)
问题:抽奖依赖数据库存储玩家道具和抽奖记录,数据库宕机可能导致 “内存中操作成功但未持久化”(如服务器内存中已扣券, but 数据库未更新)。
解决方案:
多级缓存与异步写入:
抽奖时先操作内存缓存(如 Redis):扣减缓存中的抽奖券数量,记录缓存中的奖励信息(设置 10 分钟过期)。
将数据库写入操作放入异步队列(如 RabbitMQ),由后台线程定期批量写入数据库(即使数据库宕机,队列也会暂存任务)。
数据库恢复后,队列中的任务自动执行,将缓存数据同步至数据库;若缓存过期前未同步,通过 WAL 日志补全。
示例(GOM 引擎微端):
ini
; GOMConfig.ini 配置缓存与队列
[Cache]
RedisServer=127.0.0.1:6379
LuckyDrawKey=RoleID:%s:Lucky ; 缓存键:角色ID+抽奖信息
[Queue]
MQServer=127.0.0.1:5672
LuckyDrawQueue=db_lucky_queue ; 数据库写入队列
双写一致性校验:
定期(如每小时)对比 Redis 缓存与数据库中的 “抽奖券数量”“奖励总数”,发现不一致时以数据库为准(缓存同步数据库),并记录差异日志供人工排查。
4. 并发冲突(如同一玩家同时发起多次抽奖)
问题:玩家快速点击抽奖按钮,可能导致服务器同时处理多个请求,出现 “1 张券抽 2 次奖”(并发扣券校验失效)。
解决方案:
分布式锁:
为每个玩家的抽奖操作加锁(如基于 Redis 的SETNX命令),同一时间只允许一个抽奖请求执行:
python
运行
# 伪代码(服务器端)
def lucky_draw(role_id):
lock_key = f"lucky_lock:{role_id}"
# 尝试获取锁,5秒过期(避免死锁)
if redis.set(lock_key, "1", nx=True, ex=5):
try:
# 校验抽奖券数量(从数据库读取,避免缓存脏读)
if get_ticket_count(role_id) < 1:
return "券不足"
# 执行扣券、发奖、记录日志
deduct_ticket(role_id, 1)
reward = calc_reward()
give_reward(role_id, reward)
return f"获得{reward}"
finally:
redis.delete(lock_key) # 释放锁
else:
return "操作频繁,请稍后再试"
幂等性设计:
为每个抽奖请求生成唯一RequestID(客户端生成,如 UUID),服务器端记录已处理的RequestID,重复请求直接返回历史结果,避免重复执行。
三、数据一致性校验与修复机制
即使通过上述机制预防,仍可能因极端情况(如硬件故障)导致数据不一致,需通过定期对账和自动修复兜底:
1. 对账规则(每日执行)
基础对账:
统计 “总扣券数”(从玩家背包扣减记录汇总)与 “总抽奖次数”(日志记录汇总),两者应相等(允许 ±1 误差,因未完成的预扣)。
奖励对账:
统计 “总发奖数”(玩家背包新增记录)与 “日志中奖励总数”,两者应相等(高价值道具需逐笔核对,如屠龙刀的发放记录)。
示例对账 SQL(适用于 MySQL 数据库):
sql
-- 总扣券数 vs 总抽奖次数
SELECT
(SELECT SUM(count) FROM ticket_deduct_log) AS total_deduct,
(SELECT COUNT(*) FROM lucky_draw_log) AS total_draw;
-- 屠龙刀发放数 vs 日志记录数
SELECT
(SELECT COUNT(*) FROM item_add_log WHERE item_name='屠龙刀') AS total_reward,
(SELECT COUNT(*) FROM lucky_draw_log WHERE reward='屠龙刀') AS total_log;
2. 自动修复策略
若 “总扣券数> 总抽奖次数”:说明存在 “扣券未记录”,需通过ticket_deduct_log补全lucky_draw_log中缺失的记录(标记为 “异常扣券”)。
若 “总发奖数> 日志记录数”:说明存在 “无日志发奖”,需检查item_add_log中对应奖励的来源,非抽奖渠道的忽略,抽奖渠道的补全日志。
高价值道具差异:人工介入核查,通过 WAL 日志和玩家操作记录追溯,必要时通过 GM 工具回收异常奖励或补发遗漏奖励。
四、引擎特性适配总结
引擎类型 异常场景解决方案 核心工具 / 配置
M2 脚本实现 “预扣 + 确认” 机制,WAL 日志存于Envir\Logs DBChecker(数据库校验)、CustomLog(自定义日志)
HERO 分布式锁 +QFunction.txt幂等性校验 HeroRedis(缓存集成)、ScriptLock(脚本锁)
Blue 异步队列 +BlueEngine.ini自动恢复配置 BlueMQ(消息队列)、AutoRecovery=1(自动恢复)
996 移动端本地缓存 + 服务器端对账脚本 Lua缓存逻辑、cfg_check_lucky.xls(对账配置)
核心结论
异常场景下的数据一致性,需通过 “预防(原子操作 + 锁)- 记录(日志 + 缓存)- 恢复(对账 + 修复)” 三层机制保障:
预防层:用事务、锁、幂等性避免不一致。
记录层:用 WAL 日志、异步队列确保操作可追溯。
恢复层:用对账脚本和人工介入修正残留不一致。
结合传奇游戏引擎的特性(如 M2 的脚本锁、Blue 的自动恢复),可实现高可靠的抽奖系统数据一致性,即使在极端异常下也能保障玩家权益与游戏公平性。
|
|