标签:
我们的基本架构是客户端请求API,然后由API发送RPC请求到我们的服务,服务通过注册中心来管理
检索服务,根据数据库中的广告建立索引,订阅Redis Channel通过通知机制来使内存中的广告与数据库保持一致
曝光服务,接收曝光和点击,聚合以后将聚合的结果推送到消息队列
计费服务,计费服务与支付系统进行交互,主要负责扣广告主的钱,发起余额不足,预算不足的下线
需要解决的问题
一个用户本身有一些属性,比如性别,年龄,网络环境,设备类型,地域信息等等,而广告主想让自己的广告投放在特定的人群中,根据用户的属性,检索出可用的广告过程就完成了广告定向
比如这个例子,广告2就不满足定向条件,广告3,广告1满足
广告定向表
广告ID 性别 年龄段 网络 操作系统 广告1 不限 不限 不限 不限 广告2 女 0-18 不限 不限 广告3 男 0-18 不限 IOS
根据以上这张定向表,看上去有点像数据库,假设图中小人的流量来了,换成数据库的查询语句,应该是
where (性别=男 or 性别=不限)
and (年龄=0-18 or 年龄=不限)
and (网络=wifi or 网络=不限)
and (操作系统=ios or 操作系统=不限)
如果没有“不限”,那么看上去组合索引是最好的选择,我们把 性别-年龄-网络-操作系统 组合成一个索引,这样索引的空间是
where语句只看性别一个维度,or左右两边是等值查询肯定可以利用索引求出两个集合再取并集,然后再和其他维度取交集
集合操作
如何取并集可以参考两个有序数组的合并排序
如何取交集,一般有三种方法
对每个维度在内存中建立倒排哈希索引,然后我们把某个维度下的不限做了冗余,这样就不需要做并集,每个维度用数据最少的维度数据做为驱动表,然后在其他并集的结果中做哈希检测,如果不存在就把这个广告删除掉
注意,我们的定向只做了一次adid内存拷贝到某个流量检索的上下文,而且拷贝的是最小的集合,其他要做的是不断的哈希检测,把这个集合根据其他维度定向再缩小范围
以上这种定向方法主要使用了倒排索引,还有一些求交集的技巧,倒排索引在处理等值查询和动态多维度组合的时候是非常适合的,但是在处理范围查询的时候就不太好使了,比如我们年龄要是支持任意年龄区间的定向,也就是要处理范围查询,一些有序结构比如平衡树会是更好的选择
广告在区域定向条件如下
广告区域定向表
广告ID 省 市 区 广告1 不限 不限 不限 广告2 北京 北京市区 朝阳 广告3 上海 上海市区 不限
假设一个用户在北京,北京市区,朝阳要检索广告,SQL如下:
where (省=不限 and 市=不限 and 区=不限)
or (省=北京 and 市=北京市区 and 区=不限)
or (省=北京 and 市=北京市区 and 区=朝阳)
之前我们说可把不限这种情况冗余到所有数据项下面,这样就避免了OR操作,省市区这种无法冗余的原因有两个
观察这个SQL是先AND再OR,之前分析过多个AND适合组合索引,所以假如我们可以把省-市-区看成一个索引中查询的值,相当于我们查3次组合索引,然后再取并集,得到的集合还需要和其他集合做交集,而且这个集合因为是OR出来的动态变化的所以不得不把广告ID的列表拷贝出来,弄个临时表。
如何优化临时表,我想了两种方法,但是目前还没有做
首先坐标可以转成GEOHASH,然后N公里可以再定向之后用过滤的方式计算
有几点需要注意:
过滤并不一定很慢,在数据库中用索引反而更慢的例子,这个CASE最适合的是过滤
我们的检索服务在启动的时候会全量加载数据库中的广告,构建广告的正排数据和倒排索引数据,多个副本通过消息通知维护与数据库的一致
通过订阅redis的channel来获取消息,消息类型包括,下线广告主的广告,同步推广计划,同步广告,同步素材,重新全量加载数据
在重新加载数据的时候,线上服务还要继续运行,所以我们使用了引用替换的方式,为了保证全量加载是有足够内存,内存只能使用1/2
消息机制有可能会不可靠,每个小时检索服务与数据库重新全量同步一次
每个广告素材有一个CTR,所以CTR的计算量是很大的,我们的CTR计算采用异步的方式,当查询一个广告素材的CTR缓存中没有的时候,就返回默认CTR,然后异步计算好这个素材的CTR填充到缓存中
计算方式 CPC广告
CPM广告
面临问题
ADX发送广告请求是到外网,并且请求量大,在100ms以内返回,某一家DSP超时不能对整体的广告检索有影响
我们针对这些问题做了如下几方面的控制
针对ADX还需要加强的地方
接收到曝光点击以后在内存中做聚合,每分钟把聚合后信息推送到消息队列
计费服务从消息队列消费曝光服务聚合后的曝光点击,其中曝光是每分钟消费一次,点击是实时消费。
曝光的消费是两个线程协作,一个线程负责从消息队列拉数据然后聚合N台曝光服务推入的内容,一个线程负责消费。
计费服务一主一备,通过redis实现租约
redis中有一个叫lock的key,value是当前工作节点的名字,有一个过期时间。
主服务,每秒钟先get(lock),然后判断是否与自己当前节点名字一致,如果一致的话,使用expire方法进行续约
备服务,每秒钟尝先尝试写lock这个key为自己节点的名称,使用redis的setex方法,因为key已经存在,所以会返回false
因为计费经验做一些钱方面的检查,所以涉及浮点数精度问题
浮点数做减法使用BigDecimal.subtract()方法
浮点数做乘法使用BigDecimal.multiply()方法
浮点数做除法使用BigDecimal.divide()方法
标签:
原文地址:http://www.cnblogs.com/23lalala/p/5667919.html