目标 :掌握 Spring Boot 在实际项目中的增强能力
学习时长 :3~4 周
前置要求:完成数据篇,了解 Redis 基础
目录
- [Redis 集成](#Redis 集成)
- 缓存设计
- 缓存穿透、击穿、雪崩
- 定时任务
- 异步任务
- [RabbitMQ/Kafka 消息队列](#RabbitMQ/Kafka 消息队列)
- [JWT 登录认证](#JWT 登录认证)
- [Spring Security](#Spring Security)
- [RBAC 权限模型](#RBAC 权限模型)
- [API 限流](#API 限流)
- 数据脱敏
- 面试高频题
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 的认证流程?
请求 →
UsernamePasswordAuthenticationFilter→AuthenticationManager→UserDetailsService.loadUserByUsername→ 校验密码 → 生成SecurityContext→ 响应。
Q6:RBAC 权限模型的优点?
权限与角色关联而非直接与用户关联,降低权限管理复杂度;角色复用性高,新用户赋角色即可;易于审计和维护。
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)
死信来源:
- 消息被 NACK/Reject 且 requeue=false
- 消息 TTL 过期
- 队列达到最大长度
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×tamp=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()));
}
}