2026/1/16 6:10:00
网站建设
项目流程
电子商务网站建设的规划,织梦网站2个模型,陕西省建设厅网站证件查询,产品推广方式及推广计划很多人第一反应是#xff1a;“不就随机查几条数据吗#xff1f;MySQL 既然提供了这个内置函数#xff0c;为什么不让用#xff1f;”事实上#xff0c;这可能是 MySQL 里最“坑爹”的内置函数之一。在数据量只有几百条时#xff0c;它是省时省力的小甜甜#xff1b;一旦…很多人第一反应是“不就随机查几条数据吗MySQL 既然提供了这个内置函数为什么不让用”事实上这可能是 MySQL 里最“坑爹”的内置函数之一。在数据量只有几百条时它是省时省力的小甜甜一旦数据量突破十万级它立马变身吸干 CPU 的“牛夫人”分分钟让你的数据库报警。今天我们就来扒一扒为什么这个函数是性能杀手以及在海量数据下我们该如何优雅且高性能地实现“随机推荐”功能。案发现场一条 SQL 引发的血案那个让 DBA 暴跳如雷的 SQL 长这样-- 看起来人畜无害实则剧毒无比 SELECT * FROM product ORDER BY RAND() LIMIT 3;如果你的商品表只有几百条数据怎么玩都行。但当数据量达到几万、几十万甚至上百万时这条 SQL 就是一颗定时炸弹。为什么它这么慢我在测试环境重现了一下顺手敲了个EXPLAIN。好家伙Extra字段里赫然写着Using temporary; Using filesort这简直是 MySQL 性能杀手界的“卧龙凤雏”ORDER BY RAND()的执行流程大致是这样的全表扫描MySQL 需要为每一行数据生成一个随机值。创建临时表把查询列和对应的随机值塞进临时表如果内存不够还会用到磁盘临时表。全局排序对临时表里的随机值进行排序。取出前几条这就好比你要从一袋米里随机挑 3 粒却先把整袋米倒出来给每粒米编个号排个序再挑前 3 个。这不崩谁崩深入剖析五种高性能替代方案既然ORDER BY RAND()不能用那怎么实现“随机推荐”其实思路很简单把“计算随机”的压力从 Database 转移到 Application应用层或者减少数据库的扫描行数。方案一应用层随机法Application Shuffle适用场景数据量不大例如 10万内存不值钱。核心思想既然数据库随机排序慢那我把 ID 全拿出来在 Java 代码里洗牌行不行代码实现// 1. 查出所有商品ID只查ID速度飞快 // SQL: SELECT id FROM product; ListInteger allProductIds productMapper.selectAllIds(); // 2. 利用 Java 的 Collections 工具类进行洗牌 Collections.shuffle(allProductIds); // 3. 截取前3个 ListInteger randomIds allProductIds.subList(0, 3); // 4. 回表批量查询详情 // SQL: SELECT * FROM product WHERE id IN (..., ..., ...); ListProduct results productMapper.selectByIds(randomIds);优缺点点评优点真・随机由于用了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; ListProduct results productMapper.selectByOffset(offset, 3);优缺点点评优点性能极佳大部分情况下只需要扫描offset 3行 count值可以放缓存中定期更新。缺点伪随机你取出来的 3 条数据是物理上连续的。比如正好取出了“iPhone 13, iPhone 14, iPhone 15”看起来不够随机。深分页问题如果随机到的offset很大比如 900万LIMIT 9000000, 3的性能也会下降因为 MySQL 要先扫过前 900 万行扔掉。方案三多次查询法Multiple Queries适用场景数据量大且要求高质量随机。核心思想既然方案二取出的数据是连续的那我多随机几次每次取 1 条拼凑出 3 条不就行了代码实现// 1. 获取总数 int total productMapper.count(); // 2. 生成3个不重复的随机下标Java 8 Stream 写法 ListInteger randomOffsets new Random() .ints(0, total) // 生成无限流 .distinct() // 去重 .limit(3) // 截取前3个 .boxed() .collect(Collectors.toList()); // 3. 循环查询或者拼接 SQL 用 UNION ALL ListProduct result new ArrayList(); for (Integer offset : randomOffsets) { // SQL: SELECT * FROM product LIMIT #{offset}, 1 result.add(productMapper.selectByLimit(offset, 1)); }其实这就是MySQL 45讲里推荐的优化思路。相比于方案二它打散了连续性。优缺点点评优点既避免了全表排序又保证了较好的随机性。缺点要与数据库交互多次N 次查询。不过对于高并发应用一般都是多次查询 缓存这点开销完全可以接受。方案四主键范围法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. 执行查询 ListProduct products productMapper.selectGtId(randomId, 3);SQL 实现SELECT * FROM product WHERE id #{randomId} LIMIT 3;优缺点点评优点速度快到飞起复杂度直接降为 $O(\log N)$主键查找完全没有LIMIT深分页的性能衰减。缺点非常挑食它假设 ID 是连续的。如果你的商品表里因为删删改改导致 ID 中间空洞很大这类 SQL 会导致分布严重不均空洞前的那条数据被选中的概率会暴增甚至可能取不到数据。方案五Redis 预处理法Redis Set适用场景高并发、高性能、大数据量标准的互联网大厂打法。核心思想既然 MySQL 不擅长做随机那就别难为它了交给最擅长的 Redis。代码实现// 1. 初始化只需做一次把所有商品ID丢进 Redis Set // Redis Key: all_product_ids // 2. 利用 Redis 原生命令随机获取 ID // 命令SRANDMEMBER key count // 时间复杂度O(N)N是你取的数量极快 ListInteger randomIds redisTemplate.opsForSet().randomMembers(all_product_ids, 3); // 3. 回表 MySQL 查详情这里全是主键查询性能无压力 ListProduct products productMapper.selectByIds(randomIds);优缺点点评优点天花板级别的性能。无论你有多少数据Redis 都基本能在几毫秒内吐出随机 ID。缺点架构变复杂了。你需要维护 Redis 和 MySQL 的数据同步也就是经典的缓存一致性问题。最终总结选型指南那这几种方案怎么选你的场景推荐方案理由数据量 10W方案一应用层 Shuffle开发最快逻辑最简单随机性最完美。数据量 10WID连续方案四索引跳跃既不用维护缓存又能享受极致性能。数据量 10W允许连续方案二Limit Offset性能不错通用性强是个老实人。数据量 10W要求打散方案三多次查询在性能和随机性之间找到了平衡点。高并发 / 追求极致方案五Redis Set工业界标准答案虽然稍微麻烦点但真香。想被辞退ORDER BY RAND()只要你敢用P0 故障随时带回家。最后多嘴一句如果你的业务可以接受“伪随机”比如每个人看到的随机列表在 1 小时内是一样的强烈建议把算好的随机结果丢进 Redis。毕竟最好的 SQL 优化就是不执行 SQL。别让你写的代码成为深夜报警的罪魁祸首。