一、点评星球(黑马点评)
1、项目概述
1.1、项目简介
本项目是基于Spring Boot与Redis深度整合的前后端分离的点评平台。系统以Redis为核心技术支撑,重点解决高并发场景下的缓存穿透、击穿、雪崩等问题,涵盖商户展示、优惠券秒杀、达人探店、社交互动等创新功能。
1.2、项目亮点
- 使用 Redis 解决了在集群模式下的 Session共享问题,使用双拦截器实现用户的登录校验和权限刷新
- 运用Cache Aside模式解决数据库与缓存的一致性问题,通过主动更新结合超时删除保证最终一致性
- 使用 Redis 对高频访问的信息进行缓存,降低数据库查询压力,解决了缓存穿透、雪崩、击穿问题
- 使用 Redis + Lua脚本实现对用户秒杀资格的预检,同时用乐观锁解决秒杀产生的超卖问题
- 使用 Redisson分布式锁解决在集群模式下一人一单的线程安全问题
- 基于RabbitMQ作为消息队列,实现异步秒杀下单
- 使用Redis的 ZSet 数据结构实现了点赞排行榜功能,使用Set 集合实现关注、共同关注功能
1.3、核心功能
用户服务:
- 短信登录(Redis替代Session实现集群共享,双拦截器保障Token刷新)
商户服务:
- 多级缓存优化(Cache Aside模式 + Redisson读写锁保障双写一致性)
- 缓存异常处理:空值缓存防穿透、互斥锁/逻辑过期防击穿、随机TTL防雪崩
秒杀服务:
- 分布式锁(Redisson实现)解决集群环境下“一人一单”线程安全问题
- 异步下单优化:Redis Lua脚本预检库存 + RabbitMQ消息队列异步处理订单
社交服务:
- 达人探店笔记(发布、点赞排行榜基于ZSet实现)及好友关注(Set集合实现共同关注)
2、使用 Redis 解决了在集群模式下的 Session 共享问题,使用拦截器实现了用户的登录校验和权限刷新
3、运用Cache Aside模式解决数据库与缓存的一致性问题,通过主动更新结合超时删除保证最终一致性
4、使用 Redis 对高频访问的信息进行缓存,降低数据库查询压力,解决了缓存穿透、雪崩、击穿问题
5、使用 Redis + Lua脚本实现对用户秒杀资格的预检,同时用乐观锁解决秒杀产生的超卖问题
6、使用 Redisson分布式锁解决在集群模式下一人一单的线程安全问题
7、基于RabbitMQ作为消息队列,实现异步秒杀下单
8、使用Redis的 ZSet 数据结构实现了点赞排行榜功能,使用Set 集合实现关注、共同关注功能
二、校园生活服务平台(苍穹外卖)
1、项目概述
1.1、项目简介
本项目是基于Spring Boot的校园生活服务系统,分为管理端(供校内商家使用)和用户端(微信小程序)。管理端包括员工信息、商品及分类的管理,订单状态跟踪等功能;用户端在线浏览商品,添加购物车及下单等功能。
1.2、项目亮点
- 登录及身份验证使用JWT令牌技术,用自定义拦截器完成用户认证,通过ThreadLocal优化鉴权逻辑
- 使用Redis缓存高频数据如同分类商品,并使用SpringCache优化代码,提高系统性能和响应速度
- 使用Nginx用作HTTP服务器,部署静态资源,反向代理和负载均衡
- 通过webSocket实现客户端与服务端的长连接,并实现来单提醒及客户催单等功能
- 使用SpringTask实现订单状态的定时处理,超时自动取消订单等功能
1.3、核心功能
管理端
1、用户登录(JWT、ThreadLocal)
2、员工信息维护
3、商品管理
4、分类管理
5、订单状态跟踪
6、来单/催单提醒(webSocket实时推送)
7、Spring Cache缓存菜品信息
8、定时任务处理超时订单(Spring Task)
用户端(微信小程序)
1、微信登陆
2、商品浏览(Redis缓存)
3、购物车管理
4、下单支付
1.4 模块作用
序号 | 模块 | 作用 |
---|---|---|
1 | sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
2 | sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
3 | sky-pojo | 子模块,存放实体类、VO、DTO等 |
4 | sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
2、JWT令牌+ThreadLocal
2.1、JWT登录流程
使用JWT令牌和自定义拦截器完成用户认证的流程如下:
- 用户登录时,客户端发送用户名和密码给服务器请求令牌,服务器验证通过后生成包含用户信息的JWT令牌并返回给客户端。
- 客户端收到JWT令牌后将其存储在本地(如localStorage或cookie),后续每次请求都在请求头中携带该令牌(如Authorization:Bearer )。
- 服务器端的自定义拦截器会拦截所有请求,首先从请求头中提取JWT令牌并进行解析验证(包括检查令牌是否存在、签名是否有效、是否过期等),若令牌不存在或验证失败则直接返回401未认证错误。
- 当JWT令牌验证通过后,拦截器将解析出的用户信息存储在ThreadLocal中,使本次请求的后续业务处理流程可以直接获取用户信息,避免重复解析JWT令牌。
- 请求处理完成后,拦截器会调用remove()方法清除ThreadLocal中的用户信息,确保不会发生内存泄漏。
2.2、ThreadLocal使用过多会造成的影响?怎么解决内存泄漏的问题?
每个线程都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。垃圾回收的时候会⾃动回收key,但对应的 Value 仍然是强引用,且线程未结束时,ThreadLocalMap 会一直持有该 Value,导致内存泄漏。
解决⽅法:每次使⽤完ThreadLocal就调⽤它的remove()⽅法,手动将对应的键值对删除,从⽽避免内存泄漏
2.3 JWT的组成:
它由三部分组成:header(头部)、payload(载荷)、signature(签名)
- Header(头部) 作用:描述令牌的元数据,如签名算法(如HS256、RS256)和令牌类型(固定为JWT)。
- Payload(负载) 作用:携带实际的数据(声明),分为三类:
预定义声明:标准字段,如 iss(签发者)、exp(过期时间)、sub(主题)等。
公开声明:自定义公共字段,需避免冲突(建议通过IANA 注册)。
私有声明:双方约定的自定义数据。 - Signature(签名)作用:验证令牌的完整性和真实性。生成方式:将编码后的 Header 和 Payload 拼接后通过 Header 中指定的算法(如 HS256)和密钥进行签名。
3、Redis
3.1项目中哪里用到redis了,缓存的粒度是什么?key,value如何定义?
缓存商品 用户端小程序展示的商品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。结果是系统响应慢、用户体验差。通过Redis来缓存商品数据,减少数据库查询操作。缓存逻辑是每个分类的商品下缓存一份数据,数据库中商品数据有变更时及时清理数据。key是分类的id,value是该分类下所有商品信息
3.2、数据库与redis如何实现的数据同步
在项目中,我们采用分布式锁策略实现数据库与Redis的数据同步。通过Redis分布式锁(如Redisson)对数据ID细粒度加锁,确保同一时间只有一个请求能修改数据;在锁内严格按"先更新数据库→再删除缓存"的顺序操作,保证后续请求必然读取最新数据;
因为有库存价格等信息所以不适合用延时双删)。
修改频率较低所以加锁性能损耗不高
4、Nginx
4.1 负载均衡:
Nginx 的负载均衡功能允许将请求分发给多个应用服务器,以均衡负载和提高系统的可扩展性和可靠性。下面是一些常用的 Nginx 负载均衡配置方法:
- 轮询:这是默认的负载均衡策略。Nginx 将请求依次分发给每个后端服务器,确保每个服务器都能获得相同的请求数量。
- IP 哈希(IP Hash):Nginx 使用客户端 IP地址的哈希值来决定将请求发送给哪个后端服务器。
- 加权轮询:可以为每个后端服务器设置权重,高权重的服务器将获得更多的请求。这种方式可以根据服务器的性能和处理能力来分配负载。
我们项目用的是轮询方式,共有2台后端服务器(一台本机,一台虚拟机)
在nginx.conf配置:
upstream webservers{
server 127.0.0.1:8080 weight=90 ;
#server 127.0.0.1:8088 weight=10 ;
}
4.2 反向代理与正向代理:
反向代理隐藏服务器,正向代理隐藏客户端。
- 正向代理是客户端发送请求后通过代理服务器访问目标服务器,代理服务器代表客户端发送请求并将响应返回给客户端。正向代理隐藏了客户端的真实身份和位置信息,为客户端提供代理访问互联网的功能。
- 反向代理是位于目标服务器和客户端之间的代理服务器,它代表服务器接收客户端的请求并将请求转发到真正的目标服务器上,并将得到的响应返回给客户端。反向代理隐藏了服务器的真实身份和位置信息,客户端只知道与反向代理进行通信,而不知道真正的服务器。
反向代理优点:
提高访问速度,因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
进行负载均衡,把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
保证后端服务安全因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
反向代理配置。在nginx.conf配置
server {
listen 80;
server_name localhost;
# 反向代理,处理管理端发送的请求
location /api/ {
proxy_pass http://localhost:8080/admin/;
#proxy_pass http://webservers/admin/;
}
# 反向代理,处理用户端发送的请求
location /user/ {
proxy_pass http://webservers/user/;
}
}
5、webSocket
5.1 什么是webSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
5.2、WebSocket和HTTP协议的区别?为什么不用HTTP
特性 | WebSocket | HTTP |
---|---|---|
连接性质 | 全双工(双向实时通信) | 半双工(单向请求-响应) |
持久性 | 长连接(建立后持续保持) | 短连接(默认每次请求后关闭) |
主动推送 | 服务端可主动推送数据 | 服务端只能被动响应客户端请求 |
这两个功能对实时性要求极高,传统的HTTP请求-响应模式无法满足毫秒级推送的需求,因此我们采用WebSocket协议实现服务端主动推送,确保商家能第一时间处理订单。
5.3 WebSocket在项目中的应用场景
我们使用WebSocket实现了两个核心功能: ✅ 来单实时提醒:当用户下单并支付成功后,系统立即通知商家端有新订单。 ✅ 客户催单处理:用户点击催单按钮后,商家端实时收到催单通知,并触发语音播报。
通过WebSocket实现管理端页面和服务端保持长连接状态。当客户支付完成或者点击催单后,调用WebSocket的相关API实现服务端向客户端推送消息,客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提醒和语言播报
6、SpringTask
6.1、springTask怎么实现的超时自动取消订单的功能?
Spring Task(Spring 任务调度)是 Spring 框架提供的一种任务调度框架,用于执行定时任务、异步任务、任务监听、任务调度等。
苍穹外卖采用Spring Task实现订单超时自动取消功能,核心逻辑是每分钟扫描一次数据库中订单,筛选出创建时间超过15分钟且状态为"待支付"的订单,批量修改为"已取消"状态
7、数据库相关
7.1 数据库表你是怎么设计的
序号 | 表名 |
---|---|
1 | employee(员工表) |
2 | category(分类表) |
3 | dish(菜品表) |
4 | dish_flavor(菜品口味表) |
5 | setmeal(套餐表) |
6 | setmeal_dish(套餐菜品关系表) |
7 | user(用户表) |
8 | address_book(地址表) |
9 | shopping_cart(购物车表) |
10 | orders(订单表) |
11 | order_detail(订单明细表) |
7.2 为什么用逻辑外键,而不用数据库自带的外键?
- 减少数据库开销:物理外键约束会增加数据库在插入、更新和删除操作时的额外开销。数据库需要花费额外的时间来检查外键约束的完整性,这在高并发、大数据量的场景下可能会对性能产生明显的影响。而逻辑外键由应用程序来控制,开发人员可以根据具体业务场景,在必要时才进行关联数据的检查,避免了数据库层面不必要的检查操作,从而提高系统的整体性能
- 适应变化:在软件开发过程中,业务需求可能会不断变化。如果使用物理外键,当表结构发生变化时,例如修改外键关联的字段类型、删除关联表中的字段等操作,可能会受到外键约束的限制,导致数据库结构的修改变得复杂和困难。而逻辑外键不存在这种限制,开发人员可以更自由地对表结构进行调整,只需要在应用程序中相应地修改逻辑外键的处理逻辑即可,提高了数据库设计的灵活性和可维护性。
8、微信相关
8.1 微信登录是怎么实现的?
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
8.2如何实现的微信支付功能?
完成微信支付有两个关键的步骤:
1️⃣ 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。
2️⃣ 就是支付成功之后微信后台会给推送消息。
9、你在项目中遇到了什么困难?
9.1处理公共字段的填充
如果都按照之前的操作方式来处理这些公共字段(创建时间,创建人id,修改时间,修改人id), 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐。我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解