Go 获取随机数是开发中经常会用到的功能, 不过这个里面还是有一些坑存在的, 本文将完全剖析 Go math/rand, 让你轻松使用 Go Rand.

开篇一问: 你觉得 rand 会 panic 吗 ?

源码剖析

math/rand 源码其实很简单, 就两个比较重要的函数

func (rng *rngSource) Seed(seed int64) {
rng.tap = 0
rng.feed = rngLen - rngTap //...
x := int32(seed)
for i := -20; i < rngLen; i++ {
x = seedrand(x)
if i >= 0 {
var u int64
u = int64(x) << 40
x = seedrand(x)
u ^= int64(x) << 20
x = seedrand(x)
u ^= int64(x)
u ^= rngCooked[i]
rng.vec[i] = u
}
}
}

这个函数就是在设置 seed, 其实就是对 rng.vec 各个位置设置对应的值. rng.vec 的大小是 607.

func (rng *rngSource) Uint64() uint64 {
rng.tap--
if rng.tap < 0 {
rng.tap += rngLen
} rng.feed--
if rng.feed < 0 {
rng.feed += rngLen
} x := rng.vec[rng.feed] + rng.vec[rng.tap]
rng.vec[rng.feed] = x
return uint64(x)
}

我们在使用不管调用 Intn(), Int31n() 等其他函数, 最终调用到就是这个函数. 可以看到每次调用就是利用 rng.feed rng.tap 从 rng.vec 中取到两个值相加的结果返回了. 同时还是这个结果又重新放入 rng.vec.

在这里需要注意使用 rng.go 的 rngSource 时, 由于 rng.vec 在获取随机数时会同时设置 rng.vec 的值, 当多 goroutine 同时调用时就会有数据竞争问题. math/rand 采用在调用 rngSource 时加锁 sync.Mutex 解决.

func (r *lockedSource) Uint64() (n uint64) {
r.lk.Lock()
n = r.src.Uint64()
r.lk.Unlock()
return
}

另外我们能直接使用 rand.Seed(), rand.Intn(100), 是因为 math/rand 初始化了一个全局的 globalRand 变量.

var globalRand = New(&lockedSource{src: NewSource(1).(*rngSource)})

func Seed(seed int64) { globalRand.Seed(seed) }

func Uint32() uint32 { return globalRand.Uint32() }

需要注意到由于调用 rngSource 加了锁, 所以直接使用 rand.Int32() 会导致全局的 goroutine 锁竞争, 所以在高并发场景时, 当你的程序的性能是卡在这里的话, 你需要考虑利用 New(&lockedSource{src: NewSource(1).(*rngSource)}) 为不同的模块生成单独的 rand. 不过根据目前的实践来看, 使用全局的 globalRand 锁竞争并没有我们想象中那么激烈. 使用 New 生成新的 rand 里面是有坑的, 开篇的 panic 就是这么产生的, 后面具体再说.

种子(seed)到底起什么作用 ?

func main() {
for i := 0; i < 10; i++ {
fmt.Printf("current:%d\n", time.Now().Unix())
rand.Seed(time.Now().Unix())
fmt.Println(rand.Intn(100))
}
}

结果:

current:1613814632
65
current:1613814632
65
current:1613814632
65
...

这个例子能得出一个结论: 相同种子,每次运行的结果都是一样的. 这是为什么呢?

在使用 math/rand 的时候, 一定需要通过调用 rand.Seed 来设置种子, 其实就是给 rng.vec 的 607 个槽设置对应的值. 通过上面的源码那可以看出来, rand.Seed 会调用一个 seedrand 的函数, 来计算对应槽的值.

func seedrand(x int32) int32 {
const (
A = 48271
Q = 44488
R = 3399
) hi := x / Q
lo := x % Q
x = A*lo - R*hi
if x < 0 {
x += int32max
}
return x
}

这个函数的计算结果并不是随机的, 而是根据 seed 实际算出来的. 另外这个函数并不是随便写的, 是有相关的数学证明的.

这也导致了相同的 seed, 最终设置到 rng.vec里面的值是相同的, 通过 Intn 取出的也是相同的值

我遇到的那些坑

1. rand panic

文章开头的截图就是项目开发中使用别人封装的底层库, 在某天出现的 panic. 大概实现的代码

// random.go

var (
rrRand = rand.New(rand.NewSource(time.Now().Unix()))
) type Random struct{} func (r *Random) Balance(sf *service.Service) ([]string, error) {
// .. 通过服务发现获取到一堆ip+port, 然后随机拿到其中的一些ip和port出来
randIndexes := rrRand.Perm(randMax) // 返回这些ip 和port
}

这个 Random 会被并发调用, 由于 rrRand 不是并发安全的, 所以就导致了调用 rrRand.Perm 时偶尔会出现 panic 情况.

在使用 math/rand 的时候, 有些人使用 math.Intn() 看了下注释发现是全局共享了一个锁, 担心出现锁竞争, 所以用 rand.New 来初始化一个新的 rand, 但是要注意到 rand.New 初始化出来的 rand 并不是并发安全的.

修复方案: 就是把 rrRand 换成了 globalRand, 在线上高并发场景下, 发现全局锁影响并不大.

2. 获取的都是同一个机器

同样也是底层封装的 rpc 库, 使用 random 的方式来流量分发, 在线上跑了一段时间后, 流量都路由到一台机器上了, 导致服务直接宕机. 大概实现代码:

func Call(ctx *gin.Context, method string, service string, data map[string]interface{}) (buf []byte, err error) {
ins, err := ral.GetInstance(ctx, ral.TYPE_HTTP, service)
if err != nil {
// 错误处理
}
defer ins.Release() if b, e := ins.Request(ctx, method, data, head); e == nil {
// 错误处理
}
// 其他逻辑, 重试等等
} func GetInstance(ctx *gin.Context, modType string, name string) (*Instance, error) {
// 其他逻辑.. switch res.Strategy {
case WITH_RANDOM:
if res.rand == nil {
res.rand = rand.New(rand.NewSource(time.Now().Unix()))
}
which = res.rand.Intn(res.count)
case 其他负载均衡查了
} // 返回其中一个ip和port
}

引起问题的原因: 可以看出来每次请求到来都是利用 GetInstance 来获取一个 ip 和 port, 如果采用 Random 方式的流量负载均衡, 每次都是重新初始化一个 rand. 我们已经知道当设置相同的种子,每次运行的结果都是一样的. 当瞬间流量过大时, 并发请求 GetInstance, 由于那一刻 time.Now().Unix() 的值是一样的, 这样就会导致获取到随机数都是一样的, 所以就导致最后获取到的 ip, port都是一样的, 流量都分发到这台机器上了.

修复方案: 修改成 globalRand 即可.

rand 未来期望

说到这里基本上可以看出来, 为了防止全局锁竞争问题, 在使用 math/rand 的时候, 首先都会想到自定义 rand, 但是就容易整出来莫名其妙的问题.

为什么 math/rand 需要加锁呢?

大家都知道 math/rand 是伪随机的, 但是在设置完 seed 后, rng.vec 数组的值基本上就确定下来了, 这明显就不是随机了, 为了增加随机性, 通过 Uint64() 获取到随机数后, 还会重新去设置 rng.vec. 由于存在并发获取随机数的需求, 也就有了并发设置 rng.vec 的值, 所以需要对 rng.vec 加锁保护.

使用 rand.Intn() 确实会有全局锁竞争问题, 你觉得 math/rand 未来会优化吗? 以及如何优化? 欢迎留言讨论

一文完全掌握 Go math/rand的更多相关文章

  1. golang之math/rand随机数

    简单的随机数生成,结合时间模块初始化种子 package main import ( "fmt" "math/rand" "time" ) ...

  2. golang——随机数(math/rand包与crypto/rand包)

    1.math/rand 包 1.1.math/rand 包实现了伪随机数生成器 1.2.主要方法 (1)func Seed(seed int64) 设置随机种子,不设置则默认Seed(1) (2)fu ...

  3. 这可能是最容易理解的 Go Mutex 源码剖析

    Hi,大家好,我是 haohongfan. 上一篇文章<一文完全掌握 Go math/rand>,我们知道 math/rand 的 global rand 有一个全局锁,我的文章里面有一句 ...

  4. Go文档:go命令

    目录 go go bug--启动bug报告 go build--编译包及其依赖包 go clean--删除对象文件和缓存文件 go doc--查看包或符号的文档 go env--打印环境变量 go f ...

  5. Redis源代码分析(23)--- CRC循环冗余算法RAND随机数的算法

    他今天就开始学习Redis源代码的一些工具来实现,在任何一种语言工具.算法实现的原理应该是相同的,一些比較经典的算法.比方说我今天看的Crc循环冗余校验算法和rand随机数产生算法. CRC算法全称循 ...

  6. Leetcode_5.最长回文子串

    最长回文子串 题目描述: 给定一个字符串 s,找到 s 中最长的回文子串.你可以假设 s 的最大长度为 1000. 示例 1: 输入: "babad" 输出: "bab& ...

  7. Go Rand小结

    对于Random的使用,在业务中使用频率是非常高的,本文就小结下常用的方法: 在Golang中,有两个包提供了rand,分别为 "math/rand" 和 "crypto ...

  8. 领扣-5 最长回文子串 Longest Palindromic Substring MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  9. 【LeetCode】5# 最长回文子串

    题目描述 给定一个字符串 s,找到 s 中最长的回文子串.你可以假设 s 的最大长度为 1000. 示例 1: 输入: "babad" 输出: "bab" 注意 ...

随机推荐

  1. 运行maven遇到的坑,差点崩溃了。

    参考链接1:https://blog.csdn.net/lch_cn/article/details/8225448/ 参考链接2:https://jingyan.baidu.com/article/ ...

  2. Vue学习笔记-API调试工具--->国产apipost按装(比postman好按装好用)

    一  使用环境: windows 7 64位操作系统 二   Vue学习笔记-API调试工具--->apipost按装 1.下载: https://www.apipost.cn/ (比postm ...

  3. Kubernetes中分布式存储Rook-Ceph的使用:一个ASP.NET Core MVC的案例

    在<Kubernetes中分布式存储Rook-Ceph部署快速演练>文章中,我快速介绍了Kubernetes中分布式存储Rook-Ceph的部署过程,这里介绍如何在部署于Kubernete ...

  4. C#日志使用

    本文参考链接 日志框架 框架选择:NLog 安装方法,Nuget命令行:Install-Package NLog 常用规则 尽量不要在循环中打印日志. 应输出错误的堆栈信息:e.Message仅为异常 ...

  5. 为WebView 同步cookie

    import android.os.Build;import android.text.TextUtils;import android.webkit.CookieManager;import and ...

  6. CMD(命令提示符)的基本操作(文件)

    打开CMD窗口,接下来将介绍如何使用CMD来创建.删除.修改.查看文件 1.1 使用CMD创建空文件(为了更好的演示,本文皆以D盘为当前路径),命令如下: copy nul xxx.xx(文件名) 命 ...

  7. 后端程序员之路 8、一种内存kv数据库的实现

    键值(Key-Value)存储数据库,这是一种NoSQL(非关系型数据库)模型,其数据按照键值对的形式进行组织.索引和存储.KV存储非常适合不涉及过多数据关系业务关系的业务数据,同时能有效减少读写磁盘 ...

  8. 网络地址转换NAT的两种模式(概念浅析)& IP溯源

    由于全球IPv4地址越来越少.越来越贵,因此大到一个组织,小到一个家庭一个人都很难获得公网IP地址,所以只能使用内网地址,从而和别人共享一个公网IP地址.在这种情况下,NAT技术诞生. 翻译 NAT( ...

  9. 《C++ Primer》笔记 第2章 变量和基本类型

    如果你的数值超过了int表示范围,选用long long 如果你需要使用一个不大的整数,那么明确指定它的类型是signed char或者unsigned char 执行浮点数运算选用double 当一 ...

  10. dubbo实战之一:准备和初体验

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...