算法学习到底有没有用?一次亿级人像抓拍红名单过滤的真实业务实践

张开发
2026/6/11 19:44:41 15 分钟阅读
算法学习到底有没有用?一次亿级人像抓拍红名单过滤的真实业务实践
算法学习到底有没有用一次亿级人像抓拍红名单过滤的真实业务实践文章目录算法学习到底有没有用一次亿级人像抓拍红名单过滤的真实业务实践业务背景每天 1 亿张人像抓拍难点这不是一个简单的 if 判断分桶算法方案对比按 人员 or 设备 分桶按红名单人员进行分桶也考虑过的方案按设备分桶最终方案按时间分桶查询流程为什么选择 Redis Set封装成 SDK而不是每个模块自己写这个方案背后的算法思维1. 哈希思想2. 集合运算3. 分桶思想4. 复杂度意识一个容易忽略的点方案要服务于查询模式算法学习带来的影响总结很多人学习算法的时候都会有一个疑问我平时写业务代码真的用得上这些算法和数据结构吗如果只是从刷题的角度看算法好像离业务很远。二分、哈希、集合、分桶、复杂度这些概念看起来更像面试题而不是每天写增删改查时会直接碰到的东西。但我在一个真实项目里遇到过一个场景让我对这件事有了很深的感受算法学习带来的价值不一定是让你在业务代码里手写一个复杂算法而是让你在面对大数据量、高频查询、权限过滤这类问题时能自然地用更合适的数据结构和计算方式去设计系统。这篇文章就用一个真实业务场景聊聊算法思维在项目里的作用。业务背景每天 1 亿张人像抓拍系统每天会产生大约1 亿张人像抓拍图。算法服务会对每一张抓拍进行识别判断这张抓拍具体对应哪个人的身份信息。也就是说一张抓拍图不仅有自己的抓拍 ID、抓拍时间、抓拍地点还会关联到某个具体人员。后来客户提出了一个权限需求有一些重要人员不希望普通使用系统的人看到。无论是在抓拍查询、相册查询还是类似的人像检索功能里只要命中了这些重要人员都要自动过滤掉。这个功能在业务上叫红名单。加入红名单的人像库在系统查询抓拍数据时需要被自动排除。用户不应该感知到这些数据的存在。看起来这个需求很简单查询结果里如果有人属于红名单就过滤掉。但真正落到系统里问题就没有这么简单了。难点这不是一个简单的 if 判断如果数据量很小我们当然可以这么做查询抓拍列表。遍历每一条抓拍。判断抓拍对应人员是否属于红名单通过 1:N 底库匹配。属于红名单就过滤掉。但是在这个场景里每天有 1 亿张抓拍。用户查询可能按时间范围、设备、区域、人员特征等条件组合检索。系统里也不止一个模块会用到这类过滤能力例如capture抓拍模块album相册模块profile档案模块其他基于人像抓拍的业务模块如果每个模块都自己写一套红名单过滤逻辑不仅重复而且很容易出现不一致。更关键的是如果每次查询都去数据库里判断抓拍对应人员是否属于红名单性能压力会很大。这个判断看似只是一次权限校验但在高频查询和大数据量背景下它会变成系统瓶颈。所以这个问题的本质不是“加一个字段判断”而是如何在亿级数据规模下快速判断一批抓拍 ID 中哪些需要被红名单过滤这个时候算法和数据结构的思维就开始发挥作用了。分桶算法方案对比按 人员 or 设备 分桶对比演示数据倾斜/不均匀以人员/设备分桶为例SADDSADDSADDred_target_热门IDID_001ID_002ID_003ID_004ID_005ID_006... (数据量巨大)red_target_普通IDID_099red_target_冷门ID(无数据)痛点1数据倾斜严重热门设备/人员产生的ID数量远超普通节点痛点2Redis大Key风险单个Set过大会影响查询性能和内存分布痛点3运维成本高桶大小不可控难以预估存储空间按红名单人员进行分桶比如某个人是红名单人员就给这个人维护一个桶桶里放这个人对应的所有抓拍 ID。示意如下red_person_10001 - {captureId1, captureId2, captureId3, ...} red_person_10002 - {captureId4, captureId5, captureId6, ...} red_person_10003 - {captureId7, captureId8, captureId9, ...}这个方案从业务语义上看是很自然的红名单是人所以按人组织数据。但深入想一下会发现它有几个问题。首先不同人的抓拍数量差异可能非常大。有的人一天可能只出现几次有的人可能在多个摄像头下频繁出现。这样会导致桶的大小非常不均匀。其次用户查询通常不是按某一个红名单人员查而是按时间范围查抓拍。例如查最近 10 天、某个区域、某些设备下的抓拍数据。按人分桶以后查询时很难快速知道应该访问哪些人员桶。最后如果红名单人员数量比较多查询时可能需要扫描很多桶再把它们合并起来这会让过滤逻辑变得复杂。所以这个方案虽然直观但不适合这个场景。它的问题不在于不能实现而在于数据组织方式和查询方式不匹配。也考虑过的方案按设备分桶除了按红名单人员分桶我当时还考虑过另一种方案按设备分桶。因为抓拍图都是由摄像头或采集设备产生的所以也可以给每个设备维护一个红名单抓拍桶。示意如下red_device_10001 - {captureId1, captureId2, captureId3, ...} red_device_10002 - {captureId4, captureId5, captureId6, ...} red_device_10003 - {captureId7, captureId8, captureId9, ...}如果用户查询时经常指定设备这个方案看起来也很合理。比如用户只查某几个摄像头下的抓拍那理论上只需要访问这几个设备对应的红名单桶。但这个方案最后也被否掉了原因还是不均匀。不同设备的抓拍量差异非常大。有些摄像头部署在出入口、人流密集区域一天可能产生大量抓拍有些摄像头部署在低频区域可能很长时间都没有多少数据。这样一来设备桶的大小会严重不均衡red_device_gate_001 - 几百万条 red_device_corner_021 - 几千条 red_device_unused_099 - 几十条桶大小不均匀会带来几个问题热点设备桶会变得很大Redis 单 key 压力更高。查询命中热点设备时集合运算成本会明显上升。系统性能会被少数高频设备拖累整体表现不稳定。所以按设备分桶虽然比按人员分桶更贴近抓拍来源但它仍然不适合作为这个场景下的主分桶方式。它解决了“按设备查”的问题却引入了“桶大小不均”和“热点 key”的问题。最终方案按时间分桶最终落地方案按时间分桶 (Redis Set)SADDSADDSADDred_capture_20260101_xxxx.captureIDxx.captureIDxx.captureIDred_capture_20260102_xxxx.captureIDxx.captureIDxx.captureIDred_capture_20260103_xxxx.captureIDxx.captureIDxx.captureID优点1桶大小相对均匀优点2与用户带时间范围的查询天然匹配优点3过滤范围可控查几天找几天后来我采用了另一个方案按时间生成红名单桶。系统每天生成一个红名单桶把当天符合红名单条件的抓拍 ID 放进去。桶使用 Redis 的Set集合来存储。例如red_capture_20260101 - {captureId1, captureId2, captureId3, ...} red_capture_20260102 - {captureId4, captureId5, captureId6, ...} red_capture_20260103 - {captureId7, captureId8, captureId9, ...}如果有业务隔离、租户隔离或库隔离也可以把桶名称设计得更细一点red_capture_20260101_xx red_capture_20260102_xx red_capture_20260103_xx这里的xx可以代表业务标识、库标识或租户标识。这个设计有几个好处。第一桶大小相对更均匀。因为系统每天产生的抓拍量虽然有波动但整体比“按人”分桶更稳定。第二查询条件天然包含时间范围。用户查抓拍时一般都会带开始时间和结束时间。通过时间范围我们可以很容易推导出需要访问哪些桶。第三过滤范围可控。比如用户查 10 天的数据我们只需要找到这 10 天对应的红名单桶而不是扫描所有红名单人员。这其实就是一种很典型的分桶思想根据查询模式来设计数据分布而不是只根据业务对象来设计数据分布。查询流程假设用户查询 2026-01-01 到 2026-01-10 的抓拍数据。系统原始查询条件可能先查出 200 条抓拍结果queryResult {c1, c2, c3, c4, ..., c200}根据时间范围可以得到 10 个红名单桶red_capture_20260101_xx red_capture_20260102_xx red_capture_20260103_xx ... red_capture_20260110_xx接下来要做的事情就是把这 10 个桶里的红名单抓拍 ID 合并起来。判断原始查询结果里哪些 ID 命中了红名单。把命中的抓拍从最终结果中排除。从集合运算角度看就是redSet union(bucket1, bucket2, ..., bucket10) hitRedSet intersection(queryResult, redSet) finalResult queryResult - hitRedSet这就是 RedisSet非常适合的地方。为什么选择 Redis SetRedis 的Set本质上适合做去重、成员判断、集合交集、集合并集、集合差集等操作。在这个场景里我们关心的是抓拍 ID 是否命中红名单。抓拍 ID 天然适合作为集合元素。常用命令包括SADD 添加红名单抓拍 ID SISMEMBER 判断某个抓拍 ID 是否在集合中 SUNION 对多个集合求并集 SINTER 对多个集合求交集 SDIFF 对集合求差集如果只判断单条数据可以用SISMEMBER。如果是一批查询结果就更适合集合运算。比如把查询得到的 200 个抓拍 ID 临时放入一个 Set再和红名单桶做交集就能快速得到需要过滤的 ID。伪代码如下SetStringqueryCaptureIdsqueryCaptureIds(condition);ListStringbucketKeysbuildRedBucketKeys(condition.getStartTime(),condition.getEndTime());SetStringredCaptureIdsredis.sunion(bucketKeys);SetStringhitRedCaptureIdsintersection(queryCaptureIds,redCaptureIds);ListCapturefinalResultqueryResult.stream().filter(capture-!hitRedCaptureIds.contains(capture.getId())).collect(Collectors.toList());如果希望尽量把计算下推给 Redis也可以把本次查询结果写入临时集合然后使用 Redis 的交集命令StringqueryTempKeytmp:query:requestId;redis.sadd(queryTempKey,queryCaptureIds);redis.expire(queryTempKey,60);SetStringredBucketKeysbuildRedBucketKeys(startTime,endTime);StringredUnionTempKeytmp:red_union:requestId;redis.sunionstore(redUnionTempKey,redBucketKeys);redis.expire(redUnionTempKey,60);SetStringhitRedCaptureIdsredis.sinter(queryTempKey,redUnionTempKey);returnremoveHitRedCaptures(queryResult,hitRedCaptureIds);这个流程里Redis 负责完成集合并集和交集运算业务系统只需要根据命中结果做最终过滤。封装成 SDK而不是每个模块自己写这个能力后来没有只写在某一个业务模块里而是封装成了一个 SDK。因为红名单过滤不是capture抓拍模块独有的能力。album相册模块、profile档案模块以及其他基于抓拍数据的子系统都可能需要用到同样的过滤能力。所以更合理的做法是把它抽象成一个公共能力publicinterfaceRedListFilterSdk{SetStringfindHitRedCaptures(CollectionStringcaptureIds,TimeRangetimeRange,StringbizCode);ListCapturefilterCaptures(ListCapturecaptures,TimeRangetimeRange,StringbizCode);}子系统调用时不需要关心 Redis 里有多少个桶也不需要关心使用了SUNION还是SINTER。它只需要传入当前查询结果、时间范围和业务标识ListCapturecapturescaptureService.query(condition);ListCapturevisibleCapturesredListFilterSdk.filterCaptures(captures,condition.getTimeRange(),condition.getBizCode());SDK 内部负责根据时间范围生成桶 key。合并多个红名单桶。和本次查询结果取交集。返回命中的红名单抓拍 ID或直接返回过滤后的结果。这样设计的好处是红名单策略如果以后变化只需要改 SDK 内部实现不需要每个业务模块都跟着改。这个方案背后的算法思维这个需求最后看起来只是用了 Redis Set但背后其实包含了很多算法学习里常见的思想。1. 哈希思想Redis Set 的成员判断通常可以做到很高效本质上就是利用哈希结构快速判断一个元素是否存在。如果没有学过哈希表我们可能会习惯性地用 List 遍历判断for(StringredId:redIds){if(redId.equals(captureId)){returntrue;}}这种方式在数据量小的时候没问题但数据量一大性能就会明显下降。而 Set 的思路是我不逐个找我直接通过哈希定位。这就是数据结构选择带来的差异。2. 集合运算红名单过滤本质上不是复杂业务判断而是集合关系查询结果集合 ∩ 红名单集合 需要过滤的集合然后查询结果集合 - 需要过滤的集合 最终可见集合当你能把业务问题抽象成集合问题时解决方案会一下子变得清晰。这也是算法学习很重要的价值它让你看到问题背后的数学结构。3. 分桶思想按天生成红名单桶就是典型的分桶。分桶的目的不是为了让系统更复杂而是为了把一个巨大的集合拆成多个更容易管理的小集合。关键在于桶的设计要和查询模式匹配。这个场景里用户查询天然带时间范围所以按时间分桶比按人员分桶更合适。4. 复杂度意识如果每次查询都扫描全部红名单数据随着数据增长系统性能会越来越差。按时间分桶以后用户查 10 天就只访问 10 个桶。用户查 3 天就只访问 3 个桶。系统的计算范围由查询条件控制而不是被总数据量拖着走。这就是复杂度意识在工程里的体现。一个容易忽略的点方案要服务于查询模式这次设计里我最大的感受是数据怎么存不应该只看业务对象是什么还要看系统将来怎么查。红名单从业务对象上看是“人”所以按人分桶很自然。但实际查询时用户更多是按时间范围查抓拍。最终要过滤的对象也不是人而是抓拍 ID。所以真正适合的组织方式是时间范围 - 红名单抓拍 ID 集合 - 与查询结果做集合运算这就是为什么最终选择按时间分桶而不是按红名单人员分桶。一个好的系统设计往往不是选择最直观的模型而是选择最适合访问路径的模型。算法学习带来的影响回到最开始的问题算法学习到底有没有用我的感受是非常有用。但它的作用不一定表现为你在项目里手写了某个排序算法也不一定是你把力扣原题搬进了业务代码。它更多体现在这些地方看到“是否存在”会想到哈希和 Set。看到“多批数据合并”会想到并集。看到“共同命中”会想到交集。看到“排除一部分数据”会想到差集。看到“大集合不好处理”会想到分桶。看到“查询范围可控”会想到用查询条件缩小计算规模。这些东西看起来基础但在真实系统里非常重要。很多业务问题表面上是产品需求底层其实是数据结构问题。你是否学过算法差别就在于面对同一个需求有的人会写出一堆循环和数据库查询有的人会先把问题抽象成集合、映射、分桶和复杂度。总结这个红名单过滤需求最终落地的方案并不复杂每天生成一个红名单抓拍桶。使用 Redis Set 存储红名单抓拍 ID。查询时根据时间范围找到对应桶。使用 Redis 集合并集、交集能力找到命中的红名单抓拍。从原始查询结果中排除命中数据。将能力封装成 SDK供album、capture、profile等模块复用。但这个方案背后体现的是算法思维用合适的数据结构组织数据用集合运算表达业务关系用分桶控制数据规模用复杂度意识保护系统性能。所以算法学习不是只为了面试。真正进入项目以后你会发现很多看似普通的业务需求最后拼的不是代码写得多快而是你能不能把问题抽象对能不能选对数据结构能不能让系统在数据量上来以后依然扛得住。这就是算法学习给工程实践带来的影响。

更多文章