[Golang] 高频次和高并发下的随机数重复问题的解决方案

一、概要:

在Golang中,获取随机数的方法一般会介绍有两种,一种是基于math/rand的伪随机,一种是基于crypto/rand的真随机。其中,math/rand由于其伪随机的原理,经常会出现重复的随机数,导致在需要进行随机的业务出现较多的重复问题。在作者所经历的实际项目中便遇到在高频率和高并发下获取随机数时的重复问题,在解决该问题的道路上,尝试了几种办法,最终发现了较好的解决方案。

二、对math/rand随机数的研究:

基于math/rand获取随机数,常见的方法是:

import ("math/rand""time"
)r := rand.New(rand.NewSource(time.Now().UnixNano()))
result := r.Int31n(100)

上述代码原理,是通过纳秒时间来生成种子,然后通过该种子获取一个随机数。常用的开发者应该知道,math/rand的种子具有其固定性,同一个数作为种子时,通过New获取的实例其实是相同的,即便它们的实例地址不同,但通过同样的随机数方法,获得的值会是一样的。

这种情况是由于math/rand会根据随机种子进行一个较复杂的算法过程,从而获得一个长度为607的随机数池,由于算法过程是固定的,不存在任何的随机情况,因此同一个种子获取的随机数池是完全一致的。

此时可能会有人认为,每次取不同的纳秒时间来生成实例,不就可以解决问题?但这样的想法在实际的项目实践中是存在问题的。原因主要有两点:
1、Golang的一大优点是速度快,当一次性多次使用time.Now().UnixNano()获取纳秒时间时,会发现纳秒时间并不是不同的,而会存在重复,这使得在高频率随机下随机种子是一样的。
2、Golang的一大特点是高并发,但是在实际的运行过程中,是无法保证两个并发的协程不会在同一时间获取时间的,同样会出现重复问题。

针对这两个问题也有对应的解决方案:
1、对于第一个原因,可以在每次获取纳秒时间前,使用time.Sleep(time.Nanosecond)保证程序过一个时间再取,如此可以保证每次获取的时间是不重复的。
2、对于第二个原因,可以利用锁将获取时间的过程锁住,以保证并发时不在同一时间执行。

然而在实际项目中实践的情况并不理想。
第一个解决方案下,由于CPU处理的时间片,跳过的时间并不是1纳秒,不同的CPU会有微小差异。作者在i7-13700K的CPU下测试,跳过前后的时间差大约为0.1ms。
第二个解决方案下,倘若不结合第一个解决方案,因为无法保证锁前和锁后的时间是否完全不一的情况,因此需要保留sleep过程,然而可以计算的是,若并发量达到了10000,仅是获取种子的最长等待时间就可能达到1s,加上一般还会有其它业务逻辑,这难以保证高效的需求。

上述方案均存在隐含的问题,那么换一个解决思路,既然随机数池是固定长度,不能使用sleep或锁,为什么不让程序只保留一个随机实例并当获取次数达到一定值时更新随机实例呢?我们使用下面这个简单例子来测试可行性:

import "math/rand"t1 := time.Now().UnixNano()
fmt.Println(t1)
r := rand.New(rand.NewSource(t1))
for i := 0; i < 600; i++ {r.Int31n(100)
}
t2 := time.Now().UnixNano()
fmt.Println(t2)

上述代码中,我们通过t1时间生成一个随机实例,并让该实例随机600次,再获取一次时间t2。执行的结果会发现更大的问题,t1时间和t2时间是完全相同的!此时如果用t2作为种子,那么下一次随机的600次会和t1时间的一模一样。假如我们让不同实例之间做延时或锁,那么问题又会回到前面解决方案中同样的问题。

因此,可以做出如下的结论。常用的math/rand在基于获取当前时间作为种子的随机时,无法真正地解决重复问题。

三、对crypto/rand随机数的研究:

基于crypto/rand获取随机数,常见的方法是:

import ("crypto/rand""math/big"
)r, _ := rand.Int(rand.Reader, big.NewInt(100))
result := r.Int64()

crypto/rand获取的随机数可以看做是真随机,其原因是它获取种子的来源。简单来说,在各类系统中,均存在一个特定的文件专门用于存储真随机值,这些值的来源是硬件在电路上产生的各种信息、热噪声等,由于它们是不可控的且无法预测的,因此可看做是产生了真正的随机数。

在上述代码中,rand.Reader就会从系统中存储真随机数的文件或者通过调用系统级别的API获取一个真随机数。对于系统而言,当该随机数被获取后就会被销毁,不会二次使用。

然而,crypto/rand有一个缺点,它的获取效率非常低,这是因为它需要访问系统文件或者调用系统API,会产生不小的耗时。并且,每次从系统获取的随机值都会是一次性的,无法第二次使用。低效的获取方式显然并不能完全满足高频次和高并发下保持程序高效的需求。

三、最后的解决方案:

结合前面的相关内容,可以找到一个取长补短的方式,得到一个解决方案:利用crypto/rand的rand.Reader获取一个随机数,使用这个随机数作为math/rand的种子获得一个随机数池进行取值,当取值达到一定次数后,再获取一个新的种子生成新的实例。

通过rand.Reader获取一个种子数的方法如下:

import ("crypto/rand""encoding/binary"
)var seed int64
binary.Read(rand.Reader, binary.BigEndian, &seed)

使用上述方法,即便高频次或高并发地获取种子数,也能保证每次得到的种子都是不一样的数值,这样就能避免因种子相同导致的重复问题了。

最终的解决方案确定,结合后的代码如下:

import (crand "crypto/rand""encoding/binary"mrand "math/rand"
)var seed int64
binary.Read(crand.Reader, binary.BigEndian, &seed)
source := mrand.NewSource(seed)
r := mrand.New(source)
result := r.Int31n(100)

在上述代码中,仍然需要注意一个重要问题,就是crand.Reader的耗时较长,在对取随机的过程进行封装时,需要进行进一步的改进:
1、增加一个统计阈值,每次随机后+1,并当次数达到一定值时更新随机实例;
2、对更新随机实例过程上锁,避免并发时出现重复更新;
经过改进后,可自行测试其速度。作者参与过的项目实践中,在统计阈值为300且较高的获取次数下,平均每次获取一个随机数的耗时约25~30ns。不仅满足了接近真随机的需求,并且满足了高效的需求。

四、其它参考文章:

下面是在探索该问题的过程中找到的较好的相关知识文章,在此供大家阅读参考:
知乎文章:随机数与密钥派生
CSDN文章:Go语言 crypto/rand 随机数生成方法研究草稿

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/194495.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(四)基于高尔夫优化算法GOA求解无人机三维路径规划研究(MATLAB代码)

一、无人机模型简介&#xff1a; 单个无人机三维路径规划问题及其建模_IT猿手的博客-CSDN博客 参考文献&#xff1a; [1]胡观凯,钟建华,李永正,黎万洪.基于IPSO-GA算法的无人机三维路径规划[J].现代电子技术,2023,46(07):115-120 二、高尔夫优化算法GOA简介 高尔夫优化算法…

461. 汉明距离

461. 汉明距离 不难 class Solution {public int hammingDistance(int x, int y) {int res 0;while(x!0 || y!0) {if((x&1) ! (y&1))res ;x>>1;y>>1;}return res;} }

【LeeCode】160.链表相交

给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果后&#…

rman 工作常用

RMAN> backup archivelog all delete input; backup as compressed backupset backupset all;###备份片的再压缩 all, or until time sysdate -7 RMAN> run { backup archivelog all; delete archivelog until time sysdate -1 like /u01/archives/TEST/%; delete…

MediaPipe 3D姿态估计简明教程

姿势检测是更多地了解视频和图像中人体的重要一步。 我们现有的模型支持 2D 姿态估计已经有一段时间了&#xff0c;你们中的许多人可能已经尝试过。 今天&#xff0c;我们在 TF.js 姿势检测 API 中推出第一个 3D 模型。 3D 姿态估计为健身、医疗、动作捕捉等应用开辟了新的设计…

简易FIR数字滤波器

摘 要 随着科学技术的飞速发展&#xff0c;数字信号处理技术广泛的应用在各种领域中&#xff0c;而数字滤波技术在数字信号处理中占有极其重要的地位。传统的模拟滤波器已经很难满足工业生产的需求&#xff0c;所以&#xff0c;对数字滤波器的研究具有很重要的实际意义。相对于…

分享一个用C#写的Aspose.Pdf生成pdf的工具类

公共类 公共属性 标题级别 对应的标题样式 汉字与数字标题对应关系 using Aspose.Words; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; using System.Text.RegularExpressions;namespace Common.Bo {public class CommonStyl…

WordPress外贸站优化工具,WordPress外贸SEO优化方法

WordPress外贸站是跨国企业拓展市场、提升品牌知名度的理想选择。然而&#xff0c;如何通过SEO优化、原创文章生成以及留心站点优化的事项&#xff0c;成为众多站长关注的焦点。 SEO&#xff0c;即搜索引擎优化&#xff0c;是提高网站在搜索引擎结果中排名的关键。首先&#x…

云计算:数字时代的引擎

引言 云计算&#xff0c;作为现代信息技术领域的一项革命性创新&#xff0c;已经深刻改变了我们处理数据和应用的方式。它已经从仅仅是一个概念演变为一个全球范围内广泛应用的技术。云计算为个人、企业和政府机构提供了强大的计算能力、灵活性和可扩展性&#xff0c;同时降低…

JavaScript递归

前端面试大全JavaScript递归 &#x1f31f;经典真题 &#x1f31f;递归 &#x1f31f;真题解答 &#x1f31f;总结 &#x1f31f;经典真题 使用递归完成 1 到 100 的累加 &#x1f31f;递归 A recursive method is a method that calls itself. 递归调用是一种特殊的调…

数据集的标签文件【无标题】

这里写目录标题 读取数据集 .json是用来存储简单的数据结构和对象的文件。json是一种轻量级的数据交换格式 给了 数据集的标签文件&#xff0c; 读取数据集 .json的本质是字典 读取Json文件&#xff1a;json.load() import json with open(train.json,r,encodingutf-8) as f…

[最优化理论] 梯度下降法 + 精确线搜索(单峰区间搜索 + 黄金分割)C++ 代码

这是我的课程作业&#xff0c;用了 Eigen 库&#xff0c;最后的输出是 latex 的表格的一部分 具体内容就是 梯度下降法 精确线搜索&#xff08;单峰区间搜索 黄金分割&#xff09; 从书本的 Matlab 代码转译过来的其实&#xff0c;所以应该是一看就懂了 这里定义了两个测试…

使用pytorch从零开始实现迷你GPT

生成式建模知识回顾: [1] 生成式建模概述 [2] Transformer I&#xff0c;Transformer II [3] 变分自编码器 [4] 生成对抗网络&#xff0c;高级生成对抗网络 I&#xff0c;高级生成对抗网络 II [5] 自回归模型 [6] 归一化流模型 [7] 基于能量的模型 [8] 扩散模型 I, 扩散模型 II…

wordpress建站优化加速教程-Redis加速

这篇文章适合宝塔面板&#xff0c;在宝塔面板安装 Redis 实现网站加速&#xff08; Redis是一个高性能的key-value数据库(PHP连接redis&#xff0c;需PHP设置中安装redis扩展) &#xff09;。对在word press网站有着明显的加速效果。关于Redis具体说明请自己百度&#xff0c;…

编程中常见的技术难题有哪些?By AI

编程对于现代社会发展的重要性 编程&#xff0c;即按照特定的规则和逻辑&#xff0c;为计算机设计指令的过程&#xff0c;已经深深地融入现代社会的各个角落。它对人们的生活、工作和科技发展产生了深远的影响。 首先&#xff0c;编程改变了人们的生活方式。如今&#xff0c;…

Qt 如何操作SQLite3数据库?数据库创建和表格的增删改查?

# 前言 项目源码下载 https://gitcode.com/m0_45463480/QSQLite3/tree/main # 第一步 项目配置 平台:windows10 Qt版本:Qt 5.14.2 在.pro添加 QT += sql 需要的头文件 #include <QSqlDatabase>#include <QSqlError>#include <QSqlQuery>#include &…

SCAU:数字字符序列2

数字字符序列 Time Limit:1000MS Memory Limit:65535K 题型: 填空题 语言: G;GCC;VC 描述 有一个数字字符序列&#xff0c;它是由平方数1&#xff0c;4&#xff0c;9&#xff0c;16&#xff0c;25&#xff0c;36&#xff0c;49&#xff0c;64&#xff0c;81&#xff0c;…

Pandas进阶:拼接 concat 使用方法

1.处理索引和轴 假设我们有2个关于考试成绩的数据集。 df1 pd.DataFrame&#xff08;{ name&#xff1a;[A&#xff0c;B&#xff0c;C&#xff0c;D]&#xff0c;math&#xff1a;[60,89,82,70]&#xff0c;physics&#xff1a;[66&#xff0c; 95,83,66]&#xff0c;chemi…

SCAU:前一个和后一个字符

前一个和后一个字符 Time Limit:1000MS Memory Limit:65535K 题型: 编程题 语言: G;GCC;VC 描述 编写程序&#xff0c;输入一个数字字符&#xff0c;输出其前一个和后一个的数字字符&#xff0c;如果输入的是0前一个输出 “first”&#xff0c;9后一个则输出“last”&…

springBoot整合task

springBoot整合task 文章目录 springBoot整合task开开关设置任务&#xff0c;并设置执行周期定时任务的相关配置 开开关 设置任务&#xff0c;并设置执行周期 Component public class MyBean {Scheduled(cron "0/1 * * * * ?")public void print(){System.out.prin…