Java 大厂面试:Spring Security+Kafka+Redis 安全风控系统架构实战解析
本文通过面试官与程序员谢飞机的趣味对话,深入解析 Java 安全风控面试核心知识点,涵盖 Spring Security、Kafka、Redis 等主流技术栈,附带详细答案供学习者参考。
Java 大厂面试:Spring Security+Kafka+Redis 安全风控系统架构实战解析
面试场景背景
公司:某互联网大厂安全风控部门
岗位:高级 Java 开发工程师
面试官:技术总监(严肃脸)
候选人:谢飞机(水货程序员,自信满满)
第一轮:基础架构与安全认证
面试官:谢飞机是吧?看你简历上写了熟悉微服务架构,那我先问问,我们风控系统需要对接多个业务线,服务之间怎么调用比较合适?
谢飞机:(胸有成竹)这个简单!用 OpenFeign 啊,声明式调用,代码简洁,还支持负载均衡。
面试官:(点头)嗯,基础还可以。那服务注册发现用的什么?
谢飞机:Nacos!比 Eureka 新,支持配置中心,一举两得。
面试官:(微笑)不错。那风控系统对安全性要求很高,用户认证怎么做的?
谢飞机:(眼睛一亮)这个我熟!Spring Security+JWT,无状态认证,性能好!
面试官:(挑眉)那 JWT Token 过期了怎么处理?用户正在操作突然被踢出去体验很差吧?
谢飞机:(挠头)呃...这个...可以设置长一点过期时间?或者...双 Token 机制?
面试官:(记录)嗯,知道双 Token 还算可以。那最后一个问题,风控系统需要识别恶意请求,怎么做接口幂等性?
谢飞机:(自信)Redis 啊!用请求参数生成 key,设置过期时间,重复请求直接拦截!
面试官:(点头)行,第一轮还行,我们继续。
第二轮:消息队列与缓存架构
面试官:风控系统需要实时分析用户行为,数据量很大,怎么保证处理性能?
谢飞机:(来劲了)上 Kafka!高吞吐,异步解耦,完美!
面试官:那消息丢了怎么办?风控漏判一个恶意用户损失可能很大。
谢飞机:(思考)ACK 机制!生产者等 Broker 确认,消费者手动提交 offset!
面试官:(满意)不错。那同一用户的操作需要按顺序处理,怎么保证消息顺序?
谢飞机:(犹豫)这个...Kafka 分区?同一个用户 ID 哈希到同一个分区?
面试官:(眼睛一亮)对!继续说。
谢飞机:(松口气)然后消费者单线程消费这个分区,就能保证顺序了!
面试官:(记录)好。那风控规则需要动态更新,不能重启服务,怎么设计?
谢飞机:(挠头)配置中心?Nacos 配置热更新?
面试官:(追问)规则复杂的话,配置里放不下怎么办?
谢飞机:(含糊)呃...可以存数据库?然后...定时刷新?或者...用规则引擎?
面试官:(不置可否)行,那缓存方面呢?风控判断需要快速读取用户风险等级。
谢飞机:(恢复自信)Redis!三级缓存!Caffeine 本地缓存+Redis 分布式缓存+数据库!
面试官:缓存穿透、击穿、雪崩怎么解决?
谢飞机:(如数家珍)穿透用布隆过滤器,击穿用互斥锁,雪崩给过期时间加随机值!
面试官:(点头)还可以。
第三轮:分布式事务与监控运维
面试官:风控系统判定用户违规后,需要冻结账户、记录日志、发送通知,这几个操作要一致,怎么保证?
谢飞机:(紧张)分布式事务...Seata!
面试官:Seata 有几种模式?
谢飞机:(回忆)AT...TCC...还有 Saga?
面试官:那风控场景适合哪种?
谢飞机:(犹豫)AT 模式简单...但性能...TCC 性能好但代码复杂...这个...看场景?
面试官:(记录)行,继续。系统上线后怎么监控?
谢飞机:(放松)Prometheus+Grafana!指标采集加可视化!
面试官:具体监控哪些指标?
谢飞机:(思考)JVM 内存、GC 次数、接口 QPS、响应时间、错误率...
面试官:那链路追踪呢?一个请求经过多个服务,出问题怎么定位?
谢飞机:(含糊)SkyWalking...或者 Zipkin...埋点...然后...看链路图?
面试官:(追问)JVM 频繁 Full GC 怎么排查?
谢飞机:(擦汗)这个...用 jstat 看 GC 情况...jmap dump 内存...然后...MAT 分析?
面试官:(点头)最后一个问题,风控系统 QPS 突然飙升 10 倍,怎么应对?
谢飞机:(彻底懵了)限流...降级...扩容...这个...看情况...吧?
面试官:(合上简历)行,今天先到这,回去等通知吧。
谢飞机:(松口气)好的好的,谢谢面试官!
详细答案解析
第一轮答案详解
1. OpenFeign 服务调用
业务场景:风控系统需要调用用户服务、订单服务、支付服务获取数据
技术方案:
@FeignClient(name = "user-service", fallback = UserFallback.class)
public interface UserServiceClient {
@GetMapping("/api/user/{userId}")
Result<UserInfo> getUserInfo(@PathVariable("userId") Long userId);
}
// 熔断降级
@Component
public class UserFallback implements UserServiceClient {
@Override
public Result<UserInfo> getUserInfo(Long userId) {
return Result.fail("用户服务不可用");
}
}
配置:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
circuitbreaker:
enabled: true
resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
2. Nacos 服务注册发现
配置:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: risk-control
group: DEFAULT_GROUP
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
3. Spring Security+JWT 双 Token 认证
业务场景:Access Token 短有效期(30 分钟),Refresh Token 长有效期(7 天)
代码实现:
@Component
public class JwtTokenProvider {
@Value("${jwt.access-expiration}")
private long accessExpiration;
@Value("${jwt.refresh-expiration}")
private long refreshExpiration;
// 生成 Access Token
public String generateAccessToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessExpiration))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
// 生成 Refresh Token
public String generateRefreshToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(SignatureAlgorithm.HS512, refreshSecretKey)
.compact();
}
// 刷新 Token
public String refreshAccessToken(String refreshToken) {
if (validateToken(refreshToken)) {
String username = getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return generateAccessToken(userDetails);
}
throw new InvalidTokenException("Refresh Token 无效");
}
}
4. Redis 接口幂等性
业务场景:防止用户重复提交风控申诉请求
代码实现:
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
// 生成幂等 key
String idempotentKey = generateIdempotentKey(pjp, idempotent);
// 尝试设置 key
Boolean setResult = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", idempotent.expireTime(), TimeUnit.SECONDS);
if (Boolean.FALSE.equals(setResult)) {
throw new IdempotentException("重复请求");
}
try {
return pjp.proceed();
} finally {
// 执行成功后删除 key(可选)
if (idempotent.deleteAfterExec()) {
redisTemplate.delete(idempotentKey);
}
}
}
private String generateIdempotentKey(ProceedingJoinPoint pjp, Idempotent idempotent) {
// 根据请求参数生成唯一 key
String params = JSON.toJSONString(pjp.getArgs());
return "idempotent:" + idempotent.key() + ":" + MD5Util.md5(params);
}
}
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default "";
long expireTime() default 300;
boolean deleteAfterExec() default false;
}
第二轮答案详解
1. Kafka 消息可靠性保证
业务场景:用户行为日志采集,不能丢失
生产者配置:
spring:
kafka:
producer:
acks: all # 所有副本确认
retries: 3 # 重试次数
properties:
enable.idempotence: true # 幂等性
max.in.flight.requests.per.connection: 5
消费者配置:
spring:
kafka:
consumer:
enable-auto-commit: false # 手动提交
auto-offset-reset: earliest
properties:
max.poll.records: 100
消费者代码:
@KafkaListener(topics = "user-behavior", groupId = "risk-control-group")
public void consumeUserBehavior(ConsumerRecord<String, String> record, Acknowledgment ack) {
try {
// 处理消息
processBehavior(record);
// 手动提交 offset
ack.acknowledge();
} catch (Exception e) {
// 记录失败消息
saveFailedMessage(record);
// 可以选择不提交,让 Kafka 重试
throw e;
}
}
2. Kafka 消息顺序保证
业务场景:同一用户的操作需要按顺序处理(登录->交易->登出)
生产者:
public void sendOrderedMessage(String userId, String event) {
// 同一用户 ID 发送到同一分区
ProducerRecord<String, String> record = new ProducerRecord<>(
"user-event",
getPartition(userId), // 根据 userId 计算分区
userId,
event
);
kafkaTemplate.send(record);
}
private int getPartition(String userId) {
return Math.abs(userId.hashCode()) % partitionCount;
}
消费者:
// 单线程消费每个分区
@KafkaListener(
topics = "user-event",
groupId = "risk-control-group",
containerFactory = "singleThreadContainerFactory"
)
public void consumeEvent(ConsumerRecord<String, String> record) {
// 单线程处理,保证顺序
processEvent(record);
}
3. Redis 三级缓存架构
业务场景:用户风险等级高频读取
代码实现:
@Service
public class RiskLevelCacheService {
@Autowired
private CacheManager cacheManager;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserRiskLevelMapper mapper;
// 一级缓存:Caffeine 本地缓存
private Cache<String, RiskLevel> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public RiskLevel getRiskLevel(String userId) {
// 1. 查本地缓存
RiskLevel level = localCache.getIfPresent(userId);
if (level != null) {
return level;
}
// 2. 查 Redis 分布式缓存
String key = "risk:level:" + userId;
level = (RiskLevel) redisTemplate.opsForValue().get(key);
if (level != null) {
localCache.put(userId, level);
return level;
}
// 3. 查数据库(加锁防击穿)
return loadFromDatabaseWithLock(userId, key);
}
private RiskLevel loadFromDatabaseWithLock(String userId, String key) {
// 分布式锁
RLock lock = redissonClient.getLock("lock:" + key);
if (lock.tryLock()) {
try {
// 双重检查
RiskLevel level = (RiskLevel) redisTemplate.opsForValue().get(key);
if (level != null) {
localCache.put(userId, level);
return level;
}
// 查数据库
level = mapper.selectByUserId(userId);
if (level != null) {
// 随机过期时间防雪崩
int expireTime = 300 + new Random().nextInt(120);
redisTemplate.opsForValue().set(key, level, expireTime, TimeUnit.SECONDS);
localCache.put(userId, level);
} else {
// 空值也缓存,防穿透
redisTemplate.opsForValue().set(key, new RiskLevel(), 60, TimeUnit.SECONDS);
}
return level;
} finally {
lock.unlock();
}
} else {
// 等锁释放后递归调用
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getRiskLevel(userId);
}
}
}
4. 布隆过滤器防穿透
@Component
public class BloomFilterService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String BLOOM_FILTER_KEY = "risk:bloom:user";
// 初始化布隆过滤器
@PostConstruct
public void init() {
RFunnelnel<String> bloomFilter = redissonClient.getFunnelnel(BLOOM_FILTER_KEY);
// 预计 1000 万用户,误判率 0.01%
bloomFilter.tryInit(10000000, 0.0001);
}
// 添加用户 ID
public void addUser(String userId) {
RFunnelnel<String> bloomFilter = redissonClient.getFunnelnel(BLOOM_FILTER_KEY);
bloomFilter.add(userId);
}
// 判断用户是否存在
public boolean mayContain(String userId) {
RFunnelnel<String> bloomFilter = redissonClient.getFunnelnel(BLOOM_FILTER_KEY);
return bloomFilter.contains(userId);
}
}
第三轮答案详解
1. Seata 分布式事务
业务场景:用户违规处理(冻结账户 + 记录日志 + 发送通知)
AT 模式:
@GlobalTransactional
public void handleUserViolation(String userId, ViolationType type) {
// 1. 冻结账户
accountService.freezeAccount(userId);
// 2. 记录违规日志
violationLogService.recordLog(userId, type);
// 3. 发送通知
notificationService.sendNotification(userId, type);
}
TCC 模式:
public interface AccountTccService {
@TwoPhaseBusinessAction(name = "freezeAccount",
commitMethod = "commitFreeze",
rollbackMethod = "rollbackFreeze")
void freezeAccount(BusinessActionContext context, String userId);
boolean commitFreeze(BusinessActionContext context);
boolean rollbackFreeze(BusinessActionContext context);
}
@Service
public class AccountTccServiceImpl implements AccountTccService {
@Override
public void freezeAccount(BusinessActionContext context, String userId) {
// Try 阶段:预留资源
String xid = context.getXid();
redisTemplate.opsForValue().set("tcc:freeze:" + userId, xid, 10, TimeUnit.MINUTES);
}
@Override
public boolean commitFreeze(BusinessActionContext context) {
// Confirm 阶段:确认执行
String userId = (String) context.getActionContext("userId");
accountMapper.freeze(userId);
redisTemplate.delete("tcc:freeze:" + userId);
return true;
}
@Override
public boolean rollbackFreeze(BusinessActionContext context) {
// Cancel 阶段:回滚
String userId = (String) context.getActionContext("userId");
redisTemplate.delete("tcc:freeze:" + userId);
return true;
}
}
2. Prometheus+Grafana 监控
Micrometer 指标埋点:
@Component
public class RiskControlMetrics {
private final MeterRegistry meterRegistry;
private final Counter requestCounter;
private final Timer processTimer;
private final Gauge riskLevelGauge;
public RiskControlMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 请求计数器
this.requestCounter = Counter.builder("risk_control_requests_total")
.description("风控请求总数")
.tag("type", "api")
.register(meterRegistry);
// 处理耗时
this.processTimer = Timer.builder("risk_control_process_duration")
.description("风控处理耗时")
.register(meterRegistry);
// 风险等级分布
this.riskLevelGauge = Gauge.builder("risk_level_distribution")
.description("风险等级分布")
.register(meterRegistry, this, RiskControlMetrics::getHighRiskCount);
}
public void recordRequest() {
requestCounter.increment();
}
public <T> T recordProcess(Supplier<T> supplier) {
return processTimer.record(supplier);
}
private double getHighRiskCount() {
// 返回高风险用户数
return riskLevelService.getHighRiskCount();
}
}
Prometheus 配置:
scrape_configs:
- job_name: 'risk-control'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['risk-control-service:8080']
scrape_interval: 15s
3. SkyWalking 链路追踪
配置:
# application.yml
skywalking:
agent:
service_name: risk-control-service
namespace: production
collector:
backend_service: 127.0.0.1:11800
自定义埋点:
@Trace(operationName = "RiskControlService/checkRisk")
public RiskResult checkRisk(String userId, String action) {
// 业务逻辑
return riskResult;
}
4. JVM 调优参数
推荐配置:
# 堆内存设置
-Xms4g -Xmx4g
# 新生代比例
-XX:NewRatio=2
# 垃圾回收器
-XX:+UseG1GC
# GC 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log
# OOM Dump
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heap.hprof
# 元空间
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
GC 问题排查步骤:
jstat -gcutil <pid> 1000- 查看 GC 统计jmap -heap <pid>- 查看堆内存分布jmap -dump:format=b,file=heap.hprof <pid>- Dump 内存- MAT 分析 heap.hprof 文件
- 定位内存泄漏对象
5. 高 QPS 应对方案
限流:
@Component
public class RateLimiter {
private final RateLimiter gatewayLimiter = RateLimiter.create(1000); // 1000 QPS
public void acquire() {
if (!gatewayLimiter.tryAcquire()) {
throw new RateLimitException("请求过于频繁");
}
}
}
降级:
@FeignClient(name = "risk-score-service", fallback = RiskScoreFallback.class)
public interface RiskScoreClient {
// 服务降级
}
@Component
public class RiskScoreFallback implements RiskScoreClient {
@Override
public RiskScore getScore(String userId) {
// 返回默认分数
return RiskScore.defaultScore();
}
}
扩容:
# Kubernetes HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: risk-control-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: risk-control-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
学习路线建议
| 阶段 | 时间 | 学习重点 | |------|------|----------| | 初级 | 0-2 年 | Spring Boot、Redis 基础、RESTful API | | 中级 | 2-5 年 | 微服务架构、消息队列、分布式缓存 | | 高级 | 5 年+ | 高可用设计、分布式事务、全链路监控 |
总结
本次面试涵盖了安全风控系统的核心知识点:
✅ 安全认证:Spring Security+JWT 双 Token 机制
✅ 服务调用:OpenFeign+Resilience4j 熔断降级
✅ 消息队列:Kafka 可靠性+顺序消息保证
✅ 缓存架构:Redis 三级缓存+布隆过滤器
✅ 分布式事务:Seata AT/TCC 模式
✅ 监控运维:Prometheus+SkyWalking 全链路监控
✅ JVM 调优:G1 GC 参数+问题排查
希望这篇文章能帮助大家在面试中更好地展示技术实力!🚀
更多推荐
所有评论(0)