第四篇:进阶篇 — 缓存、消息队列、安全与常用中间件

目标 :掌握 Spring Boot 在实际项目中的增强能力
学习时长 :3~4 周
前置要求:完成数据篇,了解 Redis 基础


目录

  1. [Redis 集成](#Redis 集成)
  2. 缓存设计
  3. 缓存穿透、击穿、雪崩
  4. 定时任务
  5. 异步任务
  6. [RabbitMQ/Kafka 消息队列](#RabbitMQ/Kafka 消息队列)
  7. [JWT 登录认证](#JWT 登录认证)
  8. [Spring Security](#Spring Security)
  9. [RBAC 权限模型](#RBAC 权限模型)
  10. [API 限流](#API 限流)
  11. 数据脱敏
  12. 面试高频题

1. Redis 集成

依赖与配置

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml 复制代码
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: your_password
      database: 0
      timeout: 3000ms
      lettuce:          # 默认连接池(推荐 Lettuce,支持异步)
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: -1ms

RedisTemplate 配置(序列化)

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> jsonSerializer =
            new Jackson2JsonRedisSerializer<>(Object.class);

        // Key 用字符串序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        // Value 用 JSON 序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

常用操作

java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

// String
redisTemplate.opsForValue().set("user:1", user, 30, TimeUnit.MINUTES);
User user = (User) redisTemplate.opsForValue().get("user:1");

// Hash
redisTemplate.opsForHash().put("user:1", "name", "张三");
Map<Object, Object> map = redisTemplate.opsForHash().entries("user:1");

// List(消息队列场景)
redisTemplate.opsForList().leftPush("queue:task", task);
Object task = redisTemplate.opsForList().rightPop("queue:task");

// Set
redisTemplate.opsForSet().add("online:users", userId);
Boolean isMember = redisTemplate.opsForSet().isMember("online:users", userId);

// ZSet(排行榜)
redisTemplate.opsForZSet().add("rank:score", userId, score);
Set<Object> top10 = redisTemplate.opsForZSet().reverseRange("rank:score", 0, 9);

// 通用操作
redisTemplate.expire("key", 10, TimeUnit.MINUTES);
Boolean exists = redisTemplate.hasKey("key");
redisTemplate.delete("key");

2. 缓存设计

Spring Cache 注解

java 复制代码
@Service
@CacheConfig(cacheNames = "users")  // 统一配置缓存名
public class UserService {

    // 查询时:先查缓存,缓存没有再查数据库,并将结果写入缓存
    @Cacheable(key = "#id", unless = "#result == null")
    public User getById(Long id) {
        return userRepository.findById(id).orElse(null);
    }

    // 更新时:先更新数据库,再更新缓存
    @CachePut(key = "#user.id")
    @Transactional
    public User update(User user) {
        return userRepository.save(user);
    }

    // 删除时:先删除数据库,再删除缓存
    @CacheEvict(key = "#id")
    @Transactional
    public void delete(Long id) {
        userRepository.deleteById(id);
    }

    // 清空整个缓存
    @CacheEvict(allEntries = true)
    public void clearAll() {}
}

手动缓存(更灵活)

java 复制代码
@Service
@RequiredArgsConstructor
public class ProductService {
    private final RedisTemplate<String, Object> redisTemplate;
    private final ProductRepository productRepository;

    private static final String CACHE_KEY = "product:";
    private static final long TTL_MINUTES = 30;

    public Product getProduct(Long id) {
        String key = CACHE_KEY + id;
        // 1. 查缓存
        Product cached = (Product) redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached;
        }
        // 2. 查数据库
        Product product = productRepository.findById(id).orElse(null);
        // 3. 写缓存
        if (product != null) {
            redisTemplate.opsForValue().set(key, product, TTL_MINUTES, TimeUnit.MINUTES);
        }
        return product;
    }
}

缓存更新策略

策略 说明 适用场景
Cache-Aside(旁路缓存) 读:先缓存后DB;写:先DB后删缓存 最常用,通用场景
Write-Through 写:同时写缓存和DB 数据一致性要求高
Write-Behind 写:先写缓存,异步写DB 高写场景,有丢失风险
Refresh-Ahead 缓存快过期时提前刷新 热点数据不能有缓存缺失

3. 缓存穿透、击穿、雪崩

缓存穿透

现象 :查询不存在的数据,缓存和数据库都没有,每次请求都打到数据库。
危害:恶意攻击可打垮数据库。

java 复制代码
// 解决方案1:缓存空值
public User getUser(Long id) {
    String key = "user:" + id;
    Object cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        return cached instanceof NullValue ? null : (User) cached;
    }
    User user = userRepository.findById(id).orElse(null);
    // 即使 null 也缓存,TTL 短一些(防止大量 null 占用内存)
    redisTemplate.opsForValue().set(key,
        user != null ? user : NullValue.INSTANCE, 5, TimeUnit.MINUTES);
    return user;
}

// 解决方案2:布隆过滤器(Guava)
private final BloomFilter<Long> bloomFilter =
    BloomFilter.create(Funnels.longFunnel(), 1_000_000, 0.01);  // 误判率1%

public User getUser(Long id) {
    // 布隆过滤器不存在的一定不存在,直接返回
    if (!bloomFilter.mightContain(id)) {
        return null;
    }
    // ... 正常查缓存和DB
}

缓存击穿

现象:热点 key 过期的瞬间,大量并发请求同时打到数据库。

java 复制代码
// 解决方案:互斥锁(只让一个线程查数据库,其余等待)
public User getUserWithMutex(Long id) {
    String key = "user:" + id;
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) return user;

    String lockKey = "lock:user:" + id;
    try {
        // 抢锁
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (!Boolean.TRUE.equals(locked)) {
            // 未抢到锁,短暂等待后重试
            Thread.sleep(50);
            return getUserWithMutex(id);
        }
        // 抢到锁:查数据库 + 写缓存
        user = userRepository.findById(id).orElse(null);
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        return user;
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return null;
    } finally {
        redisTemplate.delete(lockKey);
    }
}

// 解决方案2:逻辑过期(热点数据永不过期,后台异步刷新)

缓存雪崩

现象:大量 key 同时过期 OR Redis 宕机,导致所有请求打到数据库。

java 复制代码
// 解决方案1:TTL 随机偏移(防止同时过期)
long baseTTL = 30;
long randomOffset = ThreadLocalRandom.current().nextLong(0, 10);
redisTemplate.opsForValue().set(key, value, baseTTL + randomOffset, TimeUnit.MINUTES);

// 解决方案2:Redis 高可用(主从复制 + 哨兵 / Cluster 集群)
// 解决方案3:本地缓存兜底(Caffeine)
// 解决方案4:限流熔断(Redis 不可用时快速失败)

4. 定时任务

@Scheduled 注解

java 复制代码
@Component
@EnableScheduling  // 在启动类或配置类上加此注解
public class MyScheduledTask {

    // Cron 表达式:秒 分 时 日 月 周
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
    public void dailyCleanup() { ... }

    // 固定速率(从上次开始时间算起)
    @Scheduled(fixedRate = 60000)     // 每60秒
    public void heartbeat() { ... }

    // 固定延迟(从上次完成时间算起)
    @Scheduled(fixedDelay = 30000, initialDelay = 5000)
    public void syncData() { ... }
}

分布式定时任务(生产推荐)

单机 @Scheduled 在集群部署时会重复执行,需引入分布式任务框架:

框架 特点
XXL-JOB 国内主流,可视化控制台,易上手
Elastic-Job 当当开源,分片任务,适合海量数据
Spring Batch 复杂批处理场景

5. 异步任务

java 复制代码
// 启动类添加 @EnableAsync
@SpringBootApplication
@EnableAsync
public class App { ... }

// 配置线程池
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

// 使用
@Service
public class NotificationService {

    @Async
    public CompletableFuture<Void> sendEmailAsync(String to, String content) {
        // 耗时操作在独立线程中执行
        emailClient.send(to, content);
        return CompletableFuture.completedFuture(null);
    }
}

// 调用(立即返回,不阻塞主线程)
notificationService.sendEmailAsync(user.getEmail(), "欢迎注册!");

6. RabbitMQ/Kafka 消息队列

RabbitMQ 集成

yaml 复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    virtual-host: /
java 复制代码
// 发送消息
@Service
@RequiredArgsConstructor
public class MessageProducer {
    private final RabbitTemplate rabbitTemplate;

    public void send(String exchange, String routingKey, Object message) {
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
    }
}

// 消费消息
@Component
@RabbitListener(queues = "order.queue")
public class OrderConsumer {
    @RabbitHandler
    public void handleOrder(OrderMessage message) {
        // 处理订单消息
    }
}

Kafka 集成

java 复制代码
// 生产者
@Service
@RequiredArgsConstructor
public class KafkaProducer {
    private final KafkaTemplate<String, String> kafkaTemplate;

    public void send(String topic, String key, String message) {
        kafkaTemplate.send(topic, key, message)
            .thenAccept(result -> log.info("发送成功: {}", result.getRecordMetadata().offset()));
    }
}

// 消费者
@Component
public class KafkaConsumer {
    @KafkaListener(topics = "user-events", groupId = "user-service")
    public void consume(ConsumerRecord<String, String> record) {
        log.info("收到消息: key={}, value={}", record.key(), record.value());
    }
}

7. JWT 登录认证

完整登录流程

复制代码
登录请求 → 验证用户名密码 → 生成 AccessToken + RefreshToken → 返回 Token
后续请求 → Header 携带 Token → JWT Filter 拦截解析 → 放行或拒绝

JWT Filter 实现

java 复制代码
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        String token = header.substring(7);
        if (JwtUtil.isValid(token)) {
            String username = JwtUtil.getSubject(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken auth =
                new UsernamePasswordAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities());
            auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }
}

8. Spring Security

核心配置

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // 开启方法级权限(@PreAuthorize)
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter)
            throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()   // 公开接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()                   // 其余需认证
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();  // 密码加密(不可逆)
    }
}

方法级权限控制

java 复制代码
// @PreAuthorize:方法执行前校验权限
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }

@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public Page<User> listUsers() { ... }

// 访问当前用户只能看自己的数据
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public UserProfile getProfile(Long userId) { ... }

9. RBAC 权限模型

复制代码
User(用户) ←→ Role(角色) ←→ Permission(权限)
用户通过角色获得权限,权限对应具体资源操作

核心表设计

sql 复制代码
CREATE TABLE t_user   (id, username, password, ...);
CREATE TABLE t_role   (id, name, code, ...);         -- ADMIN, USER, MANAGER
CREATE TABLE t_permission (id, name, code, resource, action); -- user:delete, order:view
CREATE TABLE t_user_role       (user_id, role_id);   -- 用户-角色关联
CREATE TABLE t_role_permission (role_id, perm_id);   -- 角色-权限关联

10. API 限流

单机限流(Guava RateLimiter)

java 复制代码
// 参见 chapter04-advanced RateLimitAspect
@RateLimit(permitsPerSecond = 10.0)  // 每秒允许10次
@GetMapping("/products")
public Result<Page<Product>> list() { ... }

集群限流(Redis + Lua 令牌桶)

java 复制代码
// Lua 脚本保证原子性
private static final String RATE_LIMIT_LUA = """
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local current = tonumber(redis.call('incr', key))
    if current == 1 then
        redis.call('expire', key, ARGV[2])
    end
    if current > limit then
        return 0
    end
    return 1
    """;

public boolean allowRequest(String key, int limit, int windowSeconds) {
    Long result = redisTemplate.execute(
        RedisScript.of(RATE_LIMIT_LUA, Long.class),
        List.of(key), String.valueOf(limit), String.valueOf(windowSeconds));
    return Long.valueOf(1L).equals(result);
}

11. 数据脱敏

java 复制代码
// 参见 chapter06-architect DesensitizeUtil
@Data
public class UserVO {
    @JsonSerialize(using = DesensitizeSerializer.class)
    @Desensitize(type = DesensitizeType.PHONE)
    private String phone;      // 138****8888

    @JsonSerialize(using = DesensitizeSerializer.class)
    @Desensitize(type = DesensitizeType.EMAIL)
    private String email;      // zh***@example.com

    @JsonSerialize(using = DesensitizeSerializer.class)
    @Desensitize(type = DesensitizeType.ID_CARD)
    private String idCard;     // 330102********1234
}

12. 面试高频题

Q1:Redis 缓存穿透、击穿、雪崩的区别和解决方案?

穿透:查不存在的数据 → 空值缓存/布隆过滤器;击穿:热点key过期瞬间并发 → 互斥锁/逻辑过期;雪崩:大量key同时过期 → TTL随机偏移/Redis高可用/本地缓存兜底。

Q2:JWT 的结构是什么?

Header(算法).Payload(数据).Signature(签名),三部分 Base64 编码用 . 连接。Payload 不加密,不要存敏感数据。

Q3:如何保证消息队列的消息不丢失?

生产者:确认机制(Confirm/ACK);Broker:持久化;消费者:手动 ACK + 失败重试。

Q4:@Async 为什么有时不生效?

①启动类未加 @EnableAsync;②同类内部调用(绕过代理);③方法非 public;④未捕获的异常导致线程池拒绝。

Q5:Spring Security 的认证流程?

请求 → UsernamePasswordAuthenticationFilterAuthenticationManagerUserDetailsService.loadUserByUsername → 校验密码 → 生成 SecurityContext → 响应。

Q6:RBAC 权限模型的优点?

权限与角色关联而非直接与用户关联,降低权限管理复杂度;角色复用性高,新用户赋角色即可;易于审计和维护。


上一篇:03_数据篇 | 下一篇:05_原理篇


13. Redis 高级数据结构与实战(专家必知)

知识点 1:BitMap ------ 签到与活跃用户统计

BitMap 本质是 String 类型的二进制位操作,每个 bit 代表一个状态(0/1),极省内存。

为什么用 BitMap:统计1亿用户的月签到,用 Set 存 userId 需要 ~400MB,用 BitMap 只需 ~12MB(1亿bit ÷ 8 = 12.5MB)。

java 复制代码
@Service
@RequiredArgsConstructor
public class SignInService {

    private final RedisTemplate<String, Object> redisTemplate;

    /**
     * 用户签到
     * key格式:sign:{userId}:{year}{month}  如 sign:1001:202404
     * offset: 当月第几天(0-based),如4月1日 offset=0
     */
    public boolean signIn(Long userId) {
        String key = String.format("sign:%d:%s",
            userId, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
        int dayOfMonth = LocalDate.now().getDayOfMonth() - 1;  // 0-based
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setBit(key, dayOfMonth, true));
    }

    /**
     * 统计本月签到天数
     */
    public Long countSignDays(Long userId) {
        String key = String.format("sign:%d:%s",
            userId, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
        return redisTemplate.execute(
            (RedisCallback<Long>) conn ->
                conn.bitCount(key.getBytes()));
    }

    /**
     * 获取本月签到详情(哪些天签到了)
     */
    public List<Boolean> getMonthSignDetail(Long userId, int year, int month) {
        String key = String.format("sign:%d:%d%02d", userId, year, month);
        int daysInMonth = YearMonth.of(year, month).lengthOfMonth();
        List<Long> bitField = redisTemplate.opsForValue().bitField(key,
            BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(daysInMonth)).valueAt(0));
        List<Boolean> result = new ArrayList<>();
        if (bitField != null && !bitField.isEmpty()) {
            long bits = bitField.get(0) == null ? 0L : bitField.get(0);
            for (int i = daysInMonth - 1; i >= 0; i--) {
                result.add(0, (bits >> i & 1) == 1);
            }
        }
        return result;
    }
}

知识点 2:HyperLogLog ------ UV 统计

HyperLogLog 是概率数据结构,用极小的内存(固定12KB)统计基数(不重复元素数量),误差约 0.81%。

使用场景:每日独立访客数(UV)、搜索词去重统计。不能用 Set 因为数据量太大(亿级用户每天 Set 会占用 GB 级内存)。

java 复制代码
@Service
@RequiredArgsConstructor
public class UvCountService {

    private final StringRedisTemplate redisTemplate;

    /**
     * 记录用户访问(HyperLogLog)
     * key格式:uv:{date}  如 uv:20240428
     */
    public void recordVisit(String userId) {
        String key = "uv:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        redisTemplate.opsForHyperLogLog().add(key, userId);
        // 设置过期时间(保留30天数据)
        redisTemplate.expire(key, 30, TimeUnit.DAYS);
    }

    /**
     * 查询今日 UV
     */
    public Long getTodayUV() {
        String key = "uv:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        return redisTemplate.opsForHyperLogLog().size(key);
    }

    /**
     * 合并多天数据统计周 UV
     */
    public Long getWeeklyUV() {
        List<String> keys = new ArrayList<>();
        LocalDate today = LocalDate.now();
        for (int i = 0; i < 7; i++) {
            keys.add("uv:" + today.minusDays(i).format(DateTimeFormatter.BASIC_ISO_DATE));
        }
        String destKey = "uv:week:" + today.format(DateTimeFormatter.BASIC_ISO_DATE);
        redisTemplate.opsForHyperLogLog().union(destKey, keys.toArray(new String[0]));
        return redisTemplate.opsForHyperLogLog().size(destKey);
    }
}

知识点 3:GEO ------ 附近的人/门店

GEO 底层是 ZSet,将经纬度编码为 score,实现地理位置存储和范围查询。

java 复制代码
@Service
@RequiredArgsConstructor
public class NearbyStoreService {

    private final RedisTemplate<String, Object> redisTemplate;
    private static final String GEO_KEY = "store:geo";

    /**
     * 添加门店坐标
     */
    public void addStore(Long storeId, double longitude, double latitude) {
        redisTemplate.opsForGeo().add(GEO_KEY,
            new RedisGeoCommands.GeoLocation<>(storeId.toString(),
                new Point(longitude, latitude)));
    }

    /**
     * 查询附近门店(5km内,按距离排序,最多10个)
     */
    public List<GeoResult<RedisGeoCommands.GeoLocation<Object>>> findNearby(
            double longitude, double latitude) {
        Circle circle = new Circle(new Point(longitude, latitude),
            new Distance(5, RedisGeoCommands.DistanceUnit.KILOMETERS));

        RedisGeoCommands.GeoSearchCommandArgs args =
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                .includeDistance()   // 包含距离信息
                .sortAscending()     // 按距离从近到远排序
                .limit(10);         // 最多返回10个

        GeoResults<RedisGeoCommands.GeoLocation<Object>> results =
            redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
        return results != null ? results.getContent() : Collections.emptyList();
    }

    /**
     * 获取两门店之间的距离(单位:千米)
     */
    public Distance getDistance(Long storeId1, Long storeId2) {
        return redisTemplate.opsForGeo().distance(GEO_KEY,
            storeId1.toString(), storeId2.toString(),
            RedisGeoCommands.DistanceUnit.KILOMETERS);
    }
}

14. 消息可靠性保障:死信队列与幂等消费

知识点 1:消息可靠性三要素

text 复制代码
消息可靠性 = 生产者确认 + Broker持久化 + 消费者手动ACK

三处都做到,才能保证消息不丢失:

【生产者侧】
  RabbitMQ: publisher-confirms + publisher-returns
  Kafka: acks=all(所有副本确认才算成功)

【Broker 侧】
  RabbitMQ: 队列和消息都设置 durable=true
  Kafka: 消息默认持久化到磁盘

【消费者侧】
  手动 ACK:业务处理成功后才确认,失败则 NACK 重新入队
  幂等处理:同一消息重复消费不产生重复结果

知识点 2:RabbitMQ 死信队列(DLQ)

死信来源:

  1. 消息被 NACK/Reject 且 requeue=false
  2. 消息 TTL 过期
  3. 队列达到最大长度
java 复制代码
// 死信队列配置
@Configuration
public class RabbitMQConfig {

    // 普通队列(绑定死信交换机)
    @Bean
    public Queue orderQueue() {
        return QueueBuilder.durable("order.queue")
            .withArgument("x-dead-letter-exchange", "order.dlx")   // 死信交换机
            .withArgument("x-dead-letter-routing-key", "order.dlq.key") // 死信路由Key
            .withArgument("x-message-ttl", 30000)                  // 消息30秒过期
            .withArgument("x-max-length", 10000)                   // 最大10000条
            .build();
    }

    // 死信交换机
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange("order.dlx");
    }

    // 死信队列(用于人工处理/告警)
    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable("order.dlq").build();
    }

    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue())
            .to(deadLetterExchange())
            .with("order.dlq.key");
    }
}

// 消费者:手动ACK + 失败进死信
@Component
@RabbitListener(queues = "order.queue")
public class OrderConsumer {

    @RabbitHandler
    public void handleOrder(OrderMessage message, Channel channel,
                            @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            // 幂等校验(防重复消费)
            if (idempotentService.isDuplicate(message.getMessageId())) {
                channel.basicAck(deliveryTag, false);  // 重复消息直接确认
                return;
            }

            orderService.processOrder(message);

            // 处理成功:手动ACK
            channel.basicAck(deliveryTag, false);

            // 标记已处理
            idempotentService.markProcessed(message.getMessageId());

        } catch (BusinessException e) {
            // 业务异常:NACK,不重新入队(进入死信队列)
            log.error("订单处理失败,进入死信: {}", message, e);
            channel.basicNack(deliveryTag, false, false);
        } catch (Exception e) {
            // 系统异常:NACK,重新入队(最多3次)
            boolean requeue = message.getRetryCount() < 3;
            log.error("系统异常,requeue={}: {}", requeue, message, e);
            channel.basicNack(deliveryTag, false, requeue);
        }
    }
}

知识点 3:幂等性实现(Redis 防重)

java 复制代码
@Service
@RequiredArgsConstructor
public class IdempotentService {

    private final StringRedisTemplate redisTemplate;
    private static final String PREFIX = "msg:processed:";
    private static final long TTL_DAYS = 7;  // 保留7天,覆盖消息重试时间窗口

    /**
     * 检查并标记(原子操作,防止并发重复)
     * 返回 true 表示重复消息,应直接 ACK 跳过
     */
    public boolean checkAndMark(String messageId) {
        String key = PREFIX + messageId;
        // SETNX:不存在则设置,成功返回true(首次处理);失败返回false(已处理)
        Boolean isNew = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", TTL_DAYS, TimeUnit.DAYS);
        return Boolean.FALSE.equals(isNew);  // true 表示重复
    }
}

15. 接口签名防篡改(专家安全能力)

知识点:为什么需要接口签名

HTTPS 保证传输加密,但无法防止:

  • 参数被篡改(中间人修改金额等)
  • 请求被重放(截获合法请求重复发送)

接口签名通过对请求参数做 HMAC 哈希,保证参数完整性和时效性。

java 复制代码
/**
 * 签名规则:
 * 1. 收集所有请求参数(key 字母序排序)
 * 2. 拼接格式:key1=v1&key2=v2&timestamp=xxx&nonce=yyy
 * 3. HMAC-SHA256(拼接字符串, secretKey) → sign
 * 4. 请求头携带:X-Timestamp、X-Nonce、X-Sign
 * 5. 服务端验证:时间戳5分钟内有效 + nonce未使用 + 签名匹配
 */
@Component
public class SignatureFilter extends OncePerRequestFilter {

    private final StringRedisTemplate redisTemplate;
    private final String secretKey = "your-hmac-secret-key";
    private static final long TIMESTAMP_EXPIRE_SECONDS = 300;  // 5分钟

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        // 1. 获取签名相关头信息
        String timestamp = request.getHeader("X-Timestamp");
        String nonce = request.getHeader("X-Nonce");
        String sign = request.getHeader("X-Sign");

        if (timestamp == null || nonce == null || sign == null) {
            chain.doFilter(request, response);  // 非必须签名接口直接放行
            return;
        }

        // 2. 验证时间戳(防重放,5分钟内有效)
        long requestTime = Long.parseLong(timestamp);
        long now = System.currentTimeMillis() / 1000;
        if (Math.abs(now - requestTime) > TIMESTAMP_EXPIRE_SECONDS) {
            sendError(response, "请求已过期");
            return;
        }

        // 3. 验证 nonce 唯一性(防重放同一请求)
        String nonceKey = "nonce:" + nonce;
        if (Boolean.FALSE.equals(redisTemplate.opsForValue()
                .setIfAbsent(nonceKey, "1", TIMESTAMP_EXPIRE_SECONDS, TimeUnit.SECONDS))) {
            sendError(response, "重复请求");
            return;
        }

        // 4. 计算并验证签名
        Map<String, String> params = extractParams(request);
        params.put("timestamp", timestamp);
        params.put("nonce", nonce);
        String expectedSign = generateHmacSign(params, secretKey);

        if (!expectedSign.equals(sign)) {
            sendError(response, "签名验证失败");
            return;
        }

        chain.doFilter(request, response);
    }

    private String generateHmacSign(Map<String, String> params, String key) {
        // 参数按 key 字母序排序拼接
        String content = params.entrySet().stream()
            .sorted(Map.Entry.comparingByKey())
            .map(e -> e.getKey() + "=" + e.getValue())
            .collect(Collectors.joining("&"));
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] hash = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("签名计算失败", e);
        }
    }
}

16. Redis 核心数据结构完整用法

知识点 1:String ------ 最通用的数据结构

java 复制代码
@Service
@RequiredArgsConstructor
public class StringRedisDemo {

    private final StringRedisTemplate redis;

    public void demonstrate() {
        // 基础操作
        redis.opsForValue().set("key", "value");
        redis.opsForValue().set("key", "value", 10, TimeUnit.MINUTES); // 带过期时间
        String val = redis.opsForValue().get("key");

        // 原子计数(如访问量统计)
        redis.opsForValue().set("page:views:home", "0");
        redis.opsForValue().increment("page:views:home");     // +1
        redis.opsForValue().increment("page:views:home", 5); // +5
        Long count = Long.parseLong(redis.opsForValue().get("page:views:home"));

        // 分布式锁(原子 SETNX)
        Boolean locked = redis.opsForValue()
            .setIfAbsent("lock:resource", "1", 10, TimeUnit.SECONDS);

        // 缓存(序列化 Java 对象)
        RedisTemplate<String, Object> objRedis = ...;
        objRedis.opsForValue().set("user:1", user, 30, TimeUnit.MINUTES);
        User user = (User) objRedis.opsForValue().get("user:1");
    }
}

知识点 2:Hash ------ 存储对象,支持字段级操作

java 复制代码
// 场景:购物车(每个用户一个 Hash,商品ID为field,数量为value)
@Service
@RequiredArgsConstructor
public class CartService {

    private final StringRedisTemplate redis;

    public void addToCart(Long userId, Long productId, int quantity) {
        String key = "cart:" + userId;
        redis.opsForHash().put(key, productId.toString(), String.valueOf(quantity));
        redis.expire(key, 7, TimeUnit.DAYS); // 7天过期
    }

    public Map<Object, Object> getCart(Long userId) {
        return redis.opsForHash().entries("cart:" + userId);
    }

    public void updateQuantity(Long userId, Long productId, int delta) {
        String key = "cart:" + userId;
        // increment 是原子操作,安全
        redis.opsForHash().increment(key, productId.toString(), delta);
    }

    public void removeItem(Long userId, Long productId) {
        redis.opsForHash().delete("cart:" + userId, productId.toString());
    }
}

知识点 3:List ------ 消息队列与时间线

java 复制代码
@Service
@RequiredArgsConstructor
public class MessageQueue {

    private final StringRedisTemplate redis;
    private static final String QUEUE_KEY = "message:queue";

    // 生产者:从左推入(LPUSH)
    public void produce(String message) {
        redis.opsForList().leftPush(QUEUE_KEY, message);
    }

    // 消费者:从右弹出(RPOP → 先进先出队列)
    public String consume() {
        // BRPOP 阻塞式弹出(无消息时阻塞等待,最多5秒)
        List<String> result = redis.opsForList().rightPop(QUEUE_KEY, 5, TimeUnit.SECONDS);
        return result != null ? result.get(1) : null;  // index 0=key, 1=value
    }

    // 消息时间线(只保留最新100条)
    public void addTimeline(String userId, String content) {
        String key = "timeline:" + userId;
        redis.opsForList().leftPush(key, content);
        redis.opsForList().trim(key, 0, 99); // 只保留前100条
    }
}

知识点 4:ZSet(有序集合)------ 排行榜

java 复制代码
@Service
@RequiredArgsConstructor
public class RankingService {

    private final StringRedisTemplate redis;
    private static final String RANKING_KEY = "game:ranking";

    // 更新用户分数
    public void updateScore(String userId, double scoreToAdd) {
        redis.opsForZSet().incrementScore(RANKING_KEY, userId, scoreToAdd);
    }

    // 获取 Top N(分数从高到低)
    public List<RankItem> getTopN(int n) {
        Set<ZSetOperations.TypedTuple<String>> tuples =
            redis.opsForZSet().reverseRangeWithScores(RANKING_KEY, 0, n - 1);
        List<RankItem> ranking = new ArrayList<>();
        int rank = 1;
        for (ZSetOperations.TypedTuple<String> tuple : tuples) {
            ranking.add(new RankItem(rank++, tuple.getValue(), tuple.getScore()));
        }
        return ranking;
    }

    // 获取用户排名
    public Long getUserRank(String userId) {
        Long rank = redis.opsForZSet().reverseRank(RANKING_KEY, userId);
        return rank != null ? rank + 1 : null; // reverseRank 从0开始,+1 变成人类可读排名
    }

    // 获取用户分数
    public Double getUserScore(String userId) {
        return redis.opsForZSet().score(RANKING_KEY, userId);
    }
}

知识点 5:Set ------ 去重与集合运算

java 复制代码
@Service
@RequiredArgsConstructor
public class SocialService {

    private final StringRedisTemplate redis;

    // 关注关系(用 Set 存储)
    public void follow(Long userId, Long targetId) {
        redis.opsForSet().add("following:" + userId, targetId.toString());
        redis.opsForSet().add("followers:" + targetId, userId.toString());
    }

    // 共同关注(交集)
    public Set<String> commonFollowing(Long userId1, Long userId2) {
        return redis.opsForSet().intersect(
            "following:" + userId1, "following:" + userId2);
    }

    // 推荐关注(userId1 的关注 - userId2 的关注 = userId2 没关注的)
    public Set<String> recommendFollow(Long userId1, Long userId2) {
        return redis.opsForSet().difference(
            "following:" + userId1, "following:" + userId2);
    }

    // 防重复抽奖
    public boolean participate(String activityId, String userId) {
        // SADD 返回添加成功的元素个数,1表示新增(未参与),0表示已存在(已参与)
        Long added = redis.opsForSet().add("activity:" + activityId + ":participants", userId);
        return Long.valueOf(1L).equals(added);
    }
}

17. 分布式锁:Redisson 完整实战

知识点 1:为什么要用 Redisson 而非自己实现

text 复制代码
自己用 SETNX 实现分布式锁的问题:

问题1:锁未能正确释放(异常导致 finally 未执行,死锁)
问题2:业务超时,锁自动过期,其他线程进入(锁提前失效)
问题3:Redis 主节点宕机,锁信息丢失(从节点未同步)
问题4:判断是不是自己的锁 + 删除 不是原子操作(可能删除别人的锁)

Redisson 解决方案:
  - WatchDog(看门狗):每30秒自动续期,业务结束才释放
  - Lua 脚本:保证判断+删除的原子性
  - RedLock:支持多个 Redis 节点,防止主从切换导致锁丢失
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.29.0</version>
</dependency>
yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379

知识点 2:Redisson 分布式锁实战

java 复制代码
@Service
@RequiredArgsConstructor
@Slf4j
public class StockService {

    private final RedissonClient redisson;
    private final ProductMapper productMapper;

    /**
     * 分布式锁保护的库存扣减
     */
    public boolean deductStock(Long productId, int quantity) {
        // 1. 获取锁对象(锁 key 建议业务+资源ID)
        RLock lock = redisson.getLock("lock:stock:" + productId);

        try {
            // 2. 尝试获取锁
            // tryLock(等待时间, 锁超时时间, 时间单位)
            // 等待3秒,拿不到锁就放弃(防止长时间等待)
            // 锁30秒后自动释放(WatchDog 会在业务未完成时自动续期)
            boolean acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!acquired) {
                log.warn("获取库存锁失败,productId={}", productId);
                return false;
            }

            // 3. 执行临界区代码
            Product product = productMapper.selectById(productId);
            if (product.getStock() < quantity) {
                throw new BusinessException(ResultCode.STOCK_NOT_ENOUGH);
            }
            product.setStock(product.getStock() - quantity);
            productMapper.updateById(product);
            log.info("库存扣减成功: productId={}, quantity={}, 剩余={}",
                productId, quantity, product.getStock());
            return true;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            // 4. 必须在 finally 中释放锁(只有当前线程持有的锁才能释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 公平锁:多个线程等待时,按等待顺序获取锁(防止饥饿)
     */
    public void fairLockDemo(Long resourceId) {
        RLock fairLock = redisson.getFairLock("fair:lock:" + resourceId);
        // 用法与普通锁相同
    }

    /**
     * 读写锁:读读不互斥,读写/写写互斥
     * 适合:读多写少的场景(读取缓存时加读锁,更新时加写锁)
     */
    public String readWithLock(Long key) {
        RReadWriteLock rwLock = redisson.getReadWriteLock("rw:lock:" + key);
        RLock readLock = rwLock.readLock();
        readLock.lock();
        try {
            return fetchData(key); // 多个线程可以并发读
        } finally {
            readLock.unlock();
        }
    }

    public void writeWithLock(Long key, String data) {
        RReadWriteLock rwLock = redisson.getReadWriteLock("rw:lock:" + key);
        RLock writeLock = rwLock.writeLock();
        writeLock.lock();
        try {
            saveData(key, data); // 写时独占,其他读写都要等待
        } finally {
            writeLock.unlock();
        }
    }
}

18. Spring Security 完整认证体系

知识点 1:Spring Security 过滤器链

text 复制代码
请求到达 Spring Security 的过滤器链(按顺序执行):

1. DisableEncodeUrlFilter           → 禁止 URL 中携带 sessionId
2. SecurityContextHolderFilter      → 初始化/清理 SecurityContextHolder
3. UsernamePasswordAuthenticationFilter → 处理 /login 表单登录
4. BasicAuthenticationFilter        → 处理 HTTP Basic 认证
5. BearerTokenAuthenticationFilter  → 处理 JWT Bearer Token(需配置)
6. [自定义过滤器]                    → 如 JwtAuthFilter
7. AuthorizationFilter              → 权限检查(最后一道关卡)

过滤器在 Controller 之前执行!

知识点 2:完整 Security 配置(JWT + 无状态)

java 复制代码
@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // 开启方法级权限控制(@PreAuthorize)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // 禁用 CSRF(前后端分离 + JWT 无状态,不需要)
            .csrf(AbstractHttpConfigurer::disable)

            // 禁用 Session(完全无状态)
            .sessionManagement(sm ->
                sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

            // 请求权限配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()         // 登录注册不需要认证
                .requestMatchers("/actuator/health").permitAll()     // 健康检查
                .requestMatchers("/api/admin/**").hasRole("ADMIN")  // 管理员接口
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // 查询接口公开
                .anyRequest().authenticated()                        // 其余需要认证
            )

            // 添加 JWT 过滤器(在用户名密码过滤器之前)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

            // 自定义异常处理
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) -> {
                    // 未认证(401)
                    res.setStatus(401);
                    res.setContentType("application/json;charset=UTF-8");
                    res.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}");
                })
                .accessDeniedHandler((req, res, e) -> {
                    // 已认证但权限不足(403)
                    res.setStatus(403);
                    res.setContentType("application/json;charset=UTF-8");
                    res.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
                })
            )
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);  // 强度12(默认10)
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

// 方法级权限控制
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")  // 需要 ADMIN 角色
    public Result<List<User>> listAll() { ... }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    // 管理员 或者 本人才能删除
    public Result<Void> delete(@PathVariable Long id) { ... }

    @GetMapping("/me")
    public Result<UserDTO> getCurrentUser(
            @AuthenticationPrincipal UserDetails userDetails) {
        // 直接从 SecurityContext 获取当前用户,无需从 Header 手动解析
        return Result.success(userService.findByUsername(userDetails.getUsername()));
    }
}
相关推荐
介一安全1 小时前
【Web安全】Blind XSS漏洞:从挖掘到防御
安全·web安全·xss
土豆.exe1 小时前
Cast Attack:Java 中 Ghost Bits(幽灵比特)引发的新型安全威胁——Java 生态里被忽视的底层风险引发一系列绕过
java·python·安全
YaBingSec2 小时前
玄机网络安全靶场:Jackson-databind 反序列化漏洞(CVE-2017-7525)
linux·网络·笔记·安全·web安全
TechWayfarer2 小时前
网络安全溯源实战:78.1%网络攻击来自境外,如何精准定位攻击源
网络·安全·web安全
视觉&物联智能2 小时前
【杂谈】-人工智能于现代网络安全运营的价值持续攀升
人工智能·安全·web安全·ai·chatgpt·agi·deepseek
上海云盾第一敬业销售2 小时前
物联网设备暴露面激增,WAF如何守护边缘计算安全?
物联网·安全·边缘计算
人道领域3 小时前
【黑马点评日记】Redis分布式锁终极方案:Redisson全面解析(含源码解析)
java·数据库·redis·分布式·缓存
老赵聊算法、大模型备案3 小时前
从剪映、即梦 AI 被罚,读懂 AI 生成内容标识硬性合规要求
人工智能·算法·安全·aigc
BullSmall3 小时前
Redis AOF 文件损坏报错:完整修复方案
数据库·redis·缓存