#. 活动发奖
1. 抽奖品
场景:在多个奖品中根据权重或概率随机抽出一个奖品。
方案:1)权重-直接存long值; 2)概率-存小数计算时转long值
long 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;
value = redis.increment(key, 1)
if(value > total){
redis.decrement(key, 1)
return "";
}
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)
forEach(过滤时间未到的中奖用户,因为提前开奖了)