🎯 核心挑战 — 风控系统的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);
}
}
🔄 关键流程 — 代码级详解
流程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. 冲突告警:规则冲突时记录日志,提醒运营调整规则。