游戏服务器开发指南(七):资源流通保证先减后增的顺序

大家好!我是长三月,一位在游戏行业工作多年的老程序员,专注于分享服务器开发相关的文章。

本文是通用程序设计主题下的第二篇。这个主题主要探讨如何编写高效、健壮、易读的游戏业务代码,每篇从一个小点切入。本次讨论的原则是:资源流通应保证先减后增的逻辑顺序,这样编写的代码通常具有更好的健壮性。

从数值的角度来看,游戏本质上是就是不同资源的持续流通。资源的流通包括两种:一种是资源转换,即消耗A资源生成B资源,例如购买商品就是花钱获得物品;另一种是资源转移,即从A所有者转移到B所有者手中,例如为公会捐钱,钱从个人转移到了公会下面。

无论是哪种资源流通,我们都有两种处理顺序:先增后减,或者先减后增。粗略看上去这两者没有什么不同,但实际在容错性上有显著差别。

让我们回顾在前一篇《条件判断永远放在状态变更前》中讲到的购买商品例子,假设我们将其逻辑修改为先增后减:

public Msg buy(int playerId, int commodityId, int num) {
	if (!isCommodityIdValid(commodityId)) {	// 判断商品id是否存在
		return newFailMsg("错误的商品id");
	}
	if (!isCommodityEnough(commodityId)) {	// 判断商店中商品数量是否足够
		return newFailMsg("这种商品剩余数量已不足");
	}
	int costMoney = getCommodityCost(commodityId, num);
	if (!isMoneyEnough(playerId, costMoney)) {	// 判断玩家金币是否足够
		return newFailMsg("您的金币不足");
	}
	deliverCommodity(playerId, commodity, num);	// 发送商品给玩家
	consumeMoney(playerId, costMoney);	// 消耗金币 
	return newSuccMsg(playerId, commodityId, num);
}

那么当代码执行consumeMoney出错而中断逻辑时,会造成非常糟糕的后果:玩家每调用一次购买接口,都会无偿获得商品,而不用消耗任何钱!这就是最让人头大的刷资源情况。如果刷资源的人数众多,而且刷到的资源又被转换成了其他资源不好追溯,那么不得已只能使用最后的保留手段——回档。这对玩家体验无疑是巨大的伤害。

再让我们考虑改回先减后增的顺序。如果在deliverCommodity这一步报错,那么会造成玩家花了钱却没能获得商品。这样的影响相对可控,因为玩家尝试过几次之后就会发现问题而主动停下来,即使没有发现,等钱花完也会被迫停下来。更重要的是,数据在事后是可恢复的,只需要检查报错日志和接口日志,就能计算出玩家错误扣除的金币数额,再给玩家补上,而不像刷资源那样只能使用回档的方式来暴力解决。所以,先减后增优于先增后减的第一个理由是,前者在代码出错时造成的影响更小更可控

很多游戏服务器使用Actor模型来开发,这样可以避免加锁和线程同步的烦恼。不过,Actor模型也引入了其他并发模型下所没有的一些陷阱,常见的例如,Actor异步通信下接口执行不再保证原子性,而可能分步乱序执行。在这种情况下,先减后增和先增后减两种逻辑顺序也会带来不同的结果。

让我们看一个为公会捐钱的例子。玩家加入公会后,可以为公会捐钱,让公会变得更强。这个过程是钱从玩家手中转移到公会下面,用Skynet来写就是:

function command.endow(player, endow_num)
    if player.money < endow_num then   -- 如果玩家持有的金币数不够,直接返回
        return
    end
    local success = skynet.call(union, "lua", "endow",  endow_num);	-- 向公会agent请求捐款
    if success then
        player.money = player.money - endow_num
    end
end

以上代码是按照先增后减的顺序,即先向公会agent请求,让公会得到这笔钱,然后如果捐款成功,那么再扣除自己的钱。

这样写的问题在于,Skynet在处理阻塞操作时,会暂停当前协程的执行,转而执行别的协程逻辑,直到阻塞操作返回才从暂停上下文重新开始执行。如果在执行别的协程逻辑时,恰好遇到了一个扣玩家钱的操作,如购买商品,那么可能导致后面再实际扣捐款钱时已经不够,钱扣成负的。

解决办法是调整逻辑顺序,改为先减后增的顺序,即先行扣除要捐的钱,再向公会agent请求捐款,如果捐款失败,那么再把钱返还给玩家。具体代码如下:

function command.endow(player, endow_num)
    if player.money < endow_num then   -- 如果玩家持有的金币数不够,直接返回
        return
    end
    player.money = player.money - endow_num	-- 先扣钱
    local success = skynet.call(union, "lua", "endow",  endow_num);	-- 向公会agent请求捐款
    if not success then	-- 如果捐款失败,再把钱加回来
        player.money = player.money + endow_num
    end
end

这里不难得出先减后增更优的第二个理由:在Actor模型下,先减后增有助于避免异步通信下接口乱序执行带来的逻辑错误

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值