You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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和缓存过期时间平衡性能与内存占用
  • 适合用户量适中、商品更新频率不高的场景

注意事项

  1. 避免使用skip+take配合RAND():skip在大型表中会导致全表扫描,性能极差,且RAND()每次执行结果不同,会出现分页重复或遗漏的问题。
  2. 优先选择游标式滚动加载:用lastRandomValue或lastId作为游标,替代skip,大幅提升查询性能。
  3. 性能优化:若商品表数据量极大,可提前生成每日随机排序的商品ID视图,或分表存储随机序列,进一步降低实时查询压力。

内容的提问来源于stack exchange,提问作者pdh28907

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 21:43:16