Golang rand.Seed 随机种子使用问题

3,627 阅读4分钟

问题

进行业务开发时,需要随机连接不同节点进行业务实现,所以需要取随机数。 由于没有设定随机种子就导致每次都取到一样的随机数,每次都是连接到同一节点,导致该节点业务压力骤增,进而影响后续的服务调用。

例子复现

// 没有添加随机种子
func main() {
	num := 10
	for j := 0; j < num; j++ {
		v := rand.Intn(num)
		fmt.Print(v)
	}
}

output:1779185060
// 添加随机种子
func main() {
	num := 10
	for j := 0; j < num; j++ {
		rand.Seed(time.Now().Unix())
		v := rand.Intn(num)
		fmt.Println(v)
	}
}

output:7777777777
// 添加随机种子,增加样本大小
func main() {
	num := 10
	randNum := 10000
	for j := 0; j < num; j++ {
		rand.Seed(time.Now().Unix())
		v := rand.Intn(randNum)
		fmt.Printf("%d,", v)
	}
}

output:3877,3877,3877,3877,3877,3877,3877,3877,3877,3877,

例子分析

后续添加了随机种子以及增加样本空间大小,但是结果就是取到的随机值在变,但是基本上都是同一值,问题在于以上对于seed的调用都是在同一个循环中进行,每次的循环间隔都是毫秒级别的,每个循环中得到的时间戳基本一样,所以time.Now().Unix()得到的值基本趋于同一个值,seed用于得到一个确定的随机序列,只要随机种子确定,得到的值就会趋于固定。 golang 官方说明

// Seed uses the provided seed value to initialize the generator to a deterministic state.
// Seed should not be called concurrently with any other Rand method.

解决方案

  • 全局初始化seed,确定随机种子
  • 使用纳秒级别的随机种子
  • 使用crypto/rand

优化

以上的方法建议结合一二使用,如果单独进行纳秒级别的随机种子,如果是在高并发的场景下,在串行业务场景使用还是会遇到纳秒级别的时间戳一样的情况,那得到的还会是重复的随机值。

优化例子

import (
	"fmt"
	"math/rand"
	"time"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

func main() {
	num := 10
	randNum := 10000
	for j := 0; j < num; j++ {
		v := rand.Intn(randNum)
		fmt.Printf("%d,", v)
	}
}

output:9225,9234,1181,4004,7447,9194,3869,1534,9819,9057,

rand 的默认的 Source 为了实现并发安全使用了全局的排他锁,如果需要控制锁的粒度,同时不与其他包竞争全局的排它锁,可以自己 New 一个 Rand 来使用,但是自己 New 的 rand 不是并发安全的,一定要自行加锁。

func main() {
	num := 10
	randNum := 10000
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	for j := 0; j < num; j++ {
		v := r.Intn(randNum)
		fmt.Printf("%d,", v)
	}
}

output:3990,223,5656,6415,8552,3513,3902,9263,8266,1938,

crypto/rand

crypto/rand包实现了用于加解密的更安全的随机数生成器,生成的随机数并不固定,并且一定会提供更好的随机性,内部实现规则:

// Reader is a global, shared instance of a cryptographically
// secure random number generator.
//
// On Linux and FreeBSD, Reader uses getrandom(2) if available, /dev/urandom otherwise.
// On OpenBSD, Reader uses getentropy(2).
// On other Unix-like systems, Reader reads from /dev/urandom.
// On Windows systems, Reader uses the CryptGenRandom API.
// On Wasm, Reader uses the Web Crypto API.
Reader是一个全局、共享的密码用强随机数生成器
在 Linux 和 FreeBSD 系统上,Reader 会使用 getrandom(2) (如果可用的话),否则使用 /dev/urandom。
在 OpenBSD 上,Reader 会使用 getentropy(2)。
在其他的类 Unix 系统上,Reader 会读取 /dev/urandom。
在 Windows 系统上,Reader 会使用 CryptGenRandom API.
在 Wasm 上,Reader 会使用 Web Cryto API。

crypto/rand包较math/rand包有了更安全的随机生成,但性能上较math/rand包有所降低。因为crypto/rand包执行更多的操作并且不能使用预生成的序列。

import (
	"crypto/rand"
	"fmt"
	"math/big"
)

```go
import (
	"crypto/rand"
	"fmt"
	"math/big"
)

func main() {
	num := 10
	for i := 0; i < num; i++ {
		n, err := rand.Int(rand.Reader, big.NewInt(10000))
		if err != nil {
			panic(err)
		}
		fmt.Printf("%d,", n)
	}
}

output:3403,3120,6278,6326,2761,9847,5262,9034,9067,8411,

性能比对

package main

import (
	cryrand "crypto/rand"
	"math/big"
	"math/rand"
	"testing"
)

func BenchmarkMathRand(b *testing.B) {
	for i := 0; i < b.N; i++ {
		rand.Intn(10000)
	}
}

func BenchmarkCryptoRand(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, _ = cryrand.Int(cryrand.Reader, big.NewInt(10000))
	}
}
BenchmarkMathRand
BenchmarkMathRand-12      	82710134	        13.60 ns/op
BenchmarkCryptoRand
BenchmarkCryptoRand-12    	 3863425	       313.9 ns/op

总结

对于 math/rand 与 crypto/rand 的使用应结合实际业务进行,大部分情况下 math/rand的正确使用就可以满足随机数生成的需求。