营销活动常见的业务场景解决方案

533 阅读4分钟

#. 活动发奖

1. 抽奖品

场景:在多个奖品中根据权重或概率随机抽出一个奖品。
方案:1)权重-直接存long值; 2)概率-存小数计算时转longlong r = ThreadLocalRandom.current().nextLong(bound);
    for (Item item : items) {
        if (item.begin <= r && r <= item.end) {
            return item.obj;
        }
    }

2. 扣奖品库存

场景:在一个活动下,给用户发奖品。
方案:乐观锁-CAS法(扣库存之前判断之前查询的数据是否被修改过)
    update prize 
    set stock = stock + 1
    where id = 10 and total >= stock + 1
    limit 1;

瓶颈:在高并发的场景下,对于一个奖品库存扣减,db扛不住!
已知:MySQL热点key更新,最大支持qps:1k(实际业务500左右)
     Redis热点key更新,最大支持qps:1w(实际业务5k左右)
进阶方案:异步扣库存
    1)扣库存缓存 ;
        count = redis.increment(key, 1)
        if(count > total){
            redis.decrement(key, 1)
            return false;
        }
    2)发送MQ;
    3)消费MQ扣库存(同上sql)

Redis也扛不住:二级缓存(加本地缓存)、奖品打散、网关限流等。
Ext:
支付宝防并发方案:一锁二判三更新(悲观锁)
    对于扣奖品库存,本质是对整数类型字段做+1 -1更新,where条件来控制是否能执行,无并发问题(update时当前读)
    而对于要对单个字段做覆盖更新、or 多个整数字段+1 -1,就会存在并发问题了;
    此时两种思路:
        1.db更新时进行一锁二判三更新; 
        2.db操作前加redis锁,trylock(10s 尝试获取锁时间),key=uid+prizeId/commodityId,
          获取不到锁的线程会进行等待,重复尝试获取锁。
    思路1:锁的粒度小,代码略微复杂,有死锁风险;思路2:锁粒度大,代码简单
    
场景:
    Case1:给用户发权益,如果用户已有权益,则进行续期(更新有效时间)
        1)锁定用户已有权益
            select * from privilege
            where uid= "782103" and privilegeId= "P234212" 
            for update;
        2)计算新的有效期
        3)更新有效期
            update privilege
            set endTime = "xxx"
            where uid= "782103" and privilegeId= "P234212"
        // 方式2:发权益时,tryLock(10s),key=uid+privilegeId,获取不到锁的线程等待,尝试获取锁
        
    Case2: 更新计次周期流水cycleInfo,该字段是个Object对象序列化字符串
        // 方式1:forupdate 判断 更新; 方式2:tryLock key=uid+cycleId
           
    Case3: 用户消耗多种卡片兑换一个奖品,每种卡片有数量,且可以送出去进行消耗;
        1)锁定用户的卡片纪录
            select * from card_record
            where uid= "782103" and activityId= "AC3b552d3" 
            for update;
        2)判断是否满足兑换条件,满足进行兑换,更新卡片数量+记流水+发奖品,反之失败。

Ref:支付宝防并发方案之"一锁二判三更新"

#. 活动定时开奖

场景:用户参与活动获得抽奖码,在某一时刻随机抽出若干抽奖码中奖。
方案:
    1)发抽奖码时,在抽奖码上给一个随机的权重值;
    2)开奖时,权重大的抽奖码中奖(或者随机一个权重值,取大于/小于该值的前几个获奖)
        select * from lottery_code_record
        where weight_num < 1234567890123456789
            and activity_id = "AC20221119b3b552d3"
        order by weight_num desc
        limit 200

#. 活动发百万兑换码奖品

场景:兑换码是一小串字符串,由三方公司提前生成以Excel形式给到,借助营销活动发出去。
方案:
    1)提前将Excel中的兑换码导入db,生成的兑换码数据ID自增,1 2 3 ... 100w;
    2)发奖时竞争redis key,value值为兑换码的数据库主键ID;
        // a. 扣库存缓存
        value = redis.increment(key, 1)
        if(value > total){
            redis.decrement(key, 1)
            return "";
        }
        // b. 查询兑换码数据
        select voucher_code from voucher_record where id = #{value}

#. 活动实时展示中奖弹幕

场景:活动每30s抽一个幸运用户,将中奖的用户信息实时展示在活动页上。
方案:后台搞一个开奖job,每5分钟执行一次,开10个奖(准确说:job每1分钟执行一次,前1分钟进行开奖,后4分钟确保奖已经开出来了),当前的5分钟开的是下个5分钟的中奖用户。
    1)后台开奖塞redis
        redis.opsForList().leftPush/leftPushAll() // 头插
        redis.opsForList().trim(key, 0, size)
    2)c侧透出
        redis.opsForList().range(key, 0, num) // num大一些,前面的中奖数据还未到时间漏出
        forEach(过滤时间未到的中奖用户,因为提前开奖了)