游戏数据零点更新问题

具体请参考原文链接

 

 

 

接上一篇【游戏架构】游戏部署分库分服的重要性 中提到游戏数据处理的一个难点,就是零点数据重置,容易出现数据不同步或者延迟的情况。分库分服只能降低数据库的压力,尽量避免出现数据库压力爆表的情况,但这不是解决游戏数据零点重置问题的根本办法。

按照常规设计思路,一个数据需要零点进行重置,我们通常会写一个零点定时器,到了每天零点,去重置玩家的数据,比较挫一点的写法是,定时器触发后,以玩家为单位进行更新,循环所有玩家,先把玩家内存的数据重置,然后把玩家的数据更新到数据库。

这种重置模式,在玩家数量少的时候没什么感觉,数量多了,比如几千个玩家就要重复几千个同样的数据库请求,会发现效率十分低下,这个时间的数据库压力很大。

怎么办?只能写一些批量重置的SQL语句,但是批量重置的写法破坏了玩家面向对象的特性,而且这样的写法其实十分丑陋,但是丑陋归丑陋,胜在实用。采用批量重置的办法确实可以有效减轻数据库请求次数,降低数据库零点压力。

但是,批量操作虽然好,却会有其他一些多线程问题。举个例子,零点的时候触发了数据重置定时器,定时器是一个单独的线程处理的,此时内存数据的重置是即时的,并通知了客户端,但这时候数据库批量重置没那么快,还没重置完(假设需要10秒钟),然后玩家在0点0分05秒的时候进行了操作,并通过游戏主线程对玩家数据进行了数据库更新,这个更新很快成功了。最后发现,这次操作被被姗姗来迟的批量重置语句覆盖了,这时候就形成了内存数据和数据库数据的不一致。

内存数据已经有新的数据,而数据库是0,在某些极端情况下,比如这个玩家又恰好下线或者因为某种方式重新更新了数据库数据,他就会出现零点被重置两次的情况,可以获得两次收益。这个案例确实存在,虽然出现的几率很低很低,但是确实出现过,而且之前排查问题的时候感觉毫无头绪。

后来我发现以个人为单位进行数据重置也会存在这个问题,这其实根本原因是一个多线程问题,只要你是多线程去处理玩家数据,就存在出现这种问题的可能性。

俗话说,解决多线程问题的办法就是用单线程,哈哈哈。

开个玩笑,不过我们确实采用了一种更加折中的办法,个人数据在个人登录的时候进行重置,这样玩家数据就在当前线程完成了重置,每日零点只对小部分还在线的玩家进行重置。玩家数据里存了一个最近一次重置的时间,登录的时候根据重置时间来判断个人数据是否需要进行重置,从而分散玩家重置的过程,让零点的压力减轻,这套方案我沿用了两个游戏,确实很有成效,但很遗憾的是,它依然没有解决在线玩家零点重置的时候这个问题。

于是,我开始思考,怎么样才能真正解决游戏里的零点重置问题。

在解决玩家数据拓展性时,之前的文章【游戏架构】如何解决玩家数据的拓展性 里有提到,我对玩家的每日数据写了这样一个k-v结构,来解决数据拓展性问题。针对每个玩家数据,这个结构的主键是roleId-dataType。

data class MemRoleDaily(
        var recordId: Int = 0,
        var roleId: Int = 0,
        var dataType: Int = 0,              // 数据类型
        var dataValue: Int = 0              // 数据值
) 

写完之后我就在考虑这个结构的重置问题,绕来绕去,还是绕不开之前的问题。于是我就思考这个数据的真实目的,它就是个每日数据,每日数据必然是只针对某一天生效,那么我是不是可以加入一个日期标签,让这个数据变成某一天的专属数据。

这个思路让我豁然开朗,于是我把结构调整为如下

data class MemRoleDaily(
        var recordId: Int = 0,
        var roleId: Int = 0,
        var date: Int = 0,                  // 数据所属日期
        var dataType: Int = 0,              // 数据类型
        var dataValue: Int = 0              // 数据值
)

这时候,这个玩家数据的主键变成roleId-date-dataType,日期成为了每日数据中关键的一个标记,所有数据都围绕这个标记展开。这个date,我写了一个实时获取函数,来获取当时当下最新的日期标记。

fun date(): Int {
    val c = Calendar.getInstance()
    return c.get(Calendar.YEAR) * 10000 + (c.get(Calendar.MONTH) + 1) * 100 + c.get(Calendar.DATE)
}

然后就是修改对外的获取和设置数据接口,这两个接口可以保证你无论何时调用,都是当天最新的数据,并可以准确更新到数据库。在零点到来的一刹那,数据已经完成了自动切换,不,它其实根本不存在切换,因为有就是有,没有就是没有。

    fun getValue(dataType: Int): Int {
        val date = date()
        val memRoleDaily = dataSet.find { it.date == date && it.dataType == dataType }
        return memRoleDaily?.dataValue ?: 0
    }

    fun setValue(dataType: Int, value: Int) {
        if (value < 0) {
            return
        }

        val date = date()
        val memRoleDaily = dataSet.find { it.date == date && it.dataType == dataType }
        if (memRoleDaily == null) {
            create(date, dataType, value)
        } else {
            memRoleDaily.dataValue = value
            update(memRoleDaily)
        }
    }

最核心的一点是,这个结构无须在零点去清理数据库。你可以在任意一个你想清理的时间去批量清理老旧数据,同时你可以清理也可以不清理,这都不会对你现在的数据准确性造成影响。这个结构甚至都不用清理内存数据,依然能保证你每天的数据都是最新的。

当然,必要的数据清理还是要做的,必要的数据库清理也是要做的,但是这些很容易实现。我自己其实也没想到,解决数据零点依赖的根本办法竟然是消灭零点更新,一个看起来简单至极的数据结构,却有化腐朽为神奇的感觉。而且,从每日数据可以延伸到很多其他数据,比如每周数据,每月数据,每年数据等等,这个结构都会让你的系统更加强壮。

写了一堆废话,谢谢你看完了,哈哈哈。有时候大道至简,返璞归真,写代码也一样,怎么让代码拓展性更强,更加健壮,更加稳定或许是一个优秀码农必备的技能。下一期和大家讲讲怎么让活动结构变得通用起来。

posted @ 2020-10-16 17:34  DNoSay  阅读(349)  评论(0编辑  收藏  举报