设为首页收藏本站
  • 官方微信
    lmkj_wx 微信公众号 添加方式:
    1:扫描左侧二维码
  • 手机访问
    lmkj_sj
  •  找回密码
     立即注册

    QQ登录

    只需一步,快速开始

    查看: 15|回复: 0

    如何确保异常场景下抽奖系统的数据一致性

    [复制链接]
    avatar
    • 打卡等级:魔龙套勇士
    • 打卡总天数:130
    • 打卡月天数:23
    • 打卡总奖励:14868
    • 最近打卡:2025-08-23 00:38:01

    7043

    主题

    150

    回帖

    8609

    积分

    管理员

    本站站长

    积分
    8609
    online_admin 发表于 5 天前 | 显示全部楼层 |阅读模式
    确保异常场景下抽奖系统的数据一致性,核心是保障「消耗道具 - 生成奖励 - 记录日志」全链路的原子性(要么全成功,要么全失败),即使遭遇网络中断、服务器宕机、数据库故障等异常,也能避免 “扣券未发奖”“发奖未扣券”“重复发奖” 等数据不一致问题。以下是结合传奇游戏引擎特性的具体实现方案:
    一、核心原则:构建 “原子化 + 可追溯” 的抽奖链路
    异常场景下的数据一致性依赖于两个核心机制:

    原子操作:抽奖的 “扣减道具”“发放奖励”“记录日志” 必须作为一个不可分割的整体,要么全部执行成功,要么全部回滚。
    可追溯性:通过详细日志记录每一步操作,异常发生后可通过日志逆向恢复或正向补全数据。
    二、分场景解决方案
    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 的自动恢复),可实现高可靠的抽奖系统数据一致性,即使在极端异常下也能保障玩家权益与游戏公平性。

    您需要登录后才可以回帖 登录 | 立即注册 qq_login

    本版积分规则

    QQArchiver 手机版 小黑屋 39传奇素材网 ( 蜀ICP备2022016510号-3 )

    快速回复 快速发帖 返回顶部 返回列表