面对海量数据随机推荐需求,如何平衡性能与随机性成为关键挑战
在电商平台开发中,我们经常需要实现"随机推荐"功能:从商品库中随机选取指定数量的商品展示给用户。假设商品表(product)有10000条数据,需要随机获取3个不重复的商品。
许多开发者第一反应是使用ORDER BY RAND()实现,但这种方法的性能代价极高,在处理大量数据时几乎不可用。
-- 常见但不推荐的方案
SELECT * FROM product ORDER BY RAND() LIMIT 3;
这条SQL语句的问题在于:
执行计划中会出现"Using temporary"和"Using filesort",这些都是性能杀手。
// 第一步:获取所有商品ID(只需执行一次并可缓存)
List<Integer> productIdList = productMapper.selectAllIds();
// 第二步:应用层随机处理
Collections.shuffle(productIdList);
List<Integer> randomIds = productIdList.subList(0, 3);
// 第三步:回表查询完整信息
List<Product> randomProducts = productMapper.selectByIds(randomIds);
对应的Mapper接口:
public interface ProductMapper {
// 获取所有商品ID
@Select("SELECT id FROM product WHERE status = 1")
List<Integer> selectAllIds();
// 根据ID列表查询商品
@Select("<script>" +
"SELECT id, name, price, image_url FROM product " +
"WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<Product> selectByIds(@Param("ids") List<Integer> ids);
}
| 数据量 | 平均响应时间 | QPS |
|---|---|---|
| 1万条 | 15ms | 66 |
| 10万条 | 18ms | 55 |
| 100万条 | 25ms | 40 |
// 获取商品总数(可缓存)
int totalCount = productMapper.selectCount();
// 计算随机偏移量(确保不越界)
Random random = new Random();
int offset = random.nextInt(Math.max(1, totalCount - 3));
// 执行分页查询
List<Product> randomProducts = productMapper.selectWithOffset(offset, 3);
对应的Mapper接口:
public interface ProductMapper {
@Select("SELECT COUNT(*) FROM product WHERE status = 1")
int selectCount();
@Select("SELECT id, name, price, image_url FROM product " +
"WHERE status = 1 " +
"LIMIT #{offset}, #{limit}")
List<Product> selectWithOffset(@Param("offset") int offset, @Param("limit") int limit);
}
| 数据量 | 平均响应时间 | QPS |
|---|---|---|
| 1万条 | 8ms | 125 |
| 10万条 | 12ms | 83 |
| 100万条 | 45ms | 22 |
public List<Product> getRandomProducts(int count) {
int totalCount = productMapper.selectCount();
Random random = new Random();
Set<Integer> offsets = new HashSet<>();
// 生成不重复的随机偏移量
while (offsets.size() < count) {
int offset = random.nextInt(totalCount);
offsets.add(offset);
}
// 并行查询提升性能
return offsets.parallelStream()
.map(offset -> productMapper.selectWithOffset(offset, 1))
.flatMap(List::stream)
.collect(Collectors.toList());
}
| 数据量 | 平均响应时间 | QPS |
|---|---|---|
| 1万条 | 25ms | 40 |
| 10万条 | 28ms | 35 |
| 100万条 | 35ms | 28 |
| 方案 | 随机性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| ORDER BY RAND() | 优 | 差 | 简单 | 不推荐用于生产环境 |
| 应用层随机 | 优 | 优 | 中等 | 数据量适中(推荐) |
| LIMIT偏移量 | 中 | 良 | 简单 | 数据量大,随机性要求不高 |
| 多次查询 | 良 | 中 | 复杂 | 数据量大,需要较好随机性 |
@Service
public class ProductRandomService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_IDS_KEY = "product:all_ids";
private static final String PRODUCT_COUNT_KEY = "product:count";
// 带缓存的随机查询
public List<Product> getRandomProductsWithCache(int count) {
// 从缓存获取ID列表
List<Integer> ids = getCachedProductIds();
if (ids.isEmpty()) {
ids = refreshProductIdsCache();
}
Collections.shuffle(ids);
List<Integer> randomIds = ids.subList(0, Math.min(count, ids.size()));
return productMapper.selectByIds(randomIds);
}
// 定时更新缓存
@Scheduled(fixedRate = 300000) // 5分钟更新一次
public void refreshProductIdsCache() {
List<Integer> ids = productMapper.selectAllIds();
redisTemplate.opsForValue().set(PRODUCT_IDS_KEY, ids, 10, TimeUnit.MINUTES);
}
}
// 基于热度的加权随机
public List<Product> getWeightedRandomProducts(int count) {
List<Product> hotProducts = productMapper.selectHotProducts(100); // 取前100个热门商品
// 根据热度权重进行随机选择
double[] weights = hotProducts.stream()
.mapToDouble(p -> p.getHeatWeight())
.toArray();
List<Product> result = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < count; i++) {
double randomValue = random.nextDouble() * Arrays.stream(weights).sum();
double cumulativeWeight = 0;
for (int j = 0; j < weights.length; j++) {
cumulativeWeight += weights[j];
if (randomValue <= cumulativeWeight) {
result.add(hotProducts.get(j));
break;
}
}
}
return result;
}
通过实际测试对比,我们可以得出以下结论:
技术选型建议:对于大多数电商场景,方案一(应用层随机)是最佳选择,既能保证真正的随机性,又具有稳定的高性能表现。
记住:在数据库优化中,有时候将计算逻辑转移到应用层,反而能获得更好的整体性能。