前言

拼单活动不是所有用户都能看到。比如校园店只给某栋楼的用户发券,写字楼活动只给企业群用户展示,或者新用户专享一杯咖啡 9.9 元。这个时候人群标签就成了营销系统的基础能力。

如果每次判断用户是否命中标签都查数据库,高峰期商品页会把数据库打得很难受。Redis BitMap 的优势是用很低的空间成本保存大量布尔状态,判断用户是否在标签里只需要一次 bit 查询。

为什么用BitMap

BitMap 本质上是按位存储。用户 ID 经过映射得到一个整数下标,命中标签就把该下标设置为 1。判断时读取同一个下标即可。对于只需要“是否属于某人群”的场景,它比 Set 更省内存,也更适合做批量统计。

当然,BitMap 要求用户 ID 能稳定映射到下标。项目里通过 RedisService 的 getIndexFromUserId 做映射。实际系统里我会把这个映射策略固定下来,并避免不同环境产生不同下标。

标签写入

@Override
public void addCrowdTagsUserId(String tagId, String userId) {
    CrowdTagsDetail detail = new CrowdTagsDetail();
    detail.setTagId(tagId);
    detail.setUserId(userId);

    try {
        crowdTagsDetailDao.addCrowdTagsUserId(detail);

        RBitSet bitSet = redisService.getBitSet(tagId);
        bitSet.set(redisService.getIndexFromUserId(userId), true);
    } catch (DuplicateKeyException ignore) {
        // 忽略唯一索引冲突
    }
}

这段实现做了两件事:先把明细写入 MySQL,再同步写入 Redis BitSet。MySQL 负责可追溯和离线统计,Redis 负责线上快速判断。DuplicateKeyException 被忽略,是因为标签导入任务可能重复执行,唯一索引用来保证幂等。

用户ID映射

BitMap 最怕的是下标冲突和下标过大。如果 userId 是纯数字,可以直接转 long;如果是字符串,就需要 hash 或维护映射表。hash 会有冲突风险,映射表更准确但成本更高。我的取舍是:业务 userId 可控时优先使用数字化 ID,不可控时单独设计映射服务。

public int getIndexFromUserId(String userId) {
    // 示例:实际项目要保证映射稳定,避免环境差异。
    return Math.abs(userId.hashCode());
}

public boolean isTagged(String tagId, String userId) {
    RBitSet bitSet = redisService.getBitSet(tagId);
    return bitSet.get(getIndexFromUserId(userId));
}

如果用户规模很大,hashCode 直接取绝对值可能导致 BitMap 过稀,内存不划算。更稳的方案是给用户分配连续数字 ID,或者按标签分桶。文章里写这个细节,是因为面试官很容易追问 BitMap 的下标怎么来。

可见性判断

标签不仅能控制优惠,还能控制活动可见性。比如某个拼单活动只允许标签用户看到,此时 TagNode 可以读取 groupBuyDiscount 的 tagId,再查询 Redis BitMap。命中则 visible=true,否则返回普通商品信息。

boolean visible = true;
String tagId = dynamicContext.getGroupBuyActivityDiscountVO()
        .getGroupBuyDiscount()
        .getTagId();

if (StringUtils.isNotBlank(tagId)) {
    visible = redisService.getBitSet(tagId)
            .get(redisService.getIndexFromUserId(request.getUserId()));
}

dynamicContext.setVisible(visible);

这里我不会把“不命中标签”当成系统异常,而是业务结果。用户不在活动人群内,前端仍然可以展示商品原价,只是不展示拼单优惠。这样活动配置错误也不会影响普通购买路径。

小结

Redis BitMap 适合承载高频、简单、布尔型的人群判断。MySQL 保留明细,Redis 做在线判断,唯一索引保证导入幂等,规则树节点把标签结果转成可见性和参与状态。这个设计正好对应简历里的“用户标签与可售库存、人群可见性控制”。