前言
背景简介
最近做了一些对接某书的广告业务,主要还是根据自己业务的需求调用它的SDK从人家的服务器中获取源数据然后再做一下自己这边的业务逻辑的处理。
由于源数据不在我们本地,需要调用SDK从远端的服务器去获取数据,所以对于那些需要频繁调用接口获取的并且更新不是十分频繁数据我们可以考虑将它们在第一次获取到后缓存在本地,下一次再来获取这些数据的时候可以直接从缓存中获取,只要做好了缓存的一致性等措施,就可以避免由于网络延迟等原因导致的调用SDK超时而获取不到数据等等诸如此类的问题。
缓存数据库我这边选取了Redis,现在的业务中主要用到了Redis的string、list、与hash这三种数据类型,下面就为大家介绍一下笔者在本项目中使用Redis的一些思路。
注意事项
由于公司的业务代码不能外泄,所以本文只提供解决问题的思路,然后讲一下会遇到哪些坑以及解决这些坑的思路,给出的代码也都是伪代码,希望能为新手程序员提供一些解决实际问题的思路,当然欢迎老鸟们在下面评论区疯狂吐槽,指出问题对于笔者的提升来说帮助也很大。
SDK参考的资料
关于某书的SDK可以从这里找到其源码及文档说明:https://github.com/facebook/facebook-python-business-sdk
文档说明在这里(吐槽一下这文档做的真是***):https://developers.facebook.com/docs/marketing-apis
Redis初始化操作
python版本使用的是3.6.5,redis使用第三方的redis模块,需要手动安装一下:
pip3 install redis
然后使用的是redis连接池,每一个redis的client每次都从初始化好的连接池中获取连接。
这里建议大家将redis连接池这个变量放在统一的配置文件中,这样在其他模块中都从同一个配置文件中import这个变量时连接池就成了一个单例(基于模块导入的单例),实现了一个单例模式。
像下面这样:
config.py中:
import redis REDISES = { 'cache': { 'host': '127.0.0.1', 'port': 6379, 'max_connections': 300, 'db': 2, # 选择第2个db作为缓存的库 'password': '', }, } # redis连接池 FB_POOL = redis.ConnectionPool(**REDISES["cache"])
业务代码中client使用单例连接池:
import redis from config import FB_POOL redis_client = redis.Redis(connection_pool=FB_POOL)
Redis的使用之:string类型
string类型用起来十分简单,直接按照 key:value 的形式存储就可以了。
这里需要注意2点:1是如果存入redis的value是一个字典格式的格式化的数据,需要将这个字典序列化成json数据后才能存入redis;2是python中redis的set方法可以在存储key的时候设置key的过期时间。
我这里使用string类型存储了国家及语言信息,因为考虑到这样的数据改动不会很频繁,因此缓存同步的思路是设置redis中对应key的过期时间为7天,这样每7天请求一次SDK获取最新的数据更新缓存就可以了。
具体的伪代码如下:
def get_country_msg(account_id: str, result: dict) -> dict: # 如果redis中有的话从redis中获取 否则从接口中获取然后将数据写入redis中并设置生存时间下次从redis中获取 if redis_client.exists("fbadset:country_code_dic") and redis_client.exists("fbadset:country_group_area"): # print("redis中有language的数据!!!") ret_str = redis_client.get("fbadset:country_group_area") ret = json.loads(ret_str) result = {"data": ret, "code": 0, "message": "成功返回country信息"} return result # 先获取国家名称与编号的对应关系 ### 业务代码省略。。。 country_lst, err = operate_obj.get_country_msg() # 然后构建返回的数据 country_code_dic = get_country_code_dic(country_lst) # 获取 country_group 的信息构建数据 country_group_lst, err = operate_obj.get_country_msg(location_types="country_group") ret = defaultdict(dict) ### 业务代码省略。。。 ret = 构建数据 # 在redis中存一份 7天后失效 ret_str = json.dumps(ret) country_code_str = json.dumps(country_code_dic) # set值的同时设置超时时间 7天 redis_client.set("fbadset:country_code_dic", country_code_str, nx=True, ex=7 * 24 * 3600) redis_client.set("fbadset:country_group_area", ret_str, nx=True, ex=7 * 24 * 3600) result = {"data": ret, "code": 0, "message": "成功返回country信息"} return result
Redis的使用之:list类型
主要使用list构建了一个队列,并且使用其切片的特性进行分页。
实际业务中,我们需要从fb的后台获取大量的广告数据,而这些广告数据如果每次都通过调用SDK的方式获取的话效率十分低下,尤其是页码比较大的情况,实际测试发现打开一个页码比较大的数据时页面加载的时间竟然有半分多钟!这样用户的体验会非常差!
我这边优化的思路是:在请求第一页的数据时调用SDK接口获取数据,与此同时将获取到的所有数据构建成自己提前定义好的格式rpush到redis的队列中,然后再请求其他页面的时候从redis中使用lrange的方式根据前端传来的页码以及每页需要展示的数据切片获取数据并处理完毕后返回给前端展示。
特别注意:这里之所以要在请求第一页数据时调用SDK接口获取数据是为了保证缓存的一致性!如果用户在fb的后台创建了一条广告但是redis中没有及时更新的话展示的时候是一定会将新创建的广告漏掉的!
另外需要注意的是:redis的lrange方法切片的区间是:闭区间!!!
涉及到的伪代码如下:
# ################################## 账户下的所有ad ####################### page=1 size=3 fields = ["id","name","status","campaign_id","account_id"] ## 这里设置limit其实无实际意义了~因为后面都是从redis中获取的 # ret = ad_account.get_ad_sets(params={"limit":size})
# 业务代码 ret = ad_account.get_ads(params={"limit":10000},fields=fields) # print("ret<<<<",ret,type(ret),len(ret)) print("len<<<<<",len(ret)) # # 所有的数据 total_count = ret.total() # 只取1w条数据 if total_count > 10000: total_count = 10000 print("total_count>>>",total_count) #####存入redis的list中 # ret_lst = list() for i in range(total_count): dic = ret[i] print("dic>>>",dic) # 构建字典并序列化 存入list中 id_ = dic["id"] name = dic["name"] status = dic["status"] curr_dic = {"id":id_,"name":name,"status":status} insert_str = json.dumps(curr_dic) # print("insert_str>>>>",insert_str) # ret_lst.append(insert_str) # 将数据存在redis的list里面 # rpush
# redis_key 注意后面要加上账户标识:表明是哪个账户下的ad
r_key = "fbad:account_ads:{}".format(account_id) redis_client.rpush(r_key,insert_str) ## llen redis_count = redis_client.llen(r_key) print("redis_count>>>>",redis_count,type(redis_count)) ## lrange ———— 闭区间!!! redis_ret = redis_client.lrange(r_key,0,2) print("redis_ret>>>",redis_ret) for bit in redis_ret: # print(bit,type(bit)) res = json.loads(bit) print(res,type(res)) ## delete
# del_ret = redis_client.delete(r_key)
# print("del_ret.>>",del_ret)
Redis的使用之:hash类型
fb的广告的层级结构大体来讲有3层:Campaign、Adset与ad。这三个层级依次是包含的关系,也就是说,Campaign包含多个Adset,Adset包含多个ad。
而在Adset层级又包含许多受众audience。
实际业务中我们需要频繁从fb的后台拉取某一个账户下的受众信息,频繁通过SDK请求数据效率十分低下,并且这些受众信息在fb后台创建后基本不会变了。
所以我这里还是选择将账户下的受众信息缓存到本地,频繁的从redis中获取数据效率要远高于通过SDK获取。
之所以选择hash存储,是因为账户下的受众信息的结构可以抽象成下面这种数据结构:
"account_audiences" = { "audience_id1":{"id":"id1","name":"xxxx1","msg":{...}}, "audience_id2":{"id":"id2","name":"xxxx2","msg":{...}}, "audience_id3":{"id":"id3","name":"xxxx3","msg":{...}}, }
针对这种数据结构,我们只要定好hash外层的key,对于hash内部的key直接使用每一个audience的id就可以了!
这里专门写了一个demo为大家简单描述下业务实现的整体流程:
# -*- coding:utf-8 -*- import json import redis import requests from config import POOL # 导入连接池 # redis客户端 redis_client = redis.Redis(connection_pool=POOL,max_connections=1000) # 实际UA的token —— facebook_bm_ls_access_token # access_token = '' access_token = 'xxxxxx' # 初始化与实例化账户对象 # a = FacebookAdsApi.init(access_token=access_token) # ad_account = AdAccount('act_906284123078550') search_url = "https://graph.facebook.com/v8.0/act_{}".format("83xxxxx") # account_id params = { "access_token": access_token, # 只获取保存的受众的信息 —— 保存的受众中有targeting的数据 "fields": "saved_audiences.limit(300){id,name,targeting}", } ### 从接口中获取 audiences_lst err = "" resp = requests.get(search_url, params=params) print("resp>>>>>",resp) print("url>>>>",resp.url) if resp.status_code == 200: audiences_ret = resp.json() audiences_lst = resp.json().get("saved_audiences",{}).get("data",[]) print("audiences_lst>>>>",audiences_lst) else: print("err>>>>",resp.json()) audiences_lst = [] ### redis哈希的操作 ———— 外层hash的key audiences_key = "fb_adset:account_saved_audiences:{}".format("8333xxxxx") # 1、 往哈希里面写数据 if redis_client.exists(audiences_key): audiences_id_lst = redis_client.hkeys(audiences_key) print("audiences_id_lst>>>>>>",audiences_id_lst) # 假设用户不会修改原来的值,所以:对于之前存在的数据就不往redis里面写了 for dic in audiences_lst: id = dic["id"] # 如果接口获取的id不在redis里面就将这个结果写入对应的redis的字典中 # 注意redis里面的id是bytes类型的!需要转化一下再比较! id_bytes = id.encode("utf-8") if id_bytes not in audiences_id_lst: print("NONONONONONONONONONON") value = json.dumps(dic) # hash外层的key用前面定义好的(与账户关联),里层的key使用每个受众的id redis_client.hset(audiences_key,id,value) else: print("YESYESYESYES") continue else: print("redis中没有对应的audiences_key:{}".format(audiences_key)) # 将数据写入redis for dic in audiences_lst: id = dic["id"] value = json.dumps(dic) # key是id, redis_client.hset(audiences_key,id,value) # 单独为某一个key设置过期时间 redis_client.expire(audiences_key,24 * 3600) # 2、从哈希里边获取指定key(audienct_id)的数据 audience_id = "23845xxxxx" if redis_client.exists(audiences_key): # 因为redis里面的key是bytes类型的,需要将外面待比较的key编码后再与hkeys得到的结果进行比较!!! au_id_bytes = audience_id.encode("utf-8") # 注意hkeys拿到的list里面的key是bytes类型的! audience_id_lst = redis_client.hkeys(audiences_key) if au_id_bytes in audience_id_lst: audience_dic = redis_client.hget(audiences_key,audience_id) print("ret>>>",audience_dic) print("type>>>>",type(audience_dic)) audience_ret = json.loads(audience_dic) print("rr>>>>>",audience_ret)
可以给大家展示下实际redis库中的hash数据:
hkeys结果:
获取某一个受众id的数据:
后记
本文算是个人在实际中解决问题的一些笔记,希望能给新手程序员一些解决问题的思路,后续有更好的方案也会加进文章中去。