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 问题排查步骤

  1. jstat -gcutil <pid> 1000 - 查看 GC 统计
  2. jmap -heap <pid> - 查看堆内存分布
  3. jmap -dump:format=b,file=heap.hprof <pid> - Dump 内存
  4. MAT 分析 heap.hprof 文件
  5. 定位内存泄漏对象
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 参数+问题排查

希望这篇文章能帮助大家在面试中更好地展示技术实力!🚀

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐