优点:真・随机,由于用了 Collections.shuffle,随机分布非常均匀;逻辑简单粗暴。
缺点:太占内存。如果表里有 1000 万条 ID,全拉到内存里,JVM 直接 OOM 教做人。
避坑:一定要给 ID 列表加缓存(Redis 或本地缓存),别每次请求都去查全量 ID,那跟直接攻击数据库没区别。
方案二:Limit 偏移法(Limit Offset)
适用场景:数据量大(百万级以上),对随机性要求没那么严苛。
核心思想:给所有数据编个号,随机生成一个“偏移量”,直接跳到那里去拿。
代码实现
// 1. 先查询总数(可以走缓存)
// SQL: SELECT COUNT(*) FROM product;
int totalCount = productMapper.count();
// 2. 随机生成一个偏移量
// 注意:totalCount - 3 是为了防止 limit 越界,确保能取够3条
int offset = new Random().nextInt(totalCount - 3);
// 3. 直接利用 LIMIT 偏移量查询
// SQL: SELECT * FROM product LIMIT #{offset}, 3;
List<Product> results = productMapper.selectByOffset(offset, 3);
优点:性能极佳!大部分情况下只需要扫描 offset + 3 行 ,count值可以放缓存中,定期更新。
缺点:
1.伪随机:你取出来的 3 条数据是物理上连续的。比如正好取出了“iPhone 13, iPhone 14, iPhone 15”,看起来不够随机。
2.深分页问题:如果随机到的 offset 很大(比如 900万),LIMIT 9000000, 3 的性能也会下降,因为 MySQL 要先扫过前 900 万行扔掉。
方案三:多次查询法(Multiple Queries)
适用场景:数据量大,且要求高质量随机。
核心思想:既然方案二取出的数据是连续的,那我多随机几次,每次取 1 条,拼凑出 3 条不就行了?
代码实现
// 1. 获取总数
int total = productMapper.count();
// 2. 生成3个不重复的随机下标(Java 8 Stream 写法)
List<Integer> randomOffsets = new Random()
.ints(0, total) // 生成无限流
.distinct() // 去重
.limit(3) // 截取前3个
.boxed()
.collect(Collectors.toList());
// 3. 循环查询(或者拼接 SQL 用 UNION ALL)
List<Product> result = new ArrayList<>();
for (Integer offset : randomOffsets) {
// SQL: SELECT * FROM product LIMIT #{offset}, 1
result.add(productMapper.selectByLimit(offset, 1));
}
其实这就是 MySQL 45讲 里推荐的优化思路。相比于方案二,它打散了连续性。
优缺点点评
方案四:主键范围法(Index Random)
适用场景:ID 必须这是连续的(或空洞很少),追求极致性能。
核心思想:既然 LIMIT N, M 越往后越慢,那我直接算出随机 ID,用主键索引“跳”过去不就完事了?
代码实现
Java 逻辑处理:
// 1. 获取 ID 范围(minId 和 maxId)
// SQL: SELECT MIN(id), MAX(id) FROM product;
long minId = productMapper.selectMinId();
long maxId = productMapper.selectMaxId();
// 2. 计算随机起点
// 注意:maxId - minId - 3 是为了保证起点的 id 后面至少还有 3 条数据(假设 ID 连续)
// 如果 ID 极其稀疏,这个范围可能需要预留更大
long range = maxId - minId - 3;
long randomId = minId + (long)(Math.random() * range);
// 3. 执行查询
List<Product> products = productMapper.selectGtId(randomId, 3);
SQL 实现:
SELECT * FROM product
WHERE id >= #{randomId}
LIMIT 3;
优缺点点评
方案五:Redis 预处理法(Redis Set)
适用场景:高并发、高性能、大数据量,标准的互联网大厂打法。
核心思想:既然 MySQL 不擅长做随机,那就别难为它了,交给最擅长的 Redis。
代码实现
// 1. 初始化(只需做一次):把所有商品ID丢进 Redis Set
// Redis Key: "all_product_ids"
// 2. 利用 Redis 原生命令随机获取 ID
// 命令:SRANDMEMBER key count
// 时间复杂度:O(N),N是你取的数量,极快
List<Integer> randomIds = redisTemplate.opsForSet().randomMembers("all_product_ids", 3);
// 3. 回表 MySQL 查详情(这里全是主键查询,性能无压力)
List<Product> products = productMapper.selectByIds(randomIds);
优缺点点评
最终总结:选型指南
那这几种方案怎么选?

最后多嘴一句: 如果你的业务可以接受“伪随机”(比如每个人看到的随机列表在 1 小时内是一样的),强烈建议把算好的随机结果丢进 Redis。毕竟,最好的 SQL 优化就是不执行 SQL。
别让你写的代码,成为深夜报警的罪魁祸首。
参考文章:原文链接