获客项目——Redis位图与RoaringBitmap完整面试指南
大约 16 分钟
获客项目——Redis位图与RoaringBitmap完整面试指南
项目背景介绍
业务场景描述
面试官:请介绍一下你们获客项目中位图计算组件的应用场景?
回答要点:
我们的获客系统面向教育、金融等多行业提供全链路智能获客解决方案。系统需要处理亿级用户的标签管理和人群圈选,核心痛点是:
- 海量用户标签存储:1000+标签 × 亿级用户,传统关系型数据库无法承载
- 复杂标签组合计算:业务需要支持"A且B非C"等复杂逻辑的秒级响应
- 实时人群圈选:营销活动需要实时筛选目标人群,要求毫秒级响应
基于这些需求,我们设计了基于Redis位图和RoaringBitmap的混合架构。
技术选型对比
| 方案 | 存储空间 | 查询性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| MySQL关系表 | 极大 | 秒级-分钟级 | 高 | 小规模精确查询 |
| Redis Set | 大 | 毫秒级 | 中 | 中等规模集合运算 |
| Redis Bitmap | 小 | 毫秒级 | 低 | 大规模稠密数据 |
| RoaringBitmap | 极小 | 毫秒级 | 低 | 大规模稀疏数据 |
一、Redis位图计算组件实现
1.1 核心架构设计
1.2 Java实现代码
/**
* Redis位图计算组件 - 支持亿级用户标签秒级计算
*
* @author 获客项目组
*/
@Component
public class RedisBitmapCalculator {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 为用户打标签 - 批量操作优化
*
* @param tagId 标签ID
* @param userIds 用户ID列表
*/
public void batchSetUserTag(String tagId, List<Long> userIds) {
String bitmapKey = buildTagBitmapKey(tagId);
// 使用Pipeline批量操作,提升性能
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (Long userId : userIds) {
// 将用户ID映射为位图索引
long bitIndex = mapUserIdToBitIndex(userId);
connection.setBit(bitmapKey.getBytes(), bitIndex, true);
}
return null;
}
});
// 设置过期时间,防止内存泄漏
redisTemplate.expire(bitmapKey, Duration.ofDays(30));
}
/**
* 标签交集计算 - 核心圈选逻辑
*
* @param tagIds 标签ID列表
* @return 圈选结果键和人群数量
*/
public CrowdCalculationResult calculateIntersection(List<String> tagIds) {
// 生成结果键,包含时间戳避免冲突
String resultKey = buildResultKey("intersection", tagIds);
// 构建源位图键列表
String[] sourceBitmapKeys = tagIds.stream()
.map(this::buildTagBitmapKey)
.toArray(String[]::new);
// 执行位图AND运算 - Redis原生支持,性能极高
redisTemplate.opsForValue().bitOp(RedisStringCommands.BitOperation.AND,
resultKey, sourceBitmapKeys);
// 统计结果人群数量
Long crowdCount = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.bitCount(resultKey.getBytes());
}
});
// 设置结果过期时间
redisTemplate.expire(resultKey, Duration.ofHours(1));
return new CrowdCalculationResult(resultKey, crowdCount);
}
/**
* 复杂标签组合计算 - 支持"A且B非C"逻辑
*
* @param includeTags 包含标签
* @param excludeTags 排除标签
* @return 计算结果
*/
public CrowdCalculationResult calculateComplexCrowd(
List<String> includeTags, List<String> excludeTags) {
// 第一步:计算包含标签的交集
String includeResultKey = null;
if (!includeTags.isEmpty()) {
CrowdCalculationResult includeResult = calculateIntersection(includeTags);
includeResultKey = includeResult.getResultKey();
}
// 第二步:计算排除标签的并集
String excludeResultKey = null;
if (!excludeTags.isEmpty()) {
excludeResultKey = buildResultKey("exclude_union", excludeTags);
String[] excludeBitmapKeys = excludeTags.stream()
.map(this::buildTagBitmapKey)
.toArray(String[]::new);
redisTemplate.opsForValue().bitOp(RedisStringCommands.BitOperation.OR,
excludeResultKey, excludeBitmapKeys);
}
// 第三步:计算最终结果 (包含 AND NOT 排除)
String finalResultKey = buildResultKey("complex",
Arrays.asList(String.join(",", includeTags), String.join(",", excludeTags)));
if (includeResultKey != null && excludeResultKey != null) {
// 包含 - 排除
redisTemplate.opsForValue().bitOp(RedisStringCommands.BitOperation.AND,
finalResultKey, includeResultKey);
redisTemplate.opsForValue().bitOp(RedisStringCommands.BitOperation.NOT,
"temp_not_" + excludeResultKey, excludeResultKey);
redisTemplate.opsForValue().bitOp(RedisStringCommands.BitOperation.AND,
finalResultKey, finalResultKey, "temp_not_" + excludeResultKey);
} else if (includeResultKey != null) {
// 只有包含条件
redisTemplate.opsForValue().set(finalResultKey,
redisTemplate.opsForValue().get(includeResultKey));
}
// 统计最终人群数量
Long finalCount = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
return connection.bitCount(finalResultKey.getBytes());
}
});
return new CrowdCalculationResult(finalResultKey, finalCount);
}
/**
* 用户ID到位图索引的映射策略
* 核心设计:支持雪花ID等大整数的高效映射
*/
private long mapUserIdToBitIndex(Long userId) {
// 方案1:直接使用用户ID(适用于连续ID)
// return userId;
// 方案2:哈希映射(适用于稀疏ID,需要处理冲突)
// return Math.abs(userId.hashCode()) % MAX_USER_COUNT;
// 方案3:维护映射表(我们项目采用的方案)
return getUserIndexFromMappingTable(userId);
}
/**
* 从映射表获取用户索引
* 设计要点:userId <-> index 双向映射,支持快速查找
*/
private long getUserIndexFromMappingTable(Long userId) {
String mappingKey = "user_index_mapping:" + userId;
Long index = (Long) redisTemplate.opsForValue().get(mappingKey);
if (index == null) {
// 分配新的索引
index = redisTemplate.opsForValue().increment("user_index_counter");
redisTemplate.opsForValue().set(mappingKey, index);
// 反向映射,用于结果回查
redisTemplate.opsForValue().set("index_user_mapping:" + index, userId);
}
return index;
}
/**
* 从圈选结果提取用户ID列表
*
* @param resultKey 圈选结果键
* @param limit 限制返回数量
* @return 用户ID列表
*/
public List<Long> extractUserIdsFromResult(String resultKey, int limit) {
List<Long> userIds = new ArrayList<>();
// 遍历位图,找出所有为1的位
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] bitmapBytes = connection.get(resultKey.getBytes());
if (bitmapBytes == null) return null;
int count = 0;
for (int byteIndex = 0; byteIndex < bitmapBytes.length && count < limit; byteIndex++) {
byte b = bitmapBytes[byteIndex];
for (int bitIndex = 0; bitIndex < 8 && count < limit; bitIndex++) {
if ((b & (1 << bitIndex)) != 0) {
long index = byteIndex * 8L + bitIndex;
// 通过反向映射获取真实用户ID
Long userId = (Long) redisTemplate.opsForValue()
.get("index_user_mapping:" + index);
if (userId != null) {
userIds.add(userId);
count++;
}
}
}
}
return null;
}
});
return userIds;
}
// 工具方法
private String buildTagBitmapKey(String tagId) {
return "tag_bitmap:" + tagId;
}
private String buildResultKey(String operation, List<String> tagIds) {
String tagHash = String.valueOf(tagIds.hashCode());
return String.format("crowd_result:%s:%s:%d", operation, tagHash, System.currentTimeMillis());
}
}
/**
* 圈选计算结果封装
*/
@Data
@AllArgsConstructor
public class CrowdCalculationResult {
private String resultKey; // Redis结果键
private Long crowdCount; // 人群数量
private Long calculateTime; // 计算耗时(ms)
public CrowdCalculationResult(String resultKey, Long crowdCount) {
this.resultKey = resultKey;
this.crowdCount = crowdCount;
this.calculateTime = System.currentTimeMillis();
}
}1.3 性能优化策略
内存优化
/**
* 位图内存优化策略
*/
@Component
public class BitmapMemoryOptimizer {
/**
* 分片存储 - 突破Redis单key内存限制
* 设计思路:将大位图拆分为多个小位图,并行计算后合并结果
*/
public void shardedBitmapStorage(String tagId, List<Long> userIds) {
int shardSize = 10_000_000; // 每片1000万用户
Map<Integer, List<Long>> shardedUsers = userIds.stream()
.collect(Collectors.groupingBy(userId -> (int)(userId / shardSize)));
// 并行写入各分片
shardedUsers.entrySet().parallelStream().forEach(entry -> {
int shardId = entry.getKey();
List<Long> shardUsers = entry.getValue();
String shardKey = String.format("tag_bitmap:%s:shard:%d", tagId, shardId);
// 批量设置分片位图
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (Long userId : shardUsers) {
long bitOffset = userId % shardSize;
connection.setBit(shardKey.getBytes(), bitOffset, true);
}
return null;
}
});
});
}
/**
* 热点数据预计算
* 业务场景:常用标签组合提前计算并缓存
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void preCalculateHotCombinations() {
// 从配置或统计数据获取热门标签组合
List<List<String>> hotCombinations = getHotTagCombinations();
hotCombinations.parallelStream().forEach(tagCombination -> {
try {
CrowdCalculationResult result = calculateIntersection(tagCombination);
// 将结果缓存更长时间
redisTemplate.expire(result.getResultKey(), Duration.ofHours(6));
log.info("预计算完成 - 标签组合: {}, 人群数量: {}",
tagCombination, result.getCrowdCount());
} catch (Exception e) {
log.error("预计算失败 - 标签组合: {}", tagCombination, e);
}
});
}
}二、RoaringBitmap进阶应用
2.1 RoaringBitmap核心优势
核心设计原理:
- 分块存储:将32位整数空间分为65536个块,每块包含65536个数
- 自适应容器:根据数据密度自动选择最优存储结构
- ArrayContainer:稀疏数据用短数组存储
- BitmapContainer:稠密数据用传统位图
- RunContainer:连续数据用区间编码
2.2 项目中的RoaringBitmap实现
/**
* RoaringBitmap与Redis结合的高性能标签计算
*
* 设计亮点:
* 1. 利用RoaringBitmap的压缩特性,减少Redis存储空间
* 2. 支持直接存储雪花ID,无需额外映射表
* 3. 序列化后存储,支持分布式访问
*/
@Service
public class RoaringBitmapTagService {
@Autowired
private RedisTemplate<String, byte[]> redisTemplate;
/**
* 创建标签的RoaringBitmap
*
* @param tagId 标签ID
* @param userIds 用户ID列表(支持雪花ID等大整数)
*/
public void createTagBitmap(String tagId, List<Long> userIds) {
RoaringBitmap bitmap = new RoaringBitmap();
// 直接添加用户ID,无需映射
userIds.forEach(userId -> bitmap.add(userId.intValue()));
// 序列化存储到Redis
try {
byte[] serializedBitmap = serializeRoaringBitmap(bitmap);
String redisKey = "roaring_tag:" + tagId;
redisTemplate.opsForValue().set(redisKey, serializedBitmap);
redisTemplate.expire(redisKey, Duration.ofDays(7));
log.info("标签位图创建成功 - 标签: {}, 用户数: {}, 压缩后大小: {} bytes",
tagId, userIds.size(), serializedBitmap.length);
} catch (IOException e) {
throw new RuntimeException("RoaringBitmap序列化失败", e);
}
}
/**
* 高性能标签交集计算
*
* @param tagIds 标签ID列表
* @return 交集结果和统计信息
*/
public RoaringBitmapResult calculateTagIntersection(List<String> tagIds) {
long startTime = System.currentTimeMillis();
// 批量从Redis获取RoaringBitmap
List<RoaringBitmap> bitmaps = tagIds.parallelStream()
.map(this::loadRoaringBitmapFromRedis)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (bitmaps.isEmpty()) {
return new RoaringBitmapResult(new RoaringBitmap(), 0, 0);
}
// 执行交集运算 - RoaringBitmap原生支持
RoaringBitmap result = bitmaps.get(0);
for (int i = 1; i < bitmaps.size(); i++) {
result = RoaringBitmap.and(result, bitmaps.get(i));
}
long calculateTime = System.currentTimeMillis() - startTime;
// 缓存计算结果
cacheCalculationResult("intersection", tagIds, result);
return new RoaringBitmapResult(result, result.getCardinality(), calculateTime);
}
/**
* 复杂逻辑计算:A且B非C
*
* @param includeTags 包含的标签
* @param excludeTags 排除的标签
* @return 计算结果
*/
public RoaringBitmapResult calculateComplexLogic(
List<String> includeTags, List<String> excludeTags) {
long startTime = System.currentTimeMillis();
// 计算包含标签的交集
RoaringBitmap includeResult = null;
if (!includeTags.isEmpty()) {
RoaringBitmapResult includeCalc = calculateTagIntersection(includeTags);
includeResult = includeCalc.getBitmap();
}
// 计算排除标签的并集
RoaringBitmap excludeResult = null;
if (!excludeTags.isEmpty()) {
List<RoaringBitmap> excludeBitmaps = excludeTags.stream()
.map(this::loadRoaringBitmapFromRedis)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!excludeBitmaps.isEmpty()) {
excludeResult = excludeBitmaps.get(0);
for (int i = 1; i < excludeBitmaps.size(); i++) {
excludeResult = RoaringBitmap.or(excludeResult, excludeBitmaps.get(i));
}
}
}
// 计算最终结果:包含 - 排除
RoaringBitmap finalResult;
if (includeResult != null && excludeResult != null) {
finalResult = RoaringBitmap.andNot(includeResult, excludeResult);
} else if (includeResult != null) {
finalResult = includeResult;
} else {
finalResult = new RoaringBitmap();
}
long calculateTime = System.currentTimeMillis() - startTime;
return new RoaringBitmapResult(finalResult, finalResult.getCardinality(), calculateTime);
}
/**
* 从Redis加载RoaringBitmap
*/
private RoaringBitmap loadRoaringBitmapFromRedis(String tagId) {
try {
String redisKey = "roaring_tag:" + tagId;
byte[] serializedBitmap = redisTemplate.opsForValue().get(redisKey);
if (serializedBitmap == null) {
log.warn("标签位图不存在: {}", tagId);
return null;
}
return deserializeRoaringBitmap(serializedBitmap);
} catch (Exception e) {
log.error("加载RoaringBitmap失败 - 标签: {}", tagId, e);
return null;
}
}
/**
* RoaringBitmap序列化
*/
private byte[] serializeRoaringBitmap(RoaringBitmap bitmap) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
bitmap.serialize(dos);
return baos.toByteArray();
}
/**
* RoaringBitmap反序列化
*/
private RoaringBitmap deserializeRoaringBitmap(byte[] data) throws IOException {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bais);
RoaringBitmap bitmap = new RoaringBitmap();
bitmap.deserialize(dis);
return bitmap;
}
/**
* 缓存计算结果
*/
private void cacheCalculationResult(String operation, List<String> tagIds, RoaringBitmap result) {
try {
String cacheKey = String.format("roaring_result:%s:%s",
operation, String.join(",", tagIds));
byte[] serializedResult = serializeRoaringBitmap(result);
redisTemplate.opsForValue().set(cacheKey, serializedResult, Duration.ofHours(2));
} catch (IOException e) {
log.error("缓存计算结果失败", e);
}
}
}
/**
* RoaringBitmap计算结果封装
*/
@Data
@AllArgsConstructor
public class RoaringBitmapResult {
private RoaringBitmap bitmap; // 结果位图
private long cardinality; // 结果数量
private long calculateTimeMs; // 计算耗时
/**
* 提取用户ID列表
*/
public List<Integer> extractUserIds(int limit) {
List<Integer> userIds = new ArrayList<>();
IntIterator iterator = bitmap.getIntIterator();
int count = 0;
while (iterator.hasNext() && count < limit) {
userIds.add(iterator.next());
count++;
}
return userIds;
}
/**
* 获取压缩比信息
*/
public String getCompressionInfo() {
long uncompressedSize = cardinality * 4; // 每个int 4字节
long compressedSize = bitmap.getSizeInBytes();
double compressionRatio = (double) compressedSize / uncompressedSize;
return String.format("压缩前: %d bytes, 压缩后: %d bytes, 压缩比: %.2f%%",
uncompressedSize, compressedSize, compressionRatio * 100);
}
}2.3 用户ID映射策略详解
方案对比分析
| 映射方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 顺序索引映射 | 空间利用率100%,位图最紧凑 | 需要维护映射表,增加复杂度 | 用户ID稀疏分布 |
| 直接ID存储 | 实现简单,无需映射 | 空间浪费严重,雪花ID会产生巨大稀疏位图 | 用户ID连续分布 |
| 哈希映射 | 实现简单,支持任意ID | 存在哈希冲突,可能丢失数据 | 对精确性要求不高的场景 |
| RoaringBitmap直接存储 | 自动压缩,支持稀疏ID | 需要额外的序列化开销 | 大规模稀疏数据 |
项目中采用的混合策略
/**
* 用户ID映射策略管理器
*
* 设计思路:根据数据特征动态选择最优映射策略
*/
@Component
public class UserIdMappingStrategy {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 智能选择映射策略
*
* @param userIds 用户ID列表
* @return 映射策略类型
*/
public MappingStrategyType selectOptimalStrategy(List<Long> userIds) {
if (userIds.isEmpty()) {
return MappingStrategyType.DIRECT;
}
// 分析ID分布特征
long minId = userIds.stream().min(Long::compareTo).orElse(0L);
long maxId = userIds.stream().max(Long::compareTo).orElse(0L);
long idRange = maxId - minId + 1;
double sparsity = (double) userIds.size() / idRange;
// 策略选择逻辑
if (sparsity > 0.1) {
// 密集分布,直接使用ID作为索引
return MappingStrategyType.DIRECT;
} else if (idRange > 100_000_000L) {
// 超大范围稀疏分布,使用RoaringBitmap
return MappingStrategyType.ROARING_BITMAP;
} else {
// 中等稀疏分布,使用顺序映射
return MappingStrategyType.SEQUENTIAL_MAPPING;
}
}
/**
* 顺序映射实现 - 项目主要采用的方案
*/
@Component
public static class SequentialMappingHandler {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 批量分配用户索引
*
* @param userIds 用户ID列表
* @return userId -> index 映射关系
*/
public Map<Long, Long> batchAllocateUserIndex(List<Long> userIds) {
Map<Long, Long> mappingResult = new HashMap<>();
// 检查已存在的映射
List<Long> newUserIds = new ArrayList<>();
for (Long userId : userIds) {
String mappingKey = "user_index:" + userId;
Long existingIndex = (Long) redisTemplate.opsForValue().get(mappingKey);
if (existingIndex != null) {
mappingResult.put(userId, existingIndex);
} else {
newUserIds.add(userId);
}
}
// 为新用户分配索引
if (!newUserIds.isEmpty()) {
// 批量获取连续索引
Long startIndex = redisTemplate.opsForValue()
.increment("global_user_index_counter", newUserIds.size()) - newUserIds.size() + 1;
// 批量写入映射关系
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (int i = 0; i < newUserIds.size(); i++) {
Long userId = newUserIds.get(i);
Long index = startIndex + i;
// 正向映射:userId -> index
String userKey = "user_index:" + userId;
connection.set(userKey.getBytes(), index.toString().getBytes());
// 反向映射:index -> userId
String indexKey = "index_user:" + index;
connection.set(indexKey.getBytes(), userId.toString().getBytes());
mappingResult.put(userId, index);
}
return null;
}
});
}
return mappingResult;
}
/**
* 批量反查用户ID
*
* @param indices 索引列表
* @return index -> userId 映射关系
*/
public Map<Long, Long> batchQueryUserIdByIndex(List<Long> indices) {
Map<Long, Long> result = new HashMap<>();
List<String> indexKeys = indices.stream()
.map(index -> "index_user:" + index)
.collect(Collectors.toList());
List<Object> userIds = redisTemplate.opsForValue().multiGet(indexKeys);
for (int i = 0; i < indices.size(); i++) {
Object userId = userIds.get(i);
if (userId != null) {
result.put(indices.get(i), Long.valueOf(userId.toString()));
}
}
return result;
}
}
}
/**
* 映射策略枚举
*/
public enum MappingStrategyType {
DIRECT, // 直接使用ID
SEQUENTIAL_MAPPING, // 顺序映射
HASH_MAPPING, // 哈希映射
ROARING_BITMAP // RoaringBitmap存储
}三、性能优化与监控
3.1 性能监控指标
/**
* 位图计算性能监控
*/
@Component
public class BitmapPerformanceMonitor {
private final MeterRegistry meterRegistry;
private final Timer calculationTimer;
private final Counter calculationCounter;
private final Gauge memoryUsageGauge;
public BitmapPerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.calculationTimer = Timer.builder("bitmap.calculation.time")
.description("位图计算耗时")
.register(meterRegistry);
this.calculationCounter = Counter.builder("bitmap.calculation.count")
.description("位图计算次数")
.register(meterRegistry);
this.memoryUsageGauge = Gauge.builder("bitmap.memory.usage")
.description("位图内存使用量")
.register(meterRegistry, this, BitmapPerformanceMonitor::getCurrentMemoryUsage);
}
/**
* 监控位图计算性能
*/
public <T> T monitorCalculation(String operation, Supplier<T> calculation) {
return calculationTimer.recordCallable(() -> {
calculationCounter.increment(Tags.of("operation", operation));
long startMemory = getCurrentMemoryUsage();
T result = calculation.get();
long endMemory = getCurrentMemoryUsage();
// 记录内存增长
meterRegistry.counter("bitmap.memory.growth",
"operation", operation)
.increment(endMemory - startMemory);
return result;
});
}
private long getCurrentMemoryUsage() {
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
}
}3.2 缓存策略优化
/**
* 多级缓存策略
*
* L1: 本地缓存 (Caffeine) - 热点数据
* L2: Redis缓存 - 分布式共享
* L3: 数据库 - 持久化存储
*/
@Service
public class MultiLevelCacheService {
// L1缓存:本地热点数据
private final Cache<String, RoaringBitmap> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats()
.build();
@Autowired
private RedisTemplate<String, byte[]> redisTemplate;
/**
* 多级缓存获取位图
*/
public RoaringBitmap getBitmapWithCache(String tagId) {
// L1缓存查询
RoaringBitmap bitmap = localCache.getIfPresent(tagId);
if (bitmap != null) {
return bitmap;
}
// L2缓存查询
try {
String redisKey = "roaring_tag:" + tagId;
byte[] serializedBitmap = redisTemplate.opsForValue().get(redisKey);
if (serializedBitmap != null) {
bitmap = deserializeRoaringBitmap(serializedBitmap);
// 回写L1缓存
localCache.put(tagId, bitmap);
return bitmap;
}
} catch (Exception e) {
log.error("Redis缓存查询失败", e);
}
// L3数据库查询(兜底)
bitmap = loadBitmapFromDatabase(tagId);
if (bitmap != null) {
// 回写多级缓存
localCache.put(tagId, bitmap);
cacheBitmapToRedis(tagId, bitmap);
}
return bitmap;
}
/**
* 缓存预热策略
*/
@EventListener(ApplicationReadyEvent.class)
public void warmUpCache() {
// 预热热门标签
List<String> hotTags = getHotTags();
hotTags.parallelStream().forEach(tagId -> {
try {
getBitmapWithCache(tagId);
log.info("缓存预热完成: {}", tagId);
} catch (Exception e) {
log.error("缓存预热失败: {}", tagId, e);
}
});
}
}四、面试常见问题与回答
Q1: 为什么选择位图而不是其他数据结构?
回答要点:
- 空间效率:1亿用户仅需12MB,比Set结构节省99%空间
- 计算性能:位运算是CPU最基础操作,AND/OR/NOT运算毫秒级完成
- 并发友好:Redis位图操作原子性,天然支持高并发
- 业务匹配:标签圈选本质是集合运算,位图是最自然的表示方式
Q2: 如何处理用户ID不连续的问题?
回答要点:
我们采用了多种策略:
- 顺序映射:维护userId↔index双向映射表,保证位图紧凑
- RoaringBitmap:对于超大稀疏ID,直接使用RoaringBitmap存储
- 智能选择:根据ID分布特征自动选择最优策略
Q3: 如何保证数据一致性?
回答要点:
- 写入策略:先写MySQL保证核心数据,再异步同步到Redis
- 版本控制:每个位图带版本号,避免并发更新冲突
- 补偿机制:定时任务检查并修复不一致数据
- 监控告警:实时监控数据一致性指标
Q4: 系统如何支撑亿级用户?
回答要点:
- 分片存储:大位图拆分为多个小位图,突破单key限制
- 并行计算:利用多核CPU并行处理不同分片
- 缓存分层:本地缓存+Redis缓存+数据库的三级架构
- 异步处理:复杂查询异步计算,实时返回预估结果
Q5: 如何优化计算性能?
回答要点:
- 预计算:热门标签组合提前计算并缓存
- 批量操作:使用Pipeline减少网络开销
- 内存优化:及时清理临时位图,避免内存泄漏
- 算法优化:利用RoaringBitmap的分块特性,只计算有数据的块
五、项目亮点总结
技术创新点
- 混合架构:Redis Bitmap + RoaringBitmap,兼顾性能和空间效率
- 智能映射:根据数据特征自动选择最优ID映射策略
- 多级缓存:本地+分布式+持久化的三级缓存体系
- 性能监控:全链路性能监控和自动优化
业务价值
- 响应速度:标签圈选从分钟级优化到毫秒级
- 成本降低:存储成本降低90%,服务器资源节省60%
- 业务支撑:支持复杂营销策略,提升转化率30%
- 系统稳定:99.9%可用性,支撑日均千万级查询
个人成长
- 架构能力:从业务需求出发设计技术方案
- 性能优化:深入理解底层原理,持续优化系统性能
- 工程实践:掌握大规模分布式系统的设计和实现
- 团队协作:与产品、测试密切配合,保证项目质量
本文档整理了获客项目中位图计算组件的完整技术实现,涵盖了从基础原理到工程实践的各个方面,是面试准备的重要参考资料。