NestJS+TypeORM实现随机分页滚动加载方案咨询
NestJS + TypeORM 实现用户专属随机滚动加载方案
针对你的需求,这里提供几个可行的实现思路,兼顾大型表性能和用户专属随机效果:
方案一:基于用户标识的固定随机种子排序
利用用户唯一标识(如user_id、session_id)生成固定随机种子,让同一个用户的随机排序序列保持连贯,不同用户的序列完全独立,同时避免skip+take的性能问题。
实现代码
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as crypto from 'crypto'; import { Product } from './product.entity'; @Injectable() export class ProductService { constructor( @InjectRepository(Product) private productRepository: Repository<Product>, ) {} async findAll( userIdentifier: string, // 用户唯一标识:sessionId或userId take: number = 20, lastRandomValue?: number, // 滚动加载时传入上一页最后一条的随机值 ) { // 将用户标识转为数字种子(MD5哈希取前8位转16进制) const seed = parseInt( crypto.createHash('md5').update(userIdentifier).digest('hex').slice(0, 8), 16, ); const queryBuilder = this.productRepository .createQueryBuilder('product') .select(['product.*', `RAND(${seed}) AS random_val`]) .orderBy('random_val', 'DESC') .take(take); // 滚动加载时,筛选出随机值小于上一页最后一个值的记录 if (lastRandomValue) { queryBuilder.where('RAND(:seed) < :lastVal', { seed, lastVal: lastRandomValue }); } const rawResult = await queryBuilder.getRawMany(); return { data: rawResult.map(item => { // 移除返回结果中的随机值字段 const { random_val, ...product } = item; return product; }), lastRandomValue: rawResult.length ? rawResult[rawResult.length - 1].random_val : null, }; } }
方案特点
- 每个用户的随机序列固定,滚动加载不会出现重复/遗漏
- 不同用户的随机序列完全独立,满足“每个用户每页看到不同商品”的需求
- 无需额外缓存,直接通过MySQL的
RAND(seed)实现,性能优于内存打乱数组 - 若需要用户每次登录都看到全新序列,可将
userIdentifier替换为user_id + 登录时间戳组合
方案二:基于Redis的用户会话随机ID缓存
给每个用户会话预存一批随机排序的商品ID,滚动加载时直接从缓存中取ID查询详情,适合超大型商品表(避免RAND()函数的性能损耗)。
实现代码(伪代码)
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { RedisService } from '../redis/redis.service'; import { Product } from './product.entity'; @Injectable() export class ProductService { private readonly CACHE_KEY_PREFIX = 'user_product_random:'; private readonly PRE_FETCH_COUNT = 200; // 预取ID数量 constructor( @InjectRepository(Product) private productRepository: Repository<Product>, private redisService: RedisService, ) {} async findAll(sessionId: string, take: number = 20) { const cacheKey = `${this.CACHE_KEY_PREFIX}${sessionId}`; let cachedIds = await this.redisService.lrange(cacheKey, 0, -1); // 缓存为空时,预取一批随机ID存入Redis if (cachedIds.length === 0) { const randomIds = await this.productRepository .createQueryBuilder('product') .select('product.id') .orderBy('RAND()', 'DESC') .take(this.PRE_FETCH_COUNT) .getRawMany(); cachedIds = randomIds.map(item => item.id.toString()); await this.redisService.rpush(cacheKey, ...cachedIds); await this.redisService.expire(cacheKey, 3600); // 1小时后过期 } // 取出当前页需要的ID const pageIds = cachedIds.splice(0, take); // 更新缓存,移除已取走的ID await this.redisService.ltrim(cacheKey, take, -1); // 根据ID批量查询商品详情 return this.productRepository.findByIds(pageIds); } }
方案特点
- 完全规避
RAND()在大数据量下的性能问题 - 每个用户会话的随机序列独立,滚动加载体验流畅
- 可通过调整
PRE_FETCH_COUNT和缓存过期时间平衡性能与内存占用 - 适合用户量适中、商品更新频率不高的场景
注意事项
- 避免使用
skip+take配合RAND():skip在大型表中会导致全表扫描,性能极差,且RAND()每次执行结果不同,会出现分页重复或遗漏的问题。 - 优先选择游标式滚动加载:用
lastRandomValue或lastId作为游标,替代skip,大幅提升查询性能。 - 性能优化:若商品表数据量极大,可提前生成每日随机排序的商品ID视图,或分表存储随机序列,进一步降低实时查询压力。
内容的提问来源于stack exchange,提问作者pdh28907
相关产品推荐
相关产品推荐

