简介 在实际开发中,经常遇到需要生成随机数的场景,例如,发送短信验证码等。
这里对生成随机数的解决方案做个总结 随机数的生成原理 随机数是通过算法生成的,生成随机数的过程中涉及到几个重要的概念,包括种子、随机数算法、系统熵源。
种子:seed,随机数生成算法的初始输入值,决定了随机数序列的起点,相同的种子会生成完全相同的随机数序列,这使得随机数的生成可重现,用于调试或实验,如果用户没有指定种子,通常使用系统时间(System.nanoTime())作为默认种子 随机数生成算法:通过特定的计算,生成指定范围内的随机数,算法是固定的,不同的是种子,所以如果种子相同,生成的随机数序列就是固定的,统计上满足随机性,但可预测。
常见的随机数生成算法,如线性同余算法。
系统熵源:系统收集的真实随机性来源,如硬件噪声、键盘输入时间、鼠标移动等,例如,Linux系统上,系统熵源的设备文件是 /dev/random,通过系统熵源,可以获取到真正随机的种子,生成不可预测的随机数序列,但是系统熵源需要收集系统熵的信息,可能会阻塞。
三者的关系:种子是伪随机数生成的起点,伪随机数通过算法模拟随机性,依赖种子,熵源为安全随机数提供真正的不可预测性。
为什么伪随机数不安全?
因为算法是公开的,若攻击者知道种子或部分序列,可推算后续随机数。
熵:shang,“无序性”“不确定性”或“信息量”的度量 生成随机数的方法 Random Random类,支持生成整数、随机数、布尔值类型的随机数,非线程安全,适合单线程环境下的基本随机数需求,不适合高并发、安全敏感的场景,随机性可预测 案例:生成0到100之间的随机数
@Test
public void test2() {
Random random = new Random();
int i = random.nextInt(100);
System.out.println("i = " + i);
}
案例2:Math类中提供的工具方法,底层也是调用的Random
// Math.random,生成[0.0, 1.0)之间的double类型的伪随机数,内部调用Random类实现,
// 适合快速生成随机数的场景,不适合高并发,无法指定随机种子,所以无法重现随机序列
@Test
public void test1() {
double random = Math.random();
System.out.println("random = " + random); // 0.24365653692120037
}
ThreadLocalRandom 为每个线程维护独立的种子,适合高并发场景,但仍然是伪随机 案例:生成0到100之间的随机数
@Test
public void test3() {
int i = ThreadLocalRandom.current().nextInt(100);
System.out.println("i = " + i);
}
SecureRandom SecureRandom,基于系统熵源,如 /dev/random,加密强度高,线程安全但性能较低,适用于秘钥、令牌、验证码登安全敏感场景,可以生成不可预测的随机数 案例:
@Test
public void test4() {
SecureRandom secureRandom = new SecureRandom();
byte[] bytes = new byte[128];
secureRandom.nextBytes(bytes);
System.out.println("bytes = " + Arrays.toString(bytes));
}
熵池耗尽问题:SecureRandom是从系统熵源中获取种子,系统熵源可能会阻塞,可能会耗尽,Linux提供了两个熵源: /dev/random:阻塞型,当熵不足时等待新熵(更安全)。
/dev/urandom:非阻塞型,熵不足时用伪随机算法补足(性能更好,多数场景足够安全)。
实战案例 案例1:生成6位的验证码 在方案选型时,选择ThreadLocalRandom,它在高并发的情况下性能更好,虽然没有SecureRandom那么安全,一次生成6位数字,而不是生成6次,每次生成1位随机数,因为过多的随机数序列更加容易被破解,毕竟它是伪随机的。
@Test
public void testMsgCode() {
int limit = 1000_000;
int i = ThreadLocalRandom.current().nextInt(limit);
String str = String.format("%06d", i); // 随机数不足6位时前面补0
System.out.println("str = " + str); // 738210
}
案例2:生成5个0到10之间不会重复的随机数
public class Demo5RandomListEle2 {
// 使用HashSet来去重
@Test
public void test1() {
Set set = new HashSet<>();
Random random = new Random();
while (set.size() < 5) {
set.add(random.nextInt(11));
}
System.out.println("set = " + set);
}
// 使用Collections接口的shuffle函数来做
@Test
public void test2() {
List list = new ArrayList<>();
for (int i = 0; i <= 10; i++) {
list.add(i);
}
Collections.shuffle(list);
List result = list.subList(0, 5);
System.out.println("result = " + result);
}
}
案例3:假如现在有n个元素,n > 5,随机返回其中5个
public class Demo5RandomListEle {
// 使用Collections的shuffle方法,随机返回其中5个
@Test
public void test1() {
List list = Lists.newArrayList("aaa", "bbb", "ccc", "ddd", "eee", "fff");
Collections.shuffle(list);
List strings = list.subList(0, 5);
System.out.println("strings = " + strings);
}
// 使用Random类
@Test
public void test2() {
List list = Lists.newArrayList("aaa", "bbb", "ccc", "ddd", "eee", "fff");
List result = new ArrayList<>(5);
Random random = new Random();
for (int i = 0; i < 5; i++) {
int idx = random.nextInt(list.size());
result.add(list.get(idx));
list.remove(idx);
}
System.out.println("result = " + result);
}
}
UUID UUID,Universally Unique Identifier,通用唯一识别码,一种在分布式系统中独立生成不可重复的ID的手段。
在实际使用中,UUID的重复概率极低,可视为唯一 格式:UUID是一个128比特位的数字,16字节,以32个十六进制字符表示,并用连字符“-”分成5段
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
M表示标本号,1就是第一版,4就是第四版,N表示变体,决定UUID的布局 UUID的版本:UUID的发展过程中经历了多次版本迭代,不同版本使用不同的方式来生成UUID。
版本1:基于时间戳 + mac地址,它的缺点在于会暴漏mac地址,导致隐私问题 版本3:基于md5哈希 + 命名空间,相同输入会产生相同输出 版本4:基于随机数生成,这是最常用的,几乎不会重复 UUID的特点: 唯一性:理论上,UUID几乎不会重复,以第四版UUID为例,128比特位的数据,122位是随机数,共有2的122次方种组合数,这是一个很大的数字,重复概率极低 无中心化:不依赖中央服务器分配,适合分布式系统 无序性:UUID是无序的,不适合做数据库主键索引 固定长度:固定16字节长度 案例:生成UUID
@Test
public void test8() {
UUID uuid = UUID.randomUUID();
System.out.println("uuid = " + uuid.toString()); // c9b43770-0440-46ae-9c0d-8c20d21f97c7
}
