苍穹外卖
- 项目描述:本项目是一款外卖订单管理系统,是基于SpringBoot实现,包括后台管理端和用户端两部分。管理端可以对商品的分类、订单、员工等信息进行管理维护,统计各类数据,以及发放优惠卷; 用户可以通过此平台浏览菜单、下单支付,并跟踪订单状态。餐厅端则可以接收订单、管理库存,并安排配送。支持商家管理、用户下单、支付及数据可视化。
- 技术栈:Spring Boot、MySQL、Mybatis、Redis、JWT、WebSocket、Swagger、Nginx
- 工作职责:
- 使用JWT令牌技术和自定义拦截器实现用户无状态认证,并使用ThreadLocal存储用户信息;
- 使用Redis缓存热点商品信息,减少数据库查询压力,接口响应时间降低约30%;
- 采用“缓存预热+双删策略”解决缓存击穿问题,QPS提升40%;
- 利用AOP+自定义注解 实现公共字段自动填充(如创建时间、更新时间),减少冗余代码;
- 通过WebSocket向前端传输数据,实现来单提醒功能,以及利用SpringTask定时任务实现订单的超时处理,超时自动取消订单;
- 通过Nginx反向代理实现前后端分离部署,并通过Swagger生成API文档;
- 实现了分布式锁机制,确保订单处理的高一致性与数据的最终一致性;
使用JWT令牌技术和自定义拦截器实现用户无状态认证,并使用ThreadLocal存储用户信息;
# JWT令牌的结构
- Header:包含算法和类型(如
{"alg": "HS256", "typ": "JWT"}
)。 - Payload:包含用户信息和其他数据(如
{"sub": "123", "name": "John"}
)。 - Signature:对 Header 和 Payload 的签名,用于验证 Token 的完整性。
# 确保JWT令牌的安全性
防止令牌被盗用或重放攻击
使用https通过加密通道传输JWT,通过将JWt存储在Cookie中,设置HttpOnly属性防止XSS脚本攻击
设置较短的 Token 有效期,减少令牌泄漏的影响,使用双token,Refresh Token通常存储在更安全的地方,Access Token即使泄漏也会很快失效。
# 项目中的自定义拦截器是怎么实现的
项目中通过实现HandlerInterceptor
接口创建拦截器,核心逻辑在preHandle
方法中完成JWT解析和用户认证,并将用户ID存入ThreadLocal
供后续流程使用。
然后在实现了WebMvcConfigurer接口的configuration文件中,注册拦截器并指定拦截路径(如/api/**
),并排除登录等接口。
# 拦截器和过滤器的区别
它们的主要区别体现在三个方面:
- 作用时机:过滤器在Servlet容器层面,拦截器在Spring MVC层面;
- 功能范围:拦截器能访问Spring Bean和Controller元数据,过滤器更接近原生Servlet;
- 使用场景:过滤器适合处理全局逻辑(如安全过滤),拦截器适合业务逻辑(如权限校验)。
实际项目中,我们通常用过滤器处理跨域和XSS防御,用拦截器实现JWT认证和日志记录。两者互补,共同构成请求处理链路。
# ThreadLocal存储用户信息时,如何避免内存泄漏问题?
ThreadLocal 如果使用不当,确实可能导致内存泄漏,核心原因是 ThreadLocalMap 的 key 是弱引用,而 value 是强引用,加上线程复用(如线程池),无效的 Entry 无法被及时清理。
ThreadLocal 的数据存储在 线程的 ThreadLocalMap 中,key 是 ThreadLocal 对象本身, 它是弱引用,会被 GC 回收变为 null,但 Value 仍被 Entry 强引用,导致无法回收。
当线程一直存在不被清除时,比如线程池复用时,ThreadLocalMap 会一直存在
ThreadLocal有两种情况导致OOM:
当 ThreadLocal 是局部变量,用完被清除,失去强引用,key 因弱引用被 GC 回收变为null,而value会一直存在导致OOM。 当ThreadLocal 是静态变量。虽然 key 不会被回收(静态变量是强引用),但如果线程复用时不调用 remove()
,多次 set()
会导致旧 value 无法释放(例如线程池任务中重复使用同一个 ThreadLocal)。
解决方法:
threadLocalMap有清除机制,会在调用set() / get() 时自动清除key为null的数据。 但是使用ThreadLocal 时通常把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
# 为什么选择JWT而不是传统的Session机制
JWT是无状态的,服务器无需存储任何会话状态,不需要额外查询数据库或缓存。天然支持分布式系统,利于系统服务器扩展,
# 在什么场景下JWT可能不是最佳选择?
jwt是不加密的,不能传输敏感信息,如果存储敏感数据,可以对JWT进行加密或者使用Session将敏感数据存储在服务器中,
jwt是无状态的,没有办法取消令牌强制用户下线,只能把用户状态保存下来,但也改变了它的作用
使用Redis缓存热点商品信息,并使用Spring Cache优化代码,接口响应时间降低约30%;
# 如何确定哪些商品属于"热点商品"的
我们的项目是直接将热点商品缓存在Redis中, 只有商家修改商品时会对缓存进行删除,保证商品的强一致性,通过使用Redis的全key-LFU淘汰策略来保证
# Spring Cache的核心注解有哪些?各自的作用是什么?
@EnableCaching
: 开启缓存注解功能,通常加在启动类上@Cacheable
: 用于查询方法,先检查缓存,存在则直接返回,不存在则执行方法并将结果存入缓存@CachePut
: 总是执行方法,并将结果更新到缓存(常用于更新操作)@CacheEvict
: 清除缓存(常用于删除或更新操作)
# 如何实现条件化缓存
虽然项目中没有使用到,但是可以使用condition
和unless
属性
condition
: 在方法执行前判断是否使用缓存unless
: 在方法执行后判断是否缓存结果
# 你们项目是怎么解决缓存击穿问题的
因为我们的项目只涉及一个商家,缓存的信息较少,我们的缓存没有设置TTL,所以不存在缓存击穿问题,通过LRU淘汰策略淘汰不常用的缓存
利用AOP+自定义注解 实现公共字段自动填充(如创建时间、更新时间),减少冗余代码;
# 请说明如何实现公共字段的自动填充
"我们采用AOP+自定义注解的方案,我们设置了自定义注解,主要组合了两个注解Target、Retention,标注了注解的使用范围,通过切面拦截标记了自定义注解的Mapper层方法,再通过反射,自动填充创建时间、更新时间等公共字段"
通过WebSocket向前端传输数据,实现来单提醒功能,以及利用RabbbitMQ实现订单的超时处理;
# SpringTask定时任务
在启动类上通过注解EnableScheduling开启定时任务,就可以在通过@Scheduled注解使用cron表达式设置定时任务的执行频率
# 保证集群环境下的任务不重复执行
用redis分布式锁
# 使用RabbitMQ处理超时订单(项目优化)
# 如何确保消息不丢失
消息丢失的三个途径:
生产者发送、确认消息的过程、在消息队列的丢失、消费者发送、处理、确认的过程
- 生产者:可以通过消息重试、确认等确保消息成功到达 RabbitMQ。不过SpringAMQP提供的是阻塞式的重试,会影响业务性能,如果要使用需要配置合理的等待时长和重试次数,考虑使用异步线程来执行发送消息的代码。
- 持久化(Durable):交换机、队列、消息都持久化,防止服务器重启丢失。
- 消费者手动 ACK:处理完成后再确认,避免消费者崩溃导致消息丢失。
# 如何设计幂等性(Idempotency)来应对重复消息?
通过判断订单是否已经为待支付状态来实现幂等性,
# WebSocket
# 为什么选择WebSocket而不是轮询
WebSocket提供全双工通信,订单创建后立即推送,延迟低。适合高并发场景,减少服务器压力。而且订单是大部分集中在一段时间