大量超投了怎么办
业务中的超投
广告投放中会有超投,这是一个常见现象。主要原因有两个:
1、只判断广告主账户是否有钱:多个线程并发判断都有预算,然后进行消费(执行业务),最终可能导致预算不足。因为检查账户和扣费是分两步、非原子性的。
2、即使上述做了原子操作,依旧有超投情况,因为从召回到曝光到扣费天然有时间差了,而且这个时间差是无法通过技术手段去除的。
方案演化
方案一:分布式锁
面对分布式系统下竞争同一资源(同一份预算),我们首先想到的是分布式锁。但是我们的集群规模较大,并发线程数量级如果来到几百甚至几千时这个方案就会有性能问题。显式的加锁/释放锁,加上等待者的等待时间,性能劣化会非常严重。这对很多业务往往是不可接受的。因此这个方案很快就被否定了。
分布式锁,包括其他互斥锁使用时都有一个原则:在高并发的主路径上最好不采用这种方案。低频次的数据更新等场景可以采取这种方案。
方案二:redis lua原子预扣
利用lua多命令的原子性,把判断预算+减扣+返回结果压成一步,作为一个原子操作。这样做的好处是由于没有显式的加锁/释放锁。因此性能大大优于方案一,也少了线程的串行等待锁的时间。
这个方案对于大部分场景已经没啥问题了。但是如果遇到热key怎么办?Lua原子操作大约是0.01-0.05ms。一千个请求引入的效果是10-50ms。这个rt的增加不容忽视。
方案三:预算分片(分桶),把热key打散
Lua原子操作大约是0.01-0.05ms。一千个请求引入的效果是10-50ms。为了热key减小方案二对于RT的影响,我们采取了预算分片策略。这是常见的应对热key的方式。耗时劣化的问题就大大缓解了。比如分10片,那么rt增加就控制在了1-5ms了,这是可以接受的。
方案四:预算预占策略
方案三基本上解决了并发消耗同一个账户导致的超投。但是还有最开始说的问题二。问题二是说召回到曝光到结算天然有时间差,这段时间在大流量下产生的超投不容小觑。
我想到的方案是能不能先在redis里记录下召回后要去曝光的预算明细和总量。
比如现在有十个召回返回到端上了,这十个曝光还没有被结算真正扣费。我们称这十条广告预占预算为inflight.
这十条明细需要被记录下来,因为结算消费后要找到其中的某一条,我们称之为inflight_detail。然后inflight-inflight_detail,就是新的预占总额。
引擎测:那一条请求到引擎,引擎需要判断什么呢?
1、inflight=inflight+this_cost;
2、budget-spent-inflight>0,这条cost是本次要返回广告的扣费,也就是新增的inflight_detail。
结算侧:
结算侧收到一条曝光数据应该怎么处理呢?查一下明细,找到明细,将它从inflight释放:
inflight = inflight - this_cost;并删除这条明细inflight_detail。
并把预占预算转换为已花费:
spent = spent + this_cost
那我们需要维护什么样的数据结构呢?
预算以品牌为粒度隔离,那么就设计hashMap,brandId+分片号为key,找到这个HashMap。
HashMap里维护budget、spent、inflight。
预占明细不要记录在HashMap,因为要做回收和对账两个事情,因此我们选择了zset,也就是sorted set。

大致如上图所示。
我们还新增了一个inflight泄漏回收的定时任务,过一阵扫描一下,释放预占预算后没有曝光的数据。所以score必须记录为预占发生时的时间戳。比如60秒前预占没释放的都释放掉。
member就是traceId:cnid:shopId:cost。回收时要记得把cost从在途中扣掉。
该方案遇到的坑:
泄漏回收job和结算服务都会同时操作member:todo

浙公网安备 33010602011771号