大量超投了怎么办

业务中的超投


广告投放中会有超投,这是一个常见现象。主要原因有两个:
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。

d5eb70d4b55a89e7f2b0f99e01719347

 大致如上图所示。

我们还新增了一个inflight泄漏回收的定时任务,过一阵扫描一下,释放预占预算后没有曝光的数据。所以score必须记录为预占发生时的时间戳。比如60秒前预占没释放的都释放掉。

member就是traceId:cnid:shopId:cost。回收时要记得把cost从在途中扣掉。

该方案遇到的坑:

泄漏回收job和结算服务都会同时操作member:todo

 

 

 

 

 

posted @ 2026-11-03 09:47  ACityWalker  阅读(2)  评论(0)    收藏  举报