今天下午,突然线上出现大量高级掉落重复出现。
正常的概率是1%,结果部分玩家出现了很多。导致了群里炸了锅。于是开始分析掉落部分代码:

这里做过一次优化,以前是不停的new 一个Random对象。后面测试时候发现效率不高,于是采用了复用的方式。现在似乎这里除了问题,分析下来应该是Random在某种条件下输出了相同值。

问题分析

既然Random出了问题,那么有下面几个问题:
1. 触发Random出问题的条件是什么?
2. Random出问题后会输出什么值呢?
3. 假如是固定值,是有条件固定还是无条件呢?

触发条件

有一种说法是:由于不指定seed情况下,random会默认用时间的tick来当作种子。如果在一个tick内重复过多的调用,则会产生固定值。

不过根据线上情况分析:这个问题持续了半小时。所以这个马上被否决了。

接下来搜索后得到了一个线索:

并且有人提到当random停止工作后,他固定返回0。

多线程环境下,的确使用static的random会产生这样的问题。这样来看应该是这里对于random的优化不当了。不过随之而来的是,固定值真的是0么?

固定值

停摆后的random的固定输出值是什么?真的是0。

同理,线上的表现否定了这个结果。因为如果是0,玩家的掉落数目也使用了这个随机数,所以他们应该获取不到这个异常的结果。。。

那么究竟是什么值呢?只能通过模拟多线程来压测了

具体代码如上,在多线程环境下模拟随机过程,推论是:如果正常他应该输出比较均匀的分布,如果异常应该可以发现某个值特别大。

当次数低于100000次,我并没有看到太多异常。当超过这个值后有趣的事情发生了:

这三次异常的结果我差不多测试了30多遍才出来。也就是说差不多只有10%的概率出现Random罢工。这样的确证实了前面的说的多线程情况下, random输出固定值。可以看到一开始应该是均匀分布的,一旦出现多线程问题,马上罢工。

固定条件

Ranom 罢工后会输出固定值这点确认了。但不是网上大家说的是0,而是取决于我输入的min值。这样就解释了线上的大部分疑惑了。并且继续做测试可以发现,这个固定值一旦产生并不是固定不变,而是每次都等于min值。

测试条件为5-10,11-15两个取间同时开始随机取值,检查分布。结果如下图:

看到当第一个红框出现罢工后,下一个Random值不是5,而是直接都输出成11了。

如何改进呢?

原来是通过new 一个Random达到隔离的目的,但现在看来有两个问题:
1. 效率不高
2. 并没有根本解决多线程下罢工的问题。
网上给的方案是采用锁,这样感觉效率也不高。并不能给这个底层方法带来本质的变化。

现在想到的一个方案类似于concurrentDictionary那样,多粒度控制锁的一个Random池子。利用这个数据结构的并发性,来创造多个可以复用的Random实例。每次需要时候采用RondRobin算法获得一个对象即可。

当然更激进的做法就是不用并发对象,直接做1000个实例,感觉出错的概率会下降不少。
以下就是核心代码部分,每次通过到队列中获取random实例,而非反复构造它。当然在竞争十分激烈的情况下有可能拿不到random,这个时候就做了个保底。使用全局的globalRandom.

        static Random DequeueRand(Random random)
        {
            if (random == null)
            {
                if (GlobalRandomQueue.TryDequeue(out Random rand))
                {
                    random = rand;
                    GlobalRandomQueue.Enqueue(rand);
                }
                else
                {
                    // 保底
                    random = globalRandom;
                }
            }
            return random;
        }

测试效果:测试10批,每批10次, 每次1百万条,random函数再也不出现“罢工“情况了