[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.mfbz.cn/a/214886.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

04-数据库操作对象Statement对象和PreparedStatement对象的区别,SQL注入的优缺点

Statement对象和查询结果集 Statement对象相关的方法 Connection接口中获取数据库操作对象Statement对象的方法 方法名功能Statement createStatement()创建Statement对象 Statement对象执行增删改查的SQL语句(不含占位符"?")的方法,JDBC中的SQL语句不需要提供分…

基于 ESP32 的带触摸显示屏的 RFID 读取器

如何设计一款基于 ESP32 且具有 ILI9341 触摸屏显示屏且适合壁挂式安装的美观 RFID 读取器。 本项目中用到的东西 硬件组件 ESP32 开发套件 C 1 AZ-Touch ESP 套件 1 RFID-RC522 IC卡读写器 1 ​编辑 电线、绕包线 1 详细设计流程 …

机器学习 - 导论

简单了解 机器学习关于数据集的概念 、

Autosar COM通信PDU

文章目录 Autosar 中各个PDU所在示意图PDU的分类PDU 和 SDU 的关系I-PDUN-PDUL-PDU相关协议其他参考 Autosar 中各个PDU所在示意图 PDU的分类 在Autosar 中&#xff0c;主要有 I-PDU、N-PDU和 L-PDU 三种。 L-PDU&#xff1a;Data Link Layer PDU&#xff0c;数据链路层PDUN-…

Spring-Boot---项目创建和使用

文章目录 什么是Spring-Boot&#xff1f;Spring-Boot项目的创建使用Idea创建使用网页创建 项目目录介绍项目启动 什么是Spring-Boot&#xff1f; Spring的诞生是为了简化Java程序开发的&#xff1b;而Spring-Boot的诞生是为了简化Spring程序开发的。 Spring-Boot具有很多优点…

知识点滴 - 什么是半透膜和渗透压

半透膜和渗透作用 1748年的一天&#xff0c;法国物理学家诺勒为了改进酒的制作水平&#xff0c;设计了这样一个试验&#xff1a;在一个玻璃圆筒中装满酒精&#xff0c;用猪膀胱封住&#xff0c;然后把圆筒全部浸在水中。当他正要做下一步的工作时&#xff0c;突然发现&#xff…

巧用JAVA自带的API解决日期类问题

文章目录 题目代码优势 题目 特殊日期 代码 import java.util.Scanner; // 1:无需package // 2: 类名必须Main, 不可修改 import java.time.LocalDate; public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);//在此输入您的代…

Java 学习之多态

多态的概念 多态 晚绑定。 所谓多态&#xff0c;就是父类型的引用可以指向子类型的对象&#xff0c;或者接口类型的引用可以指向实现该接口的类的实例。 不要把函数重载理解为多态。因为多态是一种运行期的行为&#xff0c;不是编译期的行为。 多态&#xff1a;父类型的引用可…

数据在内存中的存储(含面试题)

数据在内存中的存储 1. 整数在内存中的存储2. 大小端字节序和字节序判断2.1 什么是大小端&#xff1f;2.2 为什么有大小端?2.3 练习2.3.1 练习12.3.2 练习22.3.3 练习3第一题第二题 2.3.4 练习42.3.5 练习5第一题第二题 2.3.6 练习6 1. 整数在内存中的存储 在讲解操作符的时候…

若依的基本使用

演示使用网址:若依管理系统 网站:RuoYi 若依官方网站 |后台管理系统|权限管理系统|快速开发框架|企业管理系统|开源框架|微服务框架|前后端分离框架|开源后台系统|RuoYi|RuoYi-Vue|RuoYi-Cloud|RuoYi框架|RuoYi开源|RuoYi视频|若依视频|RuoYi开发文档|若依开发文档|Java开源框架…

2023/12/3总结

RabbitMq 消息队列 下载地址RabbitMQ: easy to use, flexible messaging and streaming — RabbitMQ 使用详情RabbitMQ使用教程(超详细)-CSDN博客 实现延迟队列&#xff08;为了实现订单15分钟后修改状态&#xff09; 1 死信队列 当一个队列中的消息满足下列情况之一时&…

基于hadoop下的Kafka分布式安装

简介 Kafka是一种分布式流处理平台&#xff0c;它具有高吞吐量、可扩展性、可靠性、实时性和灵活性等优点。它能够支持每秒数百万条消息的传输&#xff0c;并且可以通过增加节点来增加吞吐量和存储容量。Kafka通过将数据复制到多个节点来实现数据冗余和高可用性&#xff0c;即使…

【拓展】Loguru:更为优雅、简洁的Python 日志管理模块

目录 一、简单介绍 二、安装与简单使用 ​三、常见用法 3.1 显示格式 3.2 写入文件 3.3 json日志 3.4 日志绕接 3.5 并发安全 四、高级用法 4.1 接管标准日志logging 4.2 输出日志到网络服务器 4.2.1 自定义日志服务器 ​4.2.2 第三方库日志服务器 4.3 与pytest结…

RT-Thread 汇编分析启动流程

文章目录 一、汇编指令二、启动文件三、流程图 一、汇编指令 这里介绍即几条最常见实用的汇编指令 LDR R0,[R1]&#xff1a;将R1指定内存地址数据&#xff0c;存储到寄存器R0中。STR R0,[R1,#4]&#xff1a;将寄存器R0中数据存储到寄存器R1加上偏移量4的位置。MOV r0,#0x01&a…

read()之后操作系统都干了什么

首先说明三个参数 file文件 buff从内存中开辟一段缓冲区用来接收读取的数据 size表示这个缓冲区的大小 有关file的参数&#xff1a; 状态&#xff1a;被打开 被关闭权限&#xff1a;可读可写最重要的是inode: 他包含了 文件的元数据(比如文件大小 文件类型 文件在访问前需要加…

Google Earth Engine谷歌地球引擎计算多年中某两个时间点之间遥感数据差值的平均值

本文介绍在谷歌地球引擎GEE中&#xff0c;提取、计算某一种遥感影像产品在连续的多年中&#xff0c;2个不同时相的数据差值的多年平均值&#xff0c;并将计算得到的这一景差值的结果图像导出的方法。 本文是谷歌地球引擎&#xff08;Google Earth Engine&#xff0c;GEE&#x…

[二分查找]LeetCode2040:两个有序数组的第 K 小乘积

本文涉及的基础知识点 二分查找算法合集 题目 给你两个 从小到大排好序 且下标从 0 开始的整数数组 nums1 和 nums2 以及一个整数 k &#xff0c;请你返回第 k &#xff08;从 1 开始编号&#xff09;小的 nums1[i] * nums2[j] 的乘积&#xff0c;其中 0 < i < nums1.…

【Cell Signaling + 神经递质(neurotransmitter) ; 神经肽 】

Neuroscience EndocytosisExcitatory synapse pathwayGlutamatergic synapseInflammatory PainInhibitors of axonal regenerationNeurotrophin signaling pathwaySecreted Extracellular VesiclesSynaptic vesicle cycle

一个完整的手工构建的cuda动态链接库工程 03记

1&#xff0c; 源代码 仅仅是加入了模板函数和对应的 .cuh文件&#xff0c;当前的目录结构如下&#xff1a; icmm/gpu/add.cu #include <stdio.h> #include <cuda_runtime.h>#include "inc/add.cuh"// different name in this level for different type…

java企业财务管理系统springboot+jsp

1、基本内容 &#xff08;1&#xff09;搭建基础环境&#xff0c;下载JDK、开发工具eclipse/idea。 &#xff08;2&#xff09;通过HTML/CSS/JS搭建前端框架。 &#xff08;3&#xff09;下载MySql数据库&#xff0c;设计数据库表&#xff0c;用于存储系统数据。 &#xff08;4…