随机数真的随机吗?

152 阅读3分钟

起因

待办

随机算法原理

待办

应用场景

随机需要保持一致

基于随机算法的原理分析,我们只需要保证在某个维度下随机种子相等即可

例如:

  • 针对用户的随机数,可以用用户id作为随机种子
  • 针对订单的随机数,可以用订单id作为随机种子
  • 针对某个区域+时间的随机数,可以用经纬度+时间作为随机种子

随机约乱越好

在有些场景下,我们期望随机数越乱越好,例如发红包 很少会出现均分的红包,哪有应该怎么保证红包随机数不一致呢?

让我们来实现红包算法吧

1.声明一个红包

type RedPacket struct {
   Mu         sync.Mutex //互斥锁
   TotalMoney int        //红包总金额,单位分
   TotalNumber int       //红包总个数
   Money      int        //红包金额,单位分
   Number     int        //红包个数
}

2.红包的金额随机算法

func (redPacket *RedPacket) GetRandMoney() int {
   r := rand.New(rand.NewSource(time.Now().Unix()))
   money := r.Intn(int(float64(redPacket.TotalMoney) * 0.9)) //红包上限系数
   if money == 0 {
      return 1 //兜底1分红包
   } else {
      return money
   }
}

3.实现红包的接收算法

func (redPacket *RedPacket) ReceiveRedPacket() int {
   redPacket.Mu.Lock()
   defer redPacket.Mu.Unlock()

   if redPacket.Number <= 0 || redPacket.Money <= 0 {
      return 0
   } else if redPacket.Number == 1 {
      //最后一个人领取红包所有金额
      money := redPacket.Money
      redPacket.Number = 0
      redPacket.Money = 0
      return money
   } else {
      money := 0
      //重试5次
      for i := 0; i < 5; i++ {
         money = redPacket.GetRandMoney()
         if money <= redPacket.Money {
            break
         }
      }
      if money == 0 || money > redPacket.Money {
         money = 1 //兜底1分红包
      }

      redPacket.Number -= 1
      redPacket.Money -= money
      return money
   }
}

4.实现类

func main() {
   redPacket := RedPacket{
      Mu:         sync.Mutex{},
      TotalMoney: 10000,
      TotalNumber: 4,
      Money:      10000, //100元
      Number:     4,
   }

   var wg sync.WaitGroup
   wg.Add(redPacket.Number)

   for i := 0; i < redPacket.TotalNumber; i++ {
      go func(i int) {
         defer wg.Done()
         fmt.Printf("第%d个人获取了:%.2f元\n", i+1, float64(redPacket.ReceiveRedPacket())/100)
      }(i)
   }

   wg.Wait()
}

看看运行结果

image.png

image.png

可以看出n-1个人的红包金额永远是一样的,为什么会出现这种情况呢?

看看随机生成红包金额的函数

money := r.Intn(int(float64(redPacket.TotalMoney) * 0.9)) //红包上限系数

这里通过协程并发请求模拟了用户同时点击红包行为,导致抢红包的时间是同一时刻,生成的随机金额也是一样的

怎么解决呢?

首先,可以尝试用进度更高的时间精度

r := rand.New(rand.NewSource(time.Now().UnixNano()))

其次,为了解决时间相同导致的随机数生成的相同的情况,我们可以尝试维护一个随机生成的序号,每次生成随机数后+1

改造后的随机算法是这样的

var RandomID int64 = 0
func (redPacket *RedPacket) GetRandMoney() int {
r := rand.New(rand.NewSource(time.Now().Unix() | atomic.LoadInt64(&RandomID)))
   money := r.Intn(int(float64(redPacket.TotalMoney) * 0.9)) //红包上限系数
   atomic.AddInt64(&RandomID, 1)//RandomID+1
   if money == 0 {
      return 1 //兜底1分红包
   } else {
      return money
   }
}

运行后结果

image.png

如果单机服务到这里已经没啥问题了

但是分布式环境下,需要将sync.Mutex转化成分布式锁

同时,由于每个服务器各自维护一套从0开始的RandomID,如果这4个人同时获取红包时正好请求到不同的重启后机器上(RandomID=0)时,也可能造成等额红包

因此可以在随机种子中加上机器号,确保同一时刻不同的重启后机器生成的随机数不同

最后,即使我们为随机数做了这么多,但是由于红包金额大小受限,还是可能会出现等额红包。 我们可以获取上一次发放的红包金额,保证这次和上次不同即可

生产环境中红包生成不会像上述那样,只是希望通过红包这个例子说明,当我们需要在分布式环境制造出差异较大的随机数时,可以通过增加多个维度来保证随机种子尽可能的不一致

总结

  • 随机算法是一种变种的hash算法,当随机种子相等时,总能得到相同的随机结果
  • 利用hash算法的特性
    • 当需要某种维度下(用户,订单,时间....)前后随机一致性时,保证随机种子相等接客(用户id,订单id,时间...)
    • 当需要在某种维度下随机不同时,可以通过多种维度保证随机种子不相等即可