Java中随机数陷阱SecureRandom的问题解决
背景
一个看似简单的小说推荐功能,在本地开发和云服务器直接部署时都运行良好,但一旦迁移到 Docker 容器环境,接口响应时间从毫秒级飙升到几十秒甚至超时,最终导致后端服务假死。只有重启容器才能恢复。
经过系列排查,找到罪魁祸首如下:
Random rand = SecureRandom.getInstanceStrong();
问题现象
本地 Windows/Mac 开发环境:推荐接口响应 < 10ms
云服务器直接部署(CentOS 8):推荐接口响应 < 20ms
Docker 容器部署(同一台 CentOS 8):推荐接口响应 5s~60s+,高并发时线程池耗尽
错误日志
2026-06-30 18:35:18.123 ERROR [http-nio-9091-exec-42]
org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/api]
Servlet.service() for servlet [dispatcherServlet] threw exceptionjava.lang.IllegalStateException: Thread blocked waiting for entropy
at java.base/java.security.SecureRandom.nextBytes(SecureRandom.java)
at org.example.chyznovel.service.impl.BookServiceImpl.listRecBooks(BookServiceImpl.java:202)
浏览器端表现
- OPTIONS 预检请求返回 504 Gateway Time-out
- 实际 POST 请求偶尔成功,但大部分超时
- Nginx 日志显示大量 upstream timeout
原因分析
1. SecureRandom.getInstanceStrong() 的工作原理
SecureRandom.getInstanceStrong() 是 Java 提供的密码学安全随机数生成器,它会:
- Linux 系统:读取 /dev/random 设备文件
- Windows 系统:调用 CryptoAPI
- macOS 系统:读取 /dev/urandom
关键问题在于:/dev/random 是阻塞式的真随机数源。
2. 熵池(Entropy Pool)机制
Linux 内核维护一个"熵池",收集系统中的各种"噪音"作为随机数种子:
- 键盘敲击时间间隔
- 鼠标移动轨迹
- 硬盘读写延迟
- 网络包到达时间
- 中断事件
当应用程序从 /dev/random 读取数据时:
- 熵池充足:立即返回随机数
- 熵池不足:阻塞等待,直到收集到足够的熵
查看当前熵池大小:
cat /proc/sys/kernel/random/entropy_avail # 正常值:> 1000 # 危险值:< 100(此时读取 /dev/random 会阻塞)
3. 为什么容器环境特别容易触发?
| 环境 | 熵源丰富度 | 是否容易阻塞 |
|---|---|---|
| 本地开发机 | 丰富(有鼠标、键盘、GUI) | 否 |
| 物理服务器 | 较丰富(有硬盘、网络、中断) | 偶尔 |
| Docker 容器 | 贫乏(隔离环境,无外设) | 极易 |
容器的熵池问题:
- 隔离性导致熵源减少:容器内没有鼠标、键盘等交互设备
- 共享宿主机熵池:多个容器竞争同一个宿主机的熵资源
- 启动初期熵池为空:容器刚启动时熵池几乎为零,需要时间积累
- 高并发放大问题:每个线程调用 getInstanceStrong() 都会消耗熵,快速耗尽池子
4. 为什么本地和直接部署没问题?
- 本地开发:你的电脑有鼠标、键盘、浏览器等大量源,熵池始终充足
- 云服务器直接部署:虽然没有 GUI,但有网络流量、磁盘 I/O、定时中断等熵源,基本够用
- 容器部署:熵源被大幅削减,加上多容器竞争,极易触发阻塞
解决方案
方案一:改用 ThreadLocalRandom(推荐)
适用场景:推荐算法、游戏逻辑、A/B 测试等不需要密码学安全的场景
方案二:使用非阻塞的 SecureRandom
适用场景:必须用强随机数,但不能接受阻塞
方案三:增加容器熵源(治标不治本)
如果确实需要用 SecureRandom.getInstanceStrong(),可以优化容器熵池:
1. 安装 haveged(熵守护进程)
# Dockerfile
FROM openjdk:21-slim
RUN apt-get update && \
apt-get install -y haveged && \
apt-get clean
CMD ["haveged", "-w", "1024", "-v", "1", "--Foreground"]
# docker-compose.yml
services:
app:
image: myapp
cap_add:
- SYS_ADMIN # haveged 需要特权2. 挂载宿主机的 /dev/random
# docker-compose.yml
services:
app:
volumes:
- /dev/random:/dev/random
- /dev/urandom:/dev/urandom什么时候必须用 SecureRandom?
必须用的场景(密码学安全要求):
- 生成加密密钥(AES、RSA)
- 生成 JWT Token 签名密钥
- 生成密码盐值(salt)
- 生成 CSRF Token
- 生成一次性验证码(防止暴力破解)
- 彩票/赌 博系统开奖(法律要求不可预测)
不需要用的场景(伪随机即可):
- 推荐算法随机排序
- 游戏中的随机掉落
- A/B 测试分组
- 负载均衡随机选择
- 模拟数据生成
- 任何"即使被猜到也没关系"的场景
随机数生成器选型指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 密钥 / Token 生成 | new SecureRandom() | 需要强随机性,但频率低,阻塞可接受 |
| 验证码生成 | new SecureRandom() | 防暴 力 破 解,需要不可预测性 |
| 推荐 / 游戏 / A-B 测试 | ThreadLocalRandom.current() | 高频调用,性能优先,伪随机够用 |
| SSL / TLS 握手 | new SecureRandom() | JDK 内部已优化,无需手动干预 |
| ⚠️ 绝对不能用(除非有特殊强随机需求且能接受阻塞) | SecureRandom.getInstanceStrong() | 可能严重阻塞,导致应用超时或线程池耗尽 |
- 在 Docker 容器等低熵环境中,即使你用了 new SecureRandom(),也建议 配置 -Djava.security.egd=file:/dev/urandom,避免因熵池不足导致阻塞。
- 如果你用的是 物理服务器 且业务对性能极度敏感(如高并发网关),对于非安全类随机(如路由分发、采样),优先用 ThreadLocalRandom。
- SecureRandom.getInstanceStrong() 在容器环境几乎等于 自 杀式阻塞,除非你明确知道自己在做什么,否则坚决避开。(以后再也不乱抄网上的了嘤嘤嘤!)
为什么 Java 不默认用非阻塞的?
这是一个历史遗留问题:
- 安全性优先:Java 设计者认为"宁可慢,不能不安全"
- 向后兼容:改变默认行为可能影响现有应用
- 开发者责任:框架提供工具,具体选型由开发者决定
其他语言的类似陷阱
- Python:os.urandom() vs random.SystemRandom()
- Node.js:crypto.randomBytes()(异步非阻塞)vs crypto.pseudoRandomBytes()
- Go:crypto/rand(阻塞)vs math/rand(非阻塞)
通用原则:区分"密码学安全随机数"和"普通伪随机数"的使用场景。
到此这篇关于Java中随机数陷阱SecureRandom的问题解决的文章就介绍到这了,更多相关Java SecureRandom内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Eclipse 2020-06 汉化包安装步骤详解(附汉化包+安装教程)
这篇文章主要介绍了Eclipse 2020-06 汉化包安装步骤(附汉化包+安装教程),本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-08-08
springboot整合rabbitmq实现订单超时取消案例分析
本文介绍了如何使用SpringBoot和RabbitMQ实现订单超时取消功能,通过配置TTL队列和死信交换机,可以管理订单的超时逻辑,实际应用中,可以通过数据库标记订单状态或手动确认机制来防止订单被错误取消2025-01-01
java8 stream 操作map根据key或者value排序的实现
这篇文章主要介绍了java8 stream 操作map根据key或者value排序的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2019-09-09
JDK1.7 Paths,Files类实现文件夹的复制与删除的实例
下面小编就为大家分享一篇JDK1.7 Paths,Files类实现文件夹的复制与删除的实例,具有很好的参考价值,希望对大家有所帮助。以前跟随小编过来看看吧2017-11-11
Spring中@RabbitHandler和@RabbitListener的区别详析
@RabbitHandler是用于处理消息的方法注解,它与@RabbitListener注解一起使用,这篇文章主要给大家介绍了关于Spring中@RabbitHandler和@RabbitListener区别的相关资料,需要的朋友可以参考下2024-02-02
spring boot实现图片上传到后台的功能(浏览器可直接访问)
这篇文章主要介绍了spring boot实现图片上传到后台的功能(浏览器可直接访问),需要的朋友可以参考下2022-04-04


最新评论