Python技术迷

某HR:有人说boss上很多公司都把年龄要求改成了 40、45岁,证明大龄打工人就业环境开始变好,但真相是招个销售、工人都在卡年龄

刚刷到这个HR的说法,我第一反应就是:这也太会看表面了吧。

Boss上年龄那栏改成40、45,看着是挺温柔的,好像中年打工人终于能喘口气了。可你真去投一圈就知道,页面写得宽,筛人筛得狠。

很多岗位明面上不写35了,改成“经验丰富优先”“精力充沛”“能适应高强度节奏”,翻译一下,还是那套意思。更扎心的是,有些销售岗、普工岗,按理说不该那么挑年龄吧,结果一问还是卡得死死的。你四十出头,人家嘴上说考虑,实际简历可能都没点开。

Image

这事儿就像把门口牌子换了,里面保安还是那个保安。年龄限制不摆在台面上了,不代表它消失了,只是变得更委婉,更不好说破。

大龄打工人难的地方不是看不到岗位,是岗位看起来有,轮到你就没了。

算法题:批处理查询

10 万个 user_id 扔给接口查用户信息,线上直接把下游打抖了。

日志里很难看:

fetch user timeout, uid=839102, cost=3012ms
fetch user timeout, uid=839103, cost=3008ms
fetch user timeout, uid=839104, cost=3021ms

这种问题我第一眼一般不看业务逻辑,先看调用方式。一个一个查,代码最顺手,线上也最容易死。所谓批处理查询,说穿了就是把零散查询攒成一批,减少调用次数,同时还要处理去重、顺序、缺失值。

比如输入一堆用户 id:

ids = [7, 3, 7, 9, 2, 3]

接口一次最多查 3 个:

defquery_users(batch):
    fake_db = {
2: "Tom",
3: "Lucy",
7: "Jack",
9: "Rose",
    }
return {uid: fake_db.get(uid) for uid in batch}

这里最容易写歪的地方,是直接按原数组切片:

[7, 3, 7]
[9, 2, 3]

看着是批量了,其实重复 id 还在查。量小没事,量一大就是白白浪费 QPS。

我一般会先去重,但不能把原顺序弄丢:

defbatch_query(ids, batch_size, query_func):
if batch_size <= 0:
raise ValueError("batch_size 不能小于等于 0")

    seen = set()
    unique_ids = []

for uid in ids:
if uid in seen:
continue
        seen.add(uid)
        unique_ids.append(uid)

    cache = {}

for i in range(0, len(unique_ids), batch_size):
        batch = unique_ids[i:i + batch_size]
        result = query_func(batch)

for uid in batch:
            cache[uid] = result.get(uid)

return [cache.get(uid) for uid in ids]

跑一下:

print(batch_query(ids, 3, query_users))

输出:

['Jack', 'Lucy', 'Jack', 'Rose', 'Tom', 'Lucy']

这个写法有几个细节。

第一,去重只影响查询,不影响返回。原来第几个 id,最后还是第几个结果。这个在订单、用户、商品这种场景里很关键,不然上层还要重新对齐数据。

第二,缺失值不要偷偷丢掉。比如数据库里没有这个 id,就返回 None。我不太喜欢在底层函数里直接过滤掉,因为过滤以后调用方根本不知道哪个 id 没查到。

第三,batch_size 不要写死。线上接口限流、数据库压力、网络耗时都可能变,写死以后调参很麻烦。

如果是数据库查询,思路也一样:

defquery_by_db(batch):
    sql = "select id, name from user where id in %s"
    print("SQL:", sql, tuple(batch))

    rows = [
        (1, "A"),
        (2, "B"),
        (5, "E"),
    ]

return {uid: name for uid, name in rows}

真正麻烦的不是把循环写出来,而是别让批处理变成“假批处理”。

我见过一种代码,外面写了 batch,里面又循环单查:

for uid in batch:
    query_one(uid)

这种看着像优化,其实只是把问题藏深了一层。排查的时候看日志最明显,如果一批 100 个 id,SQL 还是刷了 100 条,那就不用继续猜了,肯定没批起来。

批处理查询不复杂,但它是很典型的工程题。写对了,接口调用次数从 N 次降到 N / batch_size 次;写歪了,只是多套了一层循环。线上差的就是这一层。