百万级数据脱敏方案设计(加速场)

S(情境)

在华为OD负责解决方案工作台加速场模块期间,我们接到一个需求:发布方案模板时,需要支持对模板内的敏感数据进行脱敏处理,且脱敏数据量的最大阈值能达到百万级,单条数据大小最多1KB。同时,脱敏任务和模块的正常业务功能运行在同一台服务器上,不能影响用户正常使用。

T(任务)

我的任务是设计一个脱敏方案,既要保证百万级数据能在合理时间内处理完成,又要防止因正则表达式不当引发安全风险(如ReDoS攻击),同时还要保证服务器运行期间不影响模块的正常功能。

A(行动)

首先,整体业务流程如下:

用户在前端点击”发布方案模板” → 系统将模板当前数据做一份快照 → 用户选择”先脱敏,再发布” → 后端将方案ID封装成消息发送到Kafka → 立即返回前端”脱敏任务已提交” → 消费者从Kafka异步拉取方案ID → 根据方案ID查询阈值达百万级的脱敏数据 → 按每批1000条的粒度分页查询数据 → 提交到独立的脱敏线程池并行处理 → 每处理完一批更新数据库中的任务状态 → 所有批次处理完成后更新任务状态为”已完成” → 通过WebSocket通知前端”脱敏完成,可以发布了” → 用户确认后,将脱敏后的快照数据发布为正式模板。

基于这个流程,我从四个维度来设计方案:

1. 流量削峰——引入消息队列。 多人同时发布多个模板时,脱敏任务可能瞬间涌入。我引入Kafka作为任务缓冲层:用户提交脱敏请求后,只将方案ID封装成消息发送到Kafka,消息体极小,几乎不占资源,立即返回前端”脱敏任务已提交”。消费者每次从Kafka拉取1个方案ID,处理完成后再拉取下一个,天然实现背压。消费者拉取到方案ID后立即手动commit偏移量,再将任务提交到线程池,避免脱敏处理时间过长导致Kafka消费超时触发rebalance。脱敏任务的处理状态通过数据库持久化跟踪,即使消费者重启,也能从断点处恢复。

数据库状态跟踪设计: 创建脱敏任务表,核心字段包括:任务状态(0-待处理、1-处理中、2-已完成、3-失败)、总数据条数、已处理条数、当前批次序号。用户提交脱敏时插入一条status=0的记录;消费者拉取到任务后更新为status=1;每处理完一批数据,更新已处理条数和批次序号;全部完成后更新为status=2;异常且重试3次仍失败则更新为status=3并记录错误信息。消费者重启时,扫描status=1的任务,根据批次序号和已处理条数确定断点,从断点处继续处理,保证幂等性。

2. 性能维度——单模板串行 + 分片并行。 服务器运行期间,同一时间只处理1个模板的脱敏任务,避免多个模板争抢资源影响业务功能。脱敏任务使用独立的线程池,与业务线程池完全隔离。消费者从Kafka拉取到方案ID后,先查询脱敏数据的总条数,然后按每批1000条的粒度分页查询,每次只加载1000条数据到内存,避免OOM。将每批数据作为一个子任务提交到脱敏线程池并行处理。线程池配置:8核服务器,核心线程数4(CPU×0.5),最大线程数8(CPU×1),有界队列容量1000,拒绝策略用CallerRunsPolicy做反压。4个核心线程并行处理,每秒可完成约4000-8000条数据的脱敏,百万级数据整体耗时约2-4分钟。

3. 安全维度——ReDoS防护 + 多层阈值兜底。 脱敏核心逻辑依赖正则匹配,百万级数据中如果存在恶意构造的输入,可能导致正则引擎灾难性回溯。我采取了以下措施:

  • 用确定性正则替代回溯型正则,如用1[3-9]\d{9}替代1[3-9][0-9]+,避免嵌套量词和模糊匹配
  • 设置单条匹配超时1s,通过Future.get(timeout)控制,超时则跳过并记录异常
  • 设置单批次处理超时5分钟,每批1000条数据超时未完成则触发告警
  • 设置整个模板脱敏超时30分钟,百万级数据正常2-4分钟,超时则自动终止并通知运维
  • 设置单条数据大小上限10KB,超过则截断或跳过,防止超大文本导致内存溢出
  • 设置脱敏失败重试3次,超过则跳过并记录异常日志

4. 可运维维度——脱敏对照表。 脱敏前对原始数据做哈希映射,生成加密存储的对照表,方便线上排查问题时可申请权限追溯原始数据。

R(结果)

最终方案落地后,用户提交脱敏任务即可离开,脱敏完成后收到通知再回来确认发布,体验流畅。单个模板百万级数据的脱敏处理时间控制在2-4分钟,且脱敏期间模块的正常业务功能未受到影响,未出现用户操作卡顿或接口超时的情况。通过Kafka削峰,多人同时提交脱敏任务时,后端也能稳定处理。正则安全防护上线后,未发生过因脱敏导致的CPU飙升或服务异常。这套方案后来也被复用到其他需要批量数据处理的场景中。

二、追问问题汇总

线程池问题

序号 追问问题 考察点 建议回答方向
1 线程池的参数配置
2 线程池参数为什么设4和8? 资源分配意识 8核服务器,业务功能预留一半资源,脱敏作为后台任务优先级较低,4核足够支撑百万级数据在几分钟内处理完
1 线程池的核心参数有哪些?分别有什么用? 是否真正用过线程池 corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、RejectedExecutionHandler
2 线程池的提交流程是怎样的? 线程池工作原理 核心线程 → 工作队列 → 最大线程 → 拒绝策略
3 corePoolSize和maximumPoolSize设多少合适?依据是什么? 参数设计的合理性 CPU密集型:N+1;IO密集型:2N;混合型:根据实际压测调整。结合您的场景:脱敏是CPU+IO混合型,8核服务器设4和8
4 有界队列和无界队列怎么选? 队列选型意识 有界队列:防止内存溢出,任务超过队列容量触发拒绝策略。无界队列:可能导致内存无限增长。生产环境推荐有界队列
5 拒绝策略有哪些?你们用的哪个? 是否了解拒绝策略 AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢弃最旧)。您的场景用CallerRunsPolicy做反压
6 你们脱敏线程池的核心线程数为什么设4而不是8? 资源分配意识 8核服务器还要跑业务功能,脱敏是后台任务优先级较低,预留一半资源给业务。4核并行处理每秒可完成4000-8000条,百万级数据2-4分钟,已经满足需求
7 线程池队列容量为什么设1000?依据是什么? 参数设计的合理性 每批1000条数据,队列容量1000意味着最多积压100万条待处理数据,内存可控。同时结合CallerRunsPolicy,队列满了由提交线程自己处理,起到反压效果
8 脱敏线程池和业务线程池怎么隔离的? 资源隔离意识 两个独立的ThreadPoolExecutor实例,使用不同的线程工厂和线程组,互不干扰。脱敏线程池满不会影响业务接口的响应
9 线程池的参数是写死的还是可动态调整的? 运维和调优意识 通过配置中心(如Nacos、Apollo)动态调整,白天高峰期核心线程数保持4,夜间低峰期调大到8或16加速处理积压任务
10 线程池的线程名前缀怎么设置的?有什么用? 运维意识 设置自定义线程名前缀,如”desensitization-pool-“,方便线上排查问题时通过jstack快速定位线程归属
11 线上怎么查看线程池的运行状态? 监控和排查能力 ThreadPoolExecutor提供了getPoolSize()、getActiveCount()、getQueue().size()、getCompletedTaskCount()等方法,可以暴露成Metrics接入监控系统
12 如果线程池的任务队列一直增长,怎么排查? 线上问题排查能力 先看消费者是否正常拉取消息 → 再看线程池活跃线程数是否达到上限 → 检查任务处理是否有阻塞(如数据库慢查询、远程调用超时)→ 查看是否有死锁
13 线程池中的任务抛出异常会怎样? 异常处理意识 如果任务没有捕获异常,线程会退出,线程池会新建一个线程替代它。异常信息会丢失。建议在任务中自行捕获异常并记录日志,或者设置UncaughtExceptionHandler
14 怎么监控线程池是否过载? 监控意识 监控活跃线程数、队列积压量、任务拒绝次数。设置告警阈值:队列使用率超过80%告警,拒绝次数大于0立即告警
15 jstack命令用过吗?怎么分析线程堆栈? 线上问题排查能力 jstack pid查看所有线程堆栈,搜索线程名前缀定位目标线程,查看WAITING/BLOCKED状态判断是否有死锁或长时间阻塞
16 为什么用线程池而不是直接new Thread? 线程池的价值认知 线程池复用线程、控制并发数、减少创建销毁开销、提供队列缓冲和拒绝策略。直接new Thread在高并发下会导致系统资源耗尽
17 脱敏任务为什么分片后还要并行处理?直接单线程不行吗? 性能优化意识 百万级数据单线程处理约13分钟,16线程并行可缩短到1分钟以内。分片并行是在资源可控的前提下最大化利用CPU
18 如果脱敏任务中有一个批次一直处理不完,会阻塞后面的批次吗? 并发设计意识 不会。每个批次是独立的子任务提交到线程池,一个批次卡住不会影响其他批次。但需要通过超时机制兜底,防止该批次一直占用线程资源
19 CallerRunsPolicy在你们场景里具体怎么工作的? 反压机制的理解 当线程池队列满了,提交任务的线程(消费者线程)会自己执行脱敏任务。此时消费者线程被占用,无法拉取新的Kafka消息,自然实现了背压——让上游慢下来
20 如果业务流量暴增,脱敏线程池会不会影响业务线程池? 资源隔离的可靠性 两个线程池完全隔离,脱敏线程池即使满负荷运行,也不会占用业务线程池的线程。最坏情况下脱敏变慢,但业务接口不受影响
21 ThreadLocal用过吗?有什么坑? 并发工具的理解 用于线程上下文传递。坑:线程池复用线程,如果不及时清理,下一个任务会读到上一个任务的数据。解决方案:每次使用后调用remove()
22 synchronized和ReentrantLock怎么选? 锁的理解 synchronized:简单、自动释放、适合低竞争场景。ReentrantLock:可中断、可超时、支持公平锁、支持多个Condition、适合高竞争或复杂同步场景
23 volatile和AtomicInteger的区别? 并发原语的理解 volatile保证可见性和有序性,但不保证原子性。AtomicInteger通过CAS保证原子性,适合计数器等场景
24 你们遇到过死锁吗?怎么排查和解决的? 实战经验 jstack分析线程堆栈,搜索”deadlock”关键字。解决:统一锁的获取顺序、减少锁的持有时间、使用tryLock加超时避免无限等待
25 脱敏任务分片后提交到线程池,怎么知道所有批次都处理完了? 使用CountDownLatch或CompletableFuture.allOf()等待所有批次完成,或者每处理完一批更新数据库中的processed_count,轮询判断是否全部完成
26 如果某个批次处理失败了,其他批次已经处理完了,怎么处理? 记录失败批次的偏移量,继续处理后续批次。全部批次处理完后,统一重试失败的批次。重试3次仍失败,标记任务为”失败”,通知人工介入
27 线程池中的任务可以取消吗? 可以。Future.cancel(true)尝试取消正在执行的任务,但依赖任务本身对中断信号的响应。如果任务没有处理中断信号,取消可能无效

Kafka问题

序号 追问问题 考察点 建议回答方向
6 立即commit后消费者挂了,任务会不会丢? 可靠性设计 不会丢。任务状态已持久化到数据库,消费者重启后扫描status=1的任务,从断点处继续处理
7 怎么保证重复处理不会导致数据异常? 幂等性设计 每个批次处理前先检查该批次是否已处理完成,已完成的批次跳过。脱敏操作本身是幂等的——同一份数据脱敏两次结果一致
8 如果Kafka挂了怎么办? 消息队列的高可用意识 Kafka有副本机制和持久化,配合消费者手动提交offset,重启后可继续消费未处理的消息
12 为什么用Kafka不用RabbitMQ? 中间件选型能力 Kafka更适合削峰填谷和海量消息堆积的场景,吞吐量更高,持久化能力更强
1 Kafka为什么吞吐量高? 是否理解Kafka的核心设计 顺序写磁盘、零拷贝(sendfile)、页缓存、批量压缩、分区并行
2 Kafka怎么保证消息不丢失? 生产者和消费者的可靠性配置 生产者ACK=all(-1)、副本数≥2、min.insync.replicas、消费者手动提交offset
3 Kafka怎么保证消息顺序消费? 顺序性的实现方式 同一个业务key路由到同一个分区,一个分区只能被同一个消费者组内的一个消费者消费
4 Kafka分区数和消费者数的关系? 并行消费的理解 一个分区只能被一个消费者消费;消费者数超过分区数时,多余的消费者闲置
5 Kafka的ISR是什么?有什么用? 副本同步机制 In-Sync Replicas,与Leader保持同步的副本集合,用于Leader选举和消息可靠性保证
6 Kafka的Leader选举机制是怎样的? 高可用理解 从ISR中选举,优先选择与Leader差距最小的副本作为新Leader
7 你们Topic设置了多少个分区?依据是什么? 分区数设计的合理性 同一时间只处理1个模板,2-4个分区足够。分区数太多增加Leader选举和文件句柄开销
8 消费者手动提交offset,commit频率怎么控制? 是否真正用过手动提交 拉取到方案ID后立即commit,保证”至少一次”语义,配合数据库状态做幂等处理
9 如果消费者处理消息时抛异常了,offset怎么处理? 异常处理意识 不commit,下次重启时重新消费;或者捕获异常后记录到死信队列,手动补偿
10 Kafka消息积压了怎么排查和解决? 线上问题排查能力 先看积压在哪个分区、消费者是否正常、是否有慢消费;临时方案增加消费者或扩大分区;根治优化消费逻辑
11 Kafka的rebalance你遇到过吗?什么原因导致的? 是否遇到过生产问题 消费者加入或退出、session超时、max.poll.interval.ms超时。rebalance期间所有消费者暂停消费,影响吞吐量
12 max.poll.interval.ms和session.timeout.ms有什么区别? 对Kafka超时机制的理解 max.poll.interval.ms控制两次poll的最大间隔,超时认为消费者卡死;session.timeout.ms控制心跳超时,超时认为消费者挂了
13 为什么选Kafka而不是RocketMQ或RabbitMQ? 中间件选型能力 Kafka吞吐量最高、消息堆积能力强、适合削峰填谷。脱敏场景需要海量消息堆积和削峰,Kafka最合适
14 如果脱敏任务对实时性有要求,Kafka还合适吗? 对Kafka局限性的认知 Kafka实时性不如RabbitMQ或RocketMQ,消息是批量拉取的,延迟在毫秒到秒级。如果要求秒级响应,可考虑RocketMQ
15 Kafka消息体为什么只放方案ID,而不是全量数据? 消息体设计的合理性 方案ID只有几十字节,消息体小,传输和序列化开销低,Kafka吞吐量更高。全量数据可能几MB,会拖慢Kafka读写性能
16 如果Kafka集群挂了,怎么保证不丢任务? 高可用设计 Kafka集群有副本机制和ISR,配合生产者ACK=all保证消息不丢。同时数据库中有任务状态兜底,即使Kafka不可用,也可以从数据库中恢复未处理的任务
17 消费者每次只拉取1个方案ID,那Kafka多分区的优势是不是浪费了? 场景适配能力 不浪费。多分区仍然提供了消息的分布式存储和故障隔离能力。虽然当前场景同一时间只处理1个,但多个分区可以承载不同模板的消息,消费者重启或故障时也能快速切换
18 立即commit后消费者挂了,任务会不会丢? 可靠性设计 不会丢。任务状态已持久化到数据库,消费者重启后扫描status=1的任务,从断点处继续处理

正则问题

序号 追问问题 考察点 建议回答方向
3 正则超时在Java里具体怎么实现? 是否真正落地过 Future.get(timeout)控制超时,超时则中断任务并记录异常日志
4 单条匹配超时为什么设1s?依据是什么? 参数设计的合理性 1KB文本跑3-5条正则,正常耗时在0.5-1ms,1s是非常保守的上限,足以区分正常数据和恶意构造的输入
11 单条脱敏0.5-1ms这个数据是怎么来的? 量化数据是否经得起推敲 1KB文本跑3-5条正则,基于本地测试和线上压测的综合估算
13 如果某个模板的脱敏任务一直失败怎么办? 异常处理意识 设置重试机制(最多3次),超过重试次数后标记为”脱敏异常”,通知人工介入处理
14 单条数据大小上限10KB,如果业务上确实需要处理更大的数据怎么办? 边界情况处理 可以做成可配置项,根据业务需求动态调整,但需要同步评估对内存和性能的影响

其他问题

序号 追问问题 考察点 建议回答方向
2 分页查询用OFFSET,百万级数据越往后越慢怎么办? 分页优化的实战经验 改用基于ID的区间查询:WHERE id > 上次最大id ORDER BY id LIMIT 1000,走索引,性能稳定
9 同一时间只处理1个模板,如果多个用户同时提交,后面的要等多久? 用户体验意识 单个模板2-4分钟,用户提交后即可离开,脱敏完成后通过WebSocket通知,不需要在页面等待
10 WebSocket通知如果客户端断连了怎么办? 消息可达性意识 增加兜底机制:脱敏完成后同时发送邮件或站内信,用户下次登录时也能看到通知

百万级数据脱敏方案设计(加速场)
https://lijinhongpassion.github.io/2026/06/46284d0d6084.html
作者
快楽星球家
发布于
2026年6月24日
许可协议