← 返回目录

风控系统实战 — 实时决策与规则引擎

规则引擎 特征计算 实时决策 误杀率 风控模型

机场安检 — 身份证=身份识别、行李X光=特征检测、安检门=规则匹配、人工复检=人工审核、黑名单=禁飞名单

🛂 业务全景 — 风控系统做什么

通俗类比:机场安检
你去机场坐飞机,过安检的流程就是风控系统的缩影——
身份证核验 = 身份识别,确认你是谁
行李X光扫描 = 特征检测,从行为中提取风控特征
安检门报警 = 规则匹配,触发风控规则就拦截
人工复检 = 人工审核,机器不确定的交给人判断
黑名单 = 禁飞名单,明知是坏人直接拒绝

风控系统的目标就是:在用户无感知的情况下,毫秒级判断一个请求是否安全。安全的放行,可疑的人工审核,危险的直接拒绝。
🔑 身份识别 = 实名认证 🔍 特征检测 = 行为分析 🚪 安检门 = 规则匹配
👮 人工复检 = 人工审核 🚫 黑名单 = 封禁名单
核心流程:请求从进入到出决策的完整链路
请求进入 特征计算 规则匹配 决策输出
通过 ✅ / 拒绝 ❌ / 人工审核 🔍 后监控
请求进入:用户登录、下订单、转账、提现等操作触发风控请求。就像旅客到安检口。

特征计算:从用户的历史行为、当前操作、设备信息中提取风控特征。就像X光扫描行李,看看里面有什么。

规则匹配:把特征输入规则引擎,匹配风控规则。就像安检门——如果触发了某条规则(如携带液体超过100ml),就报警。

决策输出:通过/拒绝/人工审核三种决策。就像安检结果——放行/扣留/复检。

后监控:对于"通过"的请求,后续持续监控。就像安检过了还会在候机厅巡逻,发现异常再处理。

🎯 核心挑战 — 风控系统的5座大山

挑战1:规则引擎 — 规则怎么存、怎么执行、怎么热更新?
类比:机场安检规则可以随时增加新的违禁品清单——今天发现新型危险品,明天就在安检规则中加上。风控规则也一样,运营人员在后台配置一条新规则,不需要重启服务就立即生效。
自定义规则引擎实现
// ====== 规则定义 — 所有规则存在数据库中,支持热加载 ====== @Data @TableName("risk_rule") public class RiskRule { private Long id; private String ruleCode; // 规则编码:R001 private String ruleName; // 规则名称:同IP高频请求 private String ruleType; // 规则类型:SIMPLE/COMPOSITE private String conditionExpr; // 条件表达式(MVEL语法) private String actionType; // 触发动作:REJECT/REVIEW/PASS private int priority; // 优先级(数值越小越先执行) private boolean enabled; // 是否启用 private String scene; // 适用场景:LOGIN/ORDER/TRANSFER private LocalDateTime expireAt; // 过期时间 } // ====== 规则引擎核心 — 加载+匹配+执行 ====== @Service public class RuleEngineService { // 规则缓存 — 本地缓存,热更新时刷新 private volatile List<RiskRule> ruleCache = new ArrayList<>(); @Autowired private RiskRuleMapper ruleMapper; /** * 加载规则 — 启动时加载 + MQ通知刷新 * 就像安检人员交接班 — 新来的要重新看一遍规则手册 */ @PostConstruct public void loadRules() { refreshRuleCache(); } public synchronized void refreshRuleCache() { List<RiskRule> rules = ruleMapper.selectEnabled(); // 按优先级排序 — 数值越小越先执行 rules.sort(Comparator.comparingInt(RiskRule::getPriority)); this.ruleCache = rules; log.info("规则刷新完成,共{}条规则", rules.size()); } /** * 执行规则匹配 * 就像安检门逐条检查 — 有一条触发就报警 * 返回第一个命中的规则(优先级最高的) */ public RuleMatchResult match(RiskContext context) { for (RiskRule rule : ruleCache) { // 检查场景是否匹配 if (!rule.getScene().equals(context.getScene())) { continue; } // 检查规则是否过期 if (rule.getExpireAt() != null && rule.getExpireAt().isBefore(LocalDateTime.now())) { continue; } // 执行条件表达式(MVEL引擎) try { Boolean hit = (Boolean) MVEL.eval( rule.getConditionExpr(), context.getFeatureMap() ); if (hit != null && hit) { return RuleMatchResult.hit(rule); } } catch (Exception e) { log.error("规则执行异常: ruleId={}, expr={}", rule.getId(), rule.getConditionExpr(), e); } } return RuleMatchResult.miss(); } } // ====== 规则热更新 — 运营在后台修改规则后,通知所有节点刷新 ====== @RestController @RequestMapping("/api/risk/rule") public class RiskRuleController { @Autowired private RiskRuleMapper ruleMapper; @Autowired private RocketMQTemplate mqTemplate; /** * 更新规则 — 保存到数据库 + 广播刷新通知 * 就像安检出新规 — 先发通知,所有安检口同步更新 */ @PutMapping("/{id}") public Result<Void> updateRule(@PathVariable Long id, @RequestBody RiskRuleDTO dto) { // 1. 更新数据库 RiskRule rule = ruleMapper.selectById(id); rule.setConditionExpr(dto.getConditionExpr()); rule.setActionType(dto.getActionType()); rule.setPriority(dto.getPriority()); ruleMapper.updateById(rule); // 2. 发送MQ广播,通知所有节点刷新缓存 mqTemplate.syncSend("rule-refresh", "REFRESH:" + id); return Result.success(); } } // ====== 规则刷新消费者 ====== @Component @RocketMQMessageListener(topic = "rule-refresh", consumerGroup = "rule-refresh-group") public class RuleRefreshConsumer implements RocketMQListener<String> { @Autowired private RuleEngineService ruleEngine; @Override public void onMessage(String message) { log.info("收到规则刷新通知: {}", message); ruleEngine.refreshRuleCache(); } }
规则表达式示例(MVEL语法):
sameIpCount > 10 — 同一IP请求超过10次
amount > 10000 && isNewDevice == true — 大额且新设备
registerDays < 1 && transferAmount > 5000 — 新注册用户大额转账
运营人员在后台配置这些表达式,保存后立即生效,无需重启服务。
挑战2:特征计算 — 怎么从用户行为中提取风控特征?
类比:安检不只要看行李,还要看神态(是否紧张)、出行记录(是否频繁飞往高风险地区)、同伴关系(是否和通缉犯同行)。风控也一样,不只要看当前请求,还要看历史行为、设备信息、社交关系。
特征类型特征举例数据来源时效性存储
实时特征 同IP请求次数/同设备登录数/当前金额/操作频率 当前请求 + Redis计数 毫秒级 Redis
近线特征 最近7天交易总额/最近1小时登录次数 Flink流计算 秒级 Redis + HBase
离线特征 历史行为标签/信用分/关联网络 Spark离线计算 小时级 HBase + MySQL
// ====== 特征计算服务 ====== @Service public class FeatureService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private HBaseTemplate hbaseTemplate; /** * 计算实时特征 — 从Redis中获取计数型特征 * 就像安检门口的计数器 — 今天来了多少人 */ public Map<String, Object> computeRealTimeFeatures(RiskRequest request) { Map<String, Object> features = new HashMap<>(); String ip = request.getIp(); String deviceId = request.getDeviceId(); String userId = request.getUserId(); // 1. 同IP请求次数(最近1小时) String ipKey = "risk:ip:" + ip + ":1h"; String ipCount = redisTemplate.opsForValue().get(ipKey); features.put("sameIpCount", ipCount != null ? Integer.parseInt(ipCount) : 0); // 2. 同设备登录数(最近24小时) String deviceKey = "risk:device:" + deviceId + ":24h"; String deviceCount = redisTemplate.opsForValue().get(deviceKey); features.put("sameDeviceLoginCount", deviceCount != null ? Integer.parseInt(deviceCount) : 0); // 3. 当前请求金额 features.put("amount", request.getAmount()); // 4. 是否新设备 features.put("isNewDevice", isNewDevice(userId, deviceId)); // 5. 注册天数 features.put("registerDays", getRegisterDays(userId)); // 6. 当前时间段(0-23点) features.put("hourOfDay", LocalDateTime.now().getHour()); return features; } /** * 获取离线特征 — 从HBase中查询预计算好的特征 * 就像安检前查你的出行记录 — 提前算好了 */ public Map<String, Object> getOfflineFeatures(String userId) { Map<String, Object> features = new HashMap<>(); try { // 从HBase查询用户的离线特征 Result result = hbaseTemplate.get( "risk_feature", userId, "cf" ); // 信用分(0-100,越高越安全) String creditScore = getValue(result, "cf", "credit_score"); features.put("creditScore", creditScore != null ? Double.parseDouble(creditScore) : 50.0); // 历史风险次数 String riskCount = getValue(result, "cf", "risk_count"); features.put("historyRiskCount", riskCount != null ? Integer.parseInt(riskCount) : 0); // 关联风险用户数 String relateRisk = getValue(result, "cf", "related_risk_count"); features.put("relatedRiskCount", relateRisk != null ? Integer.parseInt(relateRisk) : 0); } catch (IOException e) { log.error("查询HBase离线特征失败: userId={}", userId, e); // 降级:返回默认值(中等信用分) features.put("creditScore", 50.0); features.put("historyRiskCount", 0); features.put("relatedRiskCount", 0); } return features; } // 判断是否新设备 private boolean isNewDevice(String userId, String deviceId) { String key = "risk:device_bind:" + userId + ":" + deviceId; return !redisTemplate.hasKey(key); } // 获取注册天数 private long getRegisterDays(String userId) { String key = "risk:register:" + userId; String date = redisTemplate.opsForValue().get(key); if (date == null) return 999; // 查不到就当老用户 LocalDate registerDate = LocalDate.parse(date); return ChronoUnit.DAYS.between(registerDate, LocalDate.now()); } }
挑战3:实时决策 — 100ms内完成风控判断
类比:安检必须在几秒内决定放不放行——不能让旅客等太久。风控也一样,用户点"提交订单"后如果等3秒才返回结果,用户体验极差。要求100ms内完成从特征计算到决策输出的全流程。
特征获取 规则匹配 模型打分 决策输出
特征获取 30ms + 规则匹配 20ms + 模型打分 40ms + 决策 10ms = 总计 ~100ms
// ====== 风控实时决策服务 ====== @Service public class RiskDecisionService { @Autowired private FeatureService featureService; @Autowired private RuleEngineService ruleEngine; @Autowired private ModelService modelService; @Autowired private BlacklistService blacklistService; /** * 实时风控决策 — 核心! * 就像安检全流程:查黑名单 → 查特征 → 过规则 → 打分 → 出结果 * 要求:100ms内完成! */ public RiskDecision decide(RiskRequest request) { long startTime = System.currentTimeMillis(); // ====== 第1步:黑名单检查(最快,O(1)查Redis) ====== if (blacklistService.isBlacklisted(request.getUserId(), request.getIp())) { return buildDecision(request, DecisionType.REJECT, "黑名单用户", startTime); } // ====== 第2步:并行获取特征(实时+离线) ====== CompletableFuture<Map<String, Object>> realTimeFuture = CompletableFuture.supplyAsync( () -> featureService.computeRealTimeFeatures(request) ); CompletableFuture<Map<String, Object>> offlineFuture = CompletableFuture.supplyAsync( () -> featureService.getOfflineFeatures(request.getUserId()) ); // 等待两个特征都获取完(最多等80ms,超时用默认值) Map<String, Object> allFeatures = new HashMap<>(); try { allFeatures.putAll(realTimeFuture.get(50, TimeUnit.MILLISECONDS)); allFeatures.putAll(offlineFuture.get(80, TimeUnit.MILLISECONDS)); } catch (TimeoutException e) { // 特征获取超时,用默认值降级 allFeatures.put("creditScore", 50.0); log.warn("特征获取超时,降级处理: userId={}", request.getUserId()); } catch (Exception e) { log.error("特征获取异常", e); allFeatures.put("creditScore", 50.0); } // ====== 第3步:规则匹配 ====== RiskContext context = RiskContext.builder() .scene(request.getScene()) .featureMap(allFeatures) .build(); RuleMatchResult ruleResult = ruleEngine.match(context); if (ruleResult.isHit()) { // 规则命中,直接返回规则决策 DecisionType type = DecisionType.valueOf(ruleResult.getRule().getActionType()); return buildDecision(request, type, "规则命中: " + ruleResult.getRule().getRuleName(), startTime); } // ====== 第4步:模型打分(规则没命中,走模型) ====== double riskScore = modelService.score(allFeatures); // ====== 第5步:根据模型分数决策 ====== if (riskScore > 0.85) { return buildDecision(request, DecisionType.REJECT, "模型风险分过高: " + riskScore, startTime); } else if (riskScore > 0.6) { return buildDecision(request, DecisionType.REVIEW, "模型风险分中等,需人工审核: " + riskScore, startTime); } else { return buildDecision(request, DecisionType.PASS, "风控通过,风险分: " + riskScore, startTime); } } private RiskDecision buildDecision(RiskRequest req, DecisionType type, String reason, long startTime) { long duration = System.currentTimeMillis() - startTime; RiskDecision decision = RiskDecision.builder() .requestId(req.getRequestId()) .userId(req.getUserId()) .decisionType(type) .reason(reason) .durationMs(duration) .build(); // 异步记录决策日志(不阻塞主流程) CompletableFuture.runAsync(() -> decisionLogMapper.insert(decision)); if (duration > 100) { log.warn("风控决策超时: {}ms, requestId={}", duration, req.getRequestId()); } return decision; } } enum DecisionType { PASS, REJECT, REVIEW }
优化要点
1. 特征预计算:离线特征提前算好存Redis/HBase,不要实时算。
2. 并行获取:实时特征和离线特征并行获取,不要串行。
3. 规则缓存:规则加载到内存,不要每次查数据库。
4. 异步模型:模型打分如果太慢,考虑异步+缓存(同一用户5分钟内复用打分结果)。
5. 超时降级:特征获取超时用默认值,绝不能让用户等。
挑战4:误杀率控制 — 太严=正常用户被拒、太松=坏人放行
类比:安检太严——每个旅客都要脱鞋搜身,误机率飙升,旅客体验极差。安检太松——什么都能带进来,出事了就是重大事故。风控也一样,误杀率太高(正常用户被拒)用户流失,误杀率太低(坏人放行)资损严重。
太严:误杀率高 理想:精准拦截 太松:漏杀率高
精确率 vs 召回率
精确率 = 拦截的人中真正是坏人的比例。精确率高 = 误杀少。
召回率 = 所有坏人中被拦截的比例。召回率高 = 漏杀少。

这两个指标是矛盾的——精确率高了召回率就低(有坏人没抓到),召回率高了精确率就低(误杀多)。就像安检门灵敏度调高——报警多了但误报也多;调低——漏报多了但报警都是真的。

解决方案:不确定的不直接拒绝,而是转人工审核——就像安检不确定就让你走人工通道。这样既不会误杀正常用户,也不会漏掉坏人。
决策类型含义阈值占比目标处理方式
PASS 通过 安全,放行 风险分 < 0.6 95%+ 直接放行
REVIEW 审核 可疑,需人工确认 0.6 ≤ 风险分 < 0.85 3-5% 转人工审核队列
REJECT 拒绝 高危,拦截 风险分 ≥ 0.85 1-2% 直接拒绝+通知
挑战5:风控模型基础 — 评分卡/决策树/机器学习
类比:老练的安检员凭经验判断——看一眼就知道谁可疑。风控模型就是"数学化的经验",用数据训练出一个能自动判断风险的模型。
评分卡模型实现(工程视角,不深入算法)
// ====== 评分卡模型 — 最基础的风控模型 ====== // 核心思路:每个特征有一个分值,所有特征分值加权求和得到总分 // 就像考试 — 每道题有分,总分越高越"危险" @Data public class ScoreCardItem { private String featureName; // 特征名:registerDays private String binRange; // 分箱范围:"0-1" / "1-7" / "7+" private double score; // 该分箱的分值 } @Service public class ScoreCardModel { // 评分卡配置(从数据库加载) private List<ScoreCardItem> scoreCards; /** * 评分卡打分 * 就像批改试卷 — 每个特征对应一个分值,求和得到总分 */ public double score(Map<String, Object> features) { double totalScore = 0; for (ScoreCardItem item : scoreCards) { Object value = features.get(item.getFeatureName()); if (value == null) continue; // 判断特征值落在哪个分箱 if (matchBin(value, item.getBinRange())) { totalScore += item.getScore(); } } // 归一化到0-1(0=安全,1=高危) double maxScore = 100; // 评分卡最高分 return Math.min(totalScore / maxScore, 1.0); } private boolean matchBin(Object value, String binRange) { // 简单实现:解析分箱范围 // 如 "0-1" 表示 0 <= value <= 1 // 如 "7+" 表示 value >= 7 if (binRange.endsWith("+")) { double threshold = Double.parseDouble(binRange.replace("+", "")); return toDouble(value) >= threshold; } String[] parts = binRange.split("-"); double low = Double.parseDouble(parts[0]); double high = Double.parseDouble(parts[1]); double v = toDouble(value); return v >= low && v <= high; } } // ====== 规则 + 模型混合方案 ====== // 先走规则(确定性强的判断),规则没命中再走模型(概率性判断) // 就像安检 — 先看黑名单(确定性),再看X光(概率性) @Service public class ModelService { @Autowired private ScoreCardModel scoreCardModel; /** * 模型打分 * 实际生产中可能调用外部模型服务(PMML/ONNX) * 这里用评分卡模型演示 */ public double score(Map<String, Object> features) { return scoreCardModel.score(features); } }

🏗️ 架构设计 — 风控系统架构

请求接入 特征服务 规则引擎 模型服务 决策服务 审核平台
🌐 请求接入层
SDK埋点 API网关 请求预处理 限流防刷
职责:收集风控请求。前端SDK上报用户行为(点击/滑动/停留时间),后端API网关拦截业务请求(登录/下单/转账)。请求预处理:解析IP、设备指纹、请求参数。限流防刷:防止恶意请求打爆风控系统。
📊 特征服务
实时特征(Redis) 离线特征(HBase) 特征计算 特征缓存
职责:特征是风控的"弹药"——没有特征,规则和模型都是无米之炊。实时特征从Redis获取(同IP计数、同设备计数等),离线特征从HBase获取(信用分、风险标签等)。特征服务要求在30ms内返回结果,超时降级用默认值。
⚙️ 规则引擎
规则加载 规则匹配(MVEL) 规则热更新 规则冲突检测
职责:规则引擎是风控的"快速通道"——确定性强的判断走规则(黑名单、高频请求),比模型更快。规则配置化存储在数据库中,通过MQ广播实现热更新。MVEL表达式引擎执行规则条件。规则优先级决定执行顺序。
🧠 模型服务
评分卡模型 PMML模型 模型AB测试 模型监控
职责:模型服务是风控的"深度思考"——规则没命中的请求走模型打分。评分卡模型是最基础的实现,生产中常用PMML/ONNX加载训练好的模型。模型AB测试:新旧模型并行跑,比较效果后决定是否上线。模型监控:PSI(群体稳定性指标)监控特征分布是否偏移。
📋 决策服务
决策聚合 阈值管理 决策日志 后监控
职责:汇总规则和模型的判断结果,输出最终决策(通过/拒绝/人工审核)。阈值管理:根据业务场景调整风险阈值。决策日志:记录每次风控决策的详细信息(特征值、规则命中、模型分数、决策结果),用于审计和回溯。后监控:对通过的请求持续监控,发现异常二次拦截。
👨‍💻 审核平台
人工审核队列 审核工作台 审核统计 反馈回路
职责:处理REVIEW(待审核)的请求。审核员查看用户信息、操作记录、风险特征,人工判断是放行还是拒绝。审核结果作为"标注数据"反馈给模型训练,形成闭环。就像安检员把每次人工检查的结果记录下来,用来改进安检规则。

🔄 关键流程 — 代码级详解

流程1:风控实时决策完整代码
// ====== 风控API入口 ====== @RestController @RequestMapping("/api/risk") public class RiskController { @Autowired private RiskDecisionService decisionService; /** * 风控检查接口 * 业务系统在关键操作前调用此接口 * 就像安检入口 — 每个旅客必须过安检才能登机 */ @PostMapping("/check") public Result<RiskDecisionVO> check(@RequestBody RiskRequest request) { RiskDecision decision = decisionService.decide(request); return Result.success(toVO(decision)); } } // ====== 风控请求DTO ====== @Data public class RiskRequest { private String requestId; // 请求唯一ID private String userId; // 用户ID private String scene; // 场景:LOGIN/ORDER/TRANSFER private String ip; // 请求IP private String deviceId; // 设备指纹 private BigDecimal amount; // 交易金额(转账/订单场景) private String payeeId; // 收款人ID(转账场景) private Map<String, String> ext; // 扩展参数 } // ====== 业务系统调用风控的示例 ====== @Service public class TransferService { @Autowired private RiskFeignClient riskClient; public TransferResult transfer(TransferRequest req) { // 1. 调用风控检查 RiskRequest riskReq = new RiskRequest(); riskReq.setScene("TRANSFER"); riskReq.setUserId(req.getUserId()); riskReq.setAmount(req.getAmount()); riskReq.setPayeeId(req.getPayeeId()); Result<RiskDecisionVO> riskResult = riskClient.check(riskReq); // 2. 根据风控决策处理 switch (riskResult.getData().getDecisionType()) { case PASS: // 风控通过,继续转账 return doTransfer(req); case REJECT: // 风控拒绝 return TransferResult.rejected(riskResult.getData().getReason()); case REVIEW: // 人工审核中 return TransferResult.pending("您的转账正在审核中,请耐心等待"); } return TransferResult.error(); } }
流程2:规则引擎配置与执行
// ====== 规则配置SQL — 在数据库中配置规则 ====== /* 规则1:同一IP 1小时内请求超过50次 → 拒绝 */ INSERT INTO risk_rule (rule_code, rule_name, condition_expr, action_type, priority, scene, enabled) VALUES ('R001', '同IP高频请求', 'sameIpCount > 50', 'REJECT', 1, 'LOGIN', 1); /* 规则2:新设备+大额转账 → 人工审核 */ INSERT INTO risk_rule (rule_code, rule_name, condition_expr, action_type, priority, scene, enabled) VALUES ('R002', '新设备大额转账', 'isNewDevice == true && amount > 10000', 'REVIEW', 2, 'TRANSFER', 1); /* 规则3:新注册用户+大额转账 → 拒绝 */ INSERT INTO risk_rule (rule_code, rule_name, condition_expr, action_type, priority, scene, enabled) VALUES ('R003', '新用户大额转账', 'registerDays < 1 && amount > 5000', 'REJECT', 3, 'TRANSFER', 1); /* 规则4:深夜+大额转账 → 人工审核 */ INSERT INTO risk_rule (rule_code, rule_name, condition_expr, action_type, priority, scene, enabled) VALUES ('R004', '深夜大额转账', '(hourOfDay < 6 || hourOfDay > 23) && amount > 5000', 'REVIEW', 4, 'TRANSFER', 1); /* 规则5:低信用分+高风险关联 → 拒绝 */ INSERT INTO risk_rule (rule_code, rule_name, condition_expr, action_type, priority, scene, enabled) VALUES ('R005', '低信用高风险', 'creditScore < 30 && relatedRiskCount > 3', 'REJECT', 5, 'TRANSFER', 1); // ====== 规则执行示例 ====== // 假设用户特征如下: // sameIpCount = 5, isNewDevice = true, amount = 20000 // registerDays = 30, creditScore = 75, relatedRiskCount = 0 // hourOfDay = 14 // 规则匹配过程: // R001: sameIpCount(5) > 50? → 否 // R002: isNewDevice(true) && amount(20000) > 10000? → 是!命中! // → 决策:REVIEW(人工审核)
流程3:人工审核流程
// ====== 人工审核服务 ====== @Service public class ReviewService { @Autowired private DecisionLogMapper decisionLogMapper; @Autowired private BlacklistService blacklistService; /** * 获取待审核列表 * 就像安检复检通道 — 排队等候人工检查 */ public PageResult<ReviewItem> getPendingReviews(String reviewerId, int page, int size) { return decisionLogMapper.selectPendingReviews(reviewerId, page, size); } /** * 审核操作 — 审核员判断通过或拒绝 * 就像安检员看完X光后决定放行还是扣留 */ @Transactional public void review(Long decisionId, ReviewAction action) { DecisionLog log = decisionLogMapper.selectById(decisionId); switch (action.getType()) { case APPROVE: // 审核通过 — 放行 log.setFinalDecision(DecisionType.PASS.name()); log.setReviewerId(action.getReviewerId()); log.setReviewComment(action.getComment()); log.setReviewTime(LocalDateTime.now()); // 通知业务系统放行 businessNotifyService.notifyApproved(log.getRequestId()); break; case REJECT: // 审核拒绝 — 拒绝 log.setFinalDecision(DecisionType.REJECT.name()); log.setReviewerId(action.getReviewerId()); log.setReviewComment(action.getComment()); log.setReviewTime(LocalDateTime.now()); // 如果审核员标记为"恶意",加入黑名单 if (action.isAddToBlacklist()) { blacklistService.addToBlacklist(log.getUserId(), "人工审核拒绝"); } // 通知业务系统拒绝 businessNotifyService.notifyRejected(log.getRequestId(), action.getComment()); break; } decisionLogMapper.updateById(log); // 记录审核日志 — 用于模型训练的标注数据 saveReviewLabel(log, action); } /** * 保存审核标注 — 作为模型训练数据 * 就像安检员的经验 — 他们的判断是训练新安检员的教材 */ private void saveReviewLabel(DecisionLog log, ReviewAction action) { ModelLabel label = new ModelLabel(); label.setUserId(log.getUserId()); label.setRequestId(log.getRequestId()); label.setFeatures(log.getFeaturesJson()); label.setIsFraud(action.getType() == ReviewActionType.REJECT); label.setCreatedAt(LocalDateTime.now()); modelLabelMapper.insert(label); } } // ====== 黑名单服务 ====== @Service public class BlacklistService { @Autowired private StringRedisTemplate redisTemplate; private static final String BLACKLIST_KEY = "risk:blacklist:"; /** * 检查是否黑名单 — O(1)操作,极快 */ public boolean isBlacklisted(String userId, String ip) { return redisTemplate.hasKey(BLACKLIST_KEY + "user:" + userId) || redisTemplate.hasKey(BLACKLIST_KEY + "ip:" + ip); } /** * 加入黑名单 */ public void addToBlacklist(String userId, String reason) { redisTemplate.opsForValue().set( BLACKLIST_KEY + "user:" + userId, reason ); // 同步写入数据库(持久化) blacklistMapper.insert(new BlacklistRecord(userId, reason)); } }

💥 踩坑实录 — 血泪教训

坑1:规则死循环 — 规则A触发规则B,B又触发A
场景:运营配置了两条规则——规则A"金额>1万触发审核",规则B"审核中的订单金额>5万触发拒绝"。结果一个1.2万的订单触发了规则A变成审核状态,然后规则B又匹配上了,变成了拒绝状态。但规则A的条件还在,又重新触发审核...死循环了!
解决:规则依赖检查 + 执行深度限制!
1. 规则依赖图:在保存规则时分析规则之间的依赖关系,如果形成环就拒绝保存。
2. 执行深度限制:规则匹配最多执行3轮,超过3轮直接返回REVIEW。
3. 状态隔离:规则A的决策结果不应作为规则B的输入。规则只看原始特征,不看其他规则的决策结果。
// 规则执行深度限制 public RuleMatchResult matchWithDepthLimit(RiskContext context, int maxDepth) { RuleMatchResult result = match(context); int depth = 0; while (result.isHit() && depth < maxDepth) { // 如果命中规则产生了新特征,重新匹配 if (result.hasNewFeatures()) { context.mergeFeatures(result.getNewFeatures()); result = match(context); depth++; } else { break; // 没有新特征产生,不会死循环 } } if (depth >= maxDepth) { log.warn("规则执行深度超限,转为人工审核"); return RuleMatchResult.review("规则执行深度超限"); } return result; }
坑2:特征计算超时 — 查HBase太慢
场景:风控请求高峰期,HBase查询P99从20ms飙升到500ms,导致风控决策总耗时超过100ms,部分请求超时降级,用了默认特征值(中等信用分),结果一个高风险用户因为降级被放行了!
解决:降级策略 + 特征缓存!
1. 超时降级:特征获取设置80ms超时,超时用默认值。但默认值要偏保守——宁可多审核也不要放过坏人。
2. 特征预热缓存:热点用户的离线特征缓存到Redis,避免每次查HBase。
3. 异步预加载:用户登录后异步预加载特征到Redis,后续请求直接从Redis取。
4. 降级决策:特征降级时,决策阈值自动调高(50→70),更多请求走人工审核。
// 降级策略:特征超时时降低阈值 private double getAdaptiveThreshold(boolean isFeatureDegraded) { // 正常情况:风险分>0.85拒绝 // 特征降级:风险分>0.7就拒绝(更严格,防止降级放行坏人) return isFeatureDegraded ? 0.7 : 0.85; }
坑3:误杀率飙升 — 新规则上线后正常用户被大量拦截
场景:运营加了一条新规则"1小时内同IP请求>10次→拒绝",结果大量使用公司WiFi的正常用户被误杀——公司出口IP就那么几个,所有员工共享,10次轻轻松就超了。
解决:灰度发布 + 监控!
1. 灰度发布:新规则先只对1%的流量生效,观察1小时没问题再逐步扩大到10%、50%、100%。
2. 影子模式:新规则先只记录命中不实际拦截(影子执行),观察误杀率后再正式生效。
3. 实时监控:新规则上线后实时监控通过率/拒绝率/审核率,异常立即自动回滚。
4. 白名单:已知的安全场景(如公司出口IP)加入白名单豁免。
// 灰度规则执行 public RuleMatchResult matchWithGray(RiskRule rule, RiskContext context) { // 判断当前请求是否命中灰度范围 if (rule.getGrayRatio() < 1.0) { int hash = context.getUserId().hashCode(); if (Math.abs(hash % 100) >= rule.getGrayRatio() * 100) { // 不在灰度范围内,跳过此规则 return RuleMatchResult.miss(); } } // 影子模式:只记录不执行 RuleMatchResult result = doMatch(rule, context); if (rule.isShadowMode() && result.isHit()) { // 影子命中,记录但不返回 shadowLogMapper.insert(new ShadowLog(rule, context)); return RuleMatchResult.miss(); // 当作没命中 } return result; }
坑4:规则冲突 — 两条规则结论矛盾
场景:规则A说"新用户大额转账→拒绝",规则B说"VIP用户大额转账→通过"。一个新注册的VIP用户转账5万,两条规则都命中了,但结论矛盾——该听谁的?
解决:优先级 + 冲突解决策略!
1. 优先级:每条规则有优先级数值,数值小的先执行,命中后直接返回(短路机制)。
2. 拒绝对优先:如果多条规则命中且结论冲突,REJECT > REVIEW > PASS(宁杀勿放原则)。
3. 规则互斥组:同组的规则只能命中一条。比如"新用户"和"VIP用户"不应该在同一组。
4. 冲突告警:规则冲突时记录日志,提醒运营调整规则。

🔍 排查思路 — 出了问题怎么找

问题现象可能原因排查步骤定位方法
正常用户被拒(误杀) 1. 规则配置错误
2. 特征计算异常
3. 阈值过低
1. 查决策日志中的规则命中
2. 查特征值是否异常
3. 查阈值配置
决策日志表 + 规则命中详情
坏人被放行(漏杀) 1. 规则覆盖不足
2. 特征缺失
3. 模型准确率低
1. 分析坏人特征模式
2. 检查特征是否完整
3. 查模型PSI指标
坏样本分析 + 特征覆盖检查
风控响应慢 1. HBase查询慢
2. 特征计算复杂
3. 规则数量太多
1. 查特征获取耗时
2. 查规则匹配耗时
3. 查是否有慢规则
APM链路追踪 + 耗时分解
规则不生效 1. 规则未启用
2. 规则缓存未刷新
3. 场景不匹配
1. 查规则enabled状态
2. 查节点缓存刷新时间
3. 查规则scene配置
规则管理后台 + 缓存监控
模型分数异常 1. 特征分布偏移(PSI高)
2. 模型过时
3. 特征值异常
1. 查PSI监控
2. 查模型上线时间
3. 查特征值分布
模型监控看板 + 特征分布报表
$ 排查误杀 — 正常用户被风控拒绝 # 1. 查决策日志 SELECT * FROM decision_log WHERE user_id = 'U12345' ORDER BY created_at DESC LIMIT 5; id | user_id | decision_type | hit_rules | risk_score | duration_ms 101 | U12345 | REJECT | R001 | 0.92 | 45ms → 命中了规则R001(同IP高频请求),风险分0.92 # 2. 查该用户当时的特征值 SELECT features_json FROM decision_log WHERE id = 101; {"sameIpCount": 52, "isNewDevice": false, "amount": 500, ...} → sameIpCount=52,超过了规则R001的阈值50 # 3. 分析原因 用户使用公司WiFi,出口IP共享,所以同IP计数偏高 → 解决:将公司出口IP加入白名单 $ 排查漏杀 — 坏人被风控放行 # 1. 找到坏样本的特征 SELECT features_json FROM decision_log WHERE user_id = 'U67890' AND is_confirmed_fraud = 1; {"sameIpCount": 3, "isNewDevice": true, "amount": 4800, "registerDays": 2, ...} → 特征值都在正常范围,所以没触发规则 # 2. 分析:缺少"同一收件人多次收款"等特征 → 解决:新增特征 + 新增规则覆盖此场景

📝 面试高频 — 5道必考题

Q1:风控规则引擎怎么设计?如何实现热更新?
:规则配置化 + 表达式引擎 + MQ广播刷新。
1. 规则存储:所有规则存在数据库中,包含条件表达式(MVEL语法)、触发动作、优先级等。
2. 规则加载:服务启动时从数据库加载所有启用的规则到内存缓存,按优先级排序。
3. 规则执行:用MVEL表达式引擎执行条件表达式,输入是特征Map,输出是boolean。
4. 热更新:运营在后台修改规则后,发送MQ广播消息,所有节点收到后重新从数据库加载规则到缓存。
5. 灰度发布:新规则先影子模式执行(只记录不拦截),确认无误后正式上线。
6. 优先级短路:规则按优先级排序,命中一条就返回(短路机制),提高效率。
Q2:风控特征怎么设计?实时特征和离线特征有什么区别?
:分层设计——实时特征+近线特征+离线特征。
1. 实时特征:当前请求直接能算的特征,如同一IP请求次数、当前交易金额、是否新设备。从Redis获取,毫秒级。
2. 近线特征:最近一段时间窗口的聚合特征,如最近7天交易总额、最近1小时登录次数。由Flink流计算预聚合,存Redis+HBase。
3. 离线特征:历史沉淀的特征,如信用分、风险标签、关联网络。由Spark离线计算,存HBase。
4. 获取策略:实时和离线特征并行获取(CompletableFuture),超时降级用默认值。
5. 存储选择:高频用Redis(快),低频用HBase(省空间),特征预热减少实时查询。
Q3:风控实时决策如何保证100ms内完成?
:全链路优化——特征预计算 + 并行获取 + 规则缓存 + 模型简化。
1. 特征预计算:离线特征提前算好存Redis/HBase,不要实时计算。
2. 并行获取:实时特征和离线特征用CompletableFuture并行获取。
3. 规则缓存:规则加载到内存,不要每次查数据库。
4. 黑名单前置:黑名单检查O(1),最先执行,命中直接返回。
5. 模型简化:评分卡模型比深度学习模型快得多,优先用轻量模型。
6. 异步日志:决策日志异步写入,不阻塞主流程。
7. 超时降级:任何环节超时都有降级策略,用默认值+调高阈值。
Q4:误杀率太高怎么优化?
:灰度发布 + 人工审核 + 白名单 + 阈值调优。
1. 灰度发布:新规则先1%流量影子执行,观察误杀率再正式上线。
2. 人工审核兜底:不确定的不直接拒绝,转人工审核。宁可多审核也不误杀。
3. 白名单:已知安全场景(公司出口IP、老用户常用设备)加入白名单豁免。
4. 阈值调优:根据误杀率和漏杀率曲线找到最佳阈值。通常阈值越高误杀越少但漏杀越多。
5. 反馈回路:审核结果作为标注数据,重新训练模型,持续优化。
Q5:风控系统架构怎么设计?
:分层架构——请求接入→特征服务→规则引擎→模型服务→决策服务→审核平台。
1. 请求接入层:SDK埋点+API网关,收集风控请求。
2. 特征服务:实时特征(Redis)+离线特征(HBase),30ms内返回。
3. 规则引擎:MVEL表达式执行规则,内存缓存,MQ热更新。
4. 模型服务:评分卡/PMML模型打分,40ms内完成。
5. 决策服务:汇总规则+模型结果,输出PASS/REJECT/REVIEW。
6. 审核平台:人工审核队列+工作台+反馈回路。
7. 后监控:对PASS的请求持续监控,异常二次拦截。