编辑
2026-01-10
java炒饭
00

教育背景

华中农业大学(211) | 数据科学与大数据技术 | 2023.09 – 至今


获奖经历

  • ICPC国际大学生程序设计竞赛区域赛(武汉站)铜奖(2025)
  • CCPC全国邀请赛(郑州)银奖(2025)
  • 蓝桥杯全国软件和信息技术专业人才大赛全国总决赛 C/C++ 程序设计大学A组 二等奖(2025)
  • 中国高校计算机大赛 – 团队程序设计天梯赛全国总决赛 个人二等奖(2025)

专业技能

  • 编程语言与基础:熟练掌握 Java,了解常用集合框架;具备扎实的算法与数据结构基础。
  • 数据库:熟悉 MySQL,了解索引、事务、锁机制,能通过执行计划进行 SQL 性能优化。
  • 后端框架:掌握 Spring Boot + MyBatis,理解 IOC、AOP 等核心设计思想。
  • 中间件
    • Redis:熟悉常用数据结构、持久化、高可用方案,了解缓存穿透/雪崩/击穿应对策略。
    • 消息队列:了解 RabbitMQ,具备消息可靠投递、幂等消费等实践经验。
  • 系统与架构
    • 了解 Spring Cloud(Nacos、OpenFeign、Gateway)微服务组件。
    • 了解高并发和分布式系统设计理念,如缓存、异步、读写分离、高可用等。
  • 开发与工具:熟练使用 Git、Maven、Docker,熟悉 Nginx、Vue,能独立完成简单项目开发与部署。

项目经历

高仿 QQ 的 Web 即时通讯系统(独立开发) | 2025.07 – 至今
技术栈:Spring Boot, WebSocket, JWT, Redis, RabbitMQ, MySQL, Redisson

项目描述
独立设计并实现一个支持高并发、可扩展的即时通讯系统。该项目核心攻克了集群环境下的消息实时推送海量消息的可靠存储与高效查询以及高并发业务场景下的数据一致性三大技术挑战,综合运用了WebSocket、消息队列、分布式锁等关键技术。

核心职责与成果

  1. 分布式实时通信与安全认证

    • 基于 WebSocket 实现双向实时通信,并设计心跳机制以维持长连接。
    • 为解决集群部署下的会话同步问题,设计了会话路由方案:将用户与服务器的映射关系存储于 Redis,跨服务器消息通过 RabbitMQ 精准路由并推送,实现了系统的横向扩展能力。
    • 采用 JWT 双Token(Access/Refresh) 机制实现无状态认证与安全刷新,并利用 AOP 与 Redis 分布式锁 对关键接口(如登录验证码发送)实施分布式限流,有效防御恶意请求。
  2. 消息可靠投递与存储性能优化

    • 设计了 “推送即持久化” 的异步流程:消息实时推送后,立即发送至 RabbitMQ,由独立的消费者服务进行异步处理,实现业务与数据层的解耦。
    • 消费者采用 “内存缓冲 + 批量写入” 策略,显著降低了数据库的写入频率与压力。通过消息唯一ID与手动确认(ACK)机制,确保了消息的可靠投递与消费的幂等性
    • 针对消息查询,设计了分级存储策略:活跃会话的最新消息直接从 Redis 缓存读取;查询历史或非活跃会话时,通过分布式锁保证缓存与数据库的数据一致性后执行查询,兼顾了性能与准确性。
  3. 高并发业务实践:群聊红包系统

    • 实现了完整的群聊红包功能,核心解决高并发下的超抢与金额分配原子性问题。
    • 使用 Redisson 分布式锁 确保对同一红包资源的访问串行化。
    • 发送红包时预分配红包金额,采用 二倍均值随机算法 公平分配金额,并通过执行 Redis Lua 脚本,原子化地完成库存扣减与领取记录写入,完美保障了高并发场景下的数据准确性与一致性。

项目总结
本项目以解决实际技术问题为导向,深入应用了消息队列、缓存、分布式锁等中间件。通过实践,不仅掌握了这些技术的核心用法,更深刻理解了它们在系统解耦、流量削峰、保证数据强一致性等场景下的价值,形成了从架构设计到编码落地的完整闭环能力。


自我评价

  • 技术扎实,善于实践:具备良好的算法与 Java 开发基础,能独立完成项目从设计到部署的全流程。
  • 逻辑清晰,解决问题能力强:通过算法竞赛与项目实践,能快速理解复杂业务场景,设计并实施可行的技术方案。
  • 主动学习,追求深入:关注后端技术发展,乐于钻研实现原理,持续优化项目与个人技术栈。
  • 团队协作意识强:具备竞赛团队合作经验,注重沟通与协作,期待在实习中为团队贡献价值。

编辑
2026-01-09
redis
00

单节点的redis并发能力有限,因此我们需要搭建主从集群,实现读写分离。一般是主节点进行写,从节点进行读。

主从同步:

  • 全量同步:从结点第一次与主节点同步数据时使用的方案。从节点向主结点发送同步请求,携带参数replicationId,offset,主节点会根据replicationId来判断是否是第一次同步,如果是第一次同步(Id不一致),则主节点会把自己的replicationId和offset发给从节点,同时主节点执行bgsave生成rdb文件。在生成的同时,会开启一个缓冲区记录该期间收到的所有写命令,最后把rdb和这个缓冲区的信息(类似日志)一起发送给从节点。
  • 增量同步:还是从节点发送请求,主节点判断是不是第一次请求,不是则获取从节点的offset,并把从结点的offset和自身offset之间的数据同步给从节点。

高可用:

哨兵模式(sentinel):

  • 本质自己也是一个redis服务
  • 每1s向集群的每个实例发送ping请求。如果在规定时间收不到响应,认为该结点主观下线。
  • 当指定数量的哨兵认为某个结点都下线了,则认为该节点客观下线。这个数量一般不低于哨兵数量的一半。
  • 当主节点下线了,哨兵会推选一个新的主节点。选主规则:
    • 如果该结点与原来的主节点断开连接时间超过指定值,则不纳入考虑。
    • 根据结点的优先级来判断,优先选择优先级高的,可在配置文件中设置优先级。
    • 优先级相同时选择offset大的节点
    • 以上条件都相同时选择运行ID最小的节点

脑裂:

  • 由于网络原因,哨兵监测不到主结点,认为主节点挂了,实际上主节点并没挂,此时客户端还在与原来的主节点发送请求,然而哨兵却选择了一个新的主节点,原来的主节点变成了从节点,在同步数据的时候,期间所写的数据就失效了。
  • 解决方案:可以通过配置min-slaves-to-write(最少从节点数)和min-slaves-max-lag(最大延迟秒数)。当主节点的从节点数量少于配置值,或者从节点的延迟时间超过配置值时,主节点会拒绝写入请求,从而避免数据不一致。这是一种 “宁可拒绝写入,也要保证数据一致性” 的取舍。在脑裂期间,被孤立的旧主节点会提供高可用性。
编辑
2026-01-09
redis
00

redis实现的分布式锁:

  • 使用setnx命令时,需要设置 ttl,防止系统故障导致锁无法释放。
  • 自己实现的分布式锁的缺陷:我们并不知道准确的业务执行时间,因此这个过期时间不好控制。
  • 不可重入

因此我们使用第三方工具redisson:

  • 提供看门狗(WatchDog),一个线程获取锁成功之后,WatchDog会给持有锁的线程续期(默认每隔10s续期)
  • 可重入,底层采用了一个hash结构,用线程id和该锁锁的次数作为依据,如果发现锁已经被获取了,但是是当前线程获取的,我们就可以再次获得锁,并把次数加1。如果发现这个线程id不是自己的,则无法获取锁。释放锁的时候让次数减1即可。
  • 不能做到主从的强一致性,如果需要,可以使用zookeeper实现的分布式锁。
  • 底层还是setnx和lua
编辑
2026-01-07
java炒饭
00

JWT双Token认证方案

一、核心设计

双Token机制

  • AccessToken:短期(15分钟),前端localStorage存储,每次请求校验
  • RefreshToken:长期(7天),后端HttpOnly Cookie + MySQL存储,仅刷新时校验

验证策略

  • AccessToken:Redis黑名单验证(被吊销的才记录)
  • RefreshToken:MySQL白名单验证(有效的才记录)

二、核心流程

1. 登录流程

用户登录成功 → 生成双Token → AccessToken返回前端存入localStorage → RefreshToken存入MySQL并设置HttpOnly Cookie。

2. API请求流程

前端携带AccessToken → 后端解析JWT(验证签名和过期)→ 查询Redis黑名单 → 不在黑名单则放行 → 处理业务返回结果。

3. 刷新流程(关键)

前端收到401 → 调用刷新接口(Cookie自动携带RefreshToken)→ 后端从MySQL查询RefreshToken进行匹配验证 → 验证通过则:

  1. 生成新双Token
  2. 旧AccessToken加入Redis黑名单(15分钟TTL)
  3. 更新MySQL中的RefreshToken为新值
  4. 新RefreshToken设置到HttpOnly Cookie
  5. 返回新AccessToken给前端

重要:RefreshToken验证只查MySQL,不查Redis黑名单。

4. 登出流程

用户登出 → 当前AccessToken加入Redis黑名单 → 删除MySQL中的RefreshToken记录 → 清除HttpOnly Cookie。

5. 安全事件处理

密码修改等安全事件 → 删除MySQL中的RefreshToken记录 → 强制重新登录。

三、存储设计

Redis黑名单

只存储被吊销的AccessToken,键格式:blacklist:access_token:{token_hash},TTL 15分钟(与AccessToken有效期一致)。

MySQL白名单

user_tokens表存储有效的RefreshToken,每个用户一条记录(user_id唯一约束),验证时只查询此表。

四、验证逻辑

AccessToken验证(黑名单逻辑)

默认所有AccessToken都有效,除非在Redis黑名单中。验证顺序:1.JWT解析 → 2.查Redis黑名单。

RefreshToken验证(白名单逻辑)

默认所有RefreshToken都无效,除非在MySQL白名单中且匹配。验证时只查MySQL,不查Redis。

五、性能与安全

性能优化

  • AccessToken高频验证:Redis内存操作
  • RefreshToken低频验证:MySQL查询,15分钟一次,可接受

安全设计

  • AccessToken:短期有效,泄露风险小,可加入黑名单立即吊销
  • RefreshToken:HttpOnly Cookie防XSS,每次刷新后失效(MySQL更新)
  • 无RefreshToken黑名单:简化设计,通过MySQL更新保证旧Token失效

六、运维管理

监控重点

  • Redis:黑名单大小、内存使用
  • MySQL:查询性能、连接数
  • 业务:刷新频率、401错误率

数据清理

  • Redis:自动过期,无需手动清理
  • MySQL:定期清理过期记录(每天凌晨)

故障处理

  • Redis故障:降级为仅JWT验证(牺牲吊销能力)
  • MySQL故障:刷新功能不可用,需重新登录

七、方案优势

  1. 高性能:高频操作用Redis,低频操作用MySQL
  2. 高安全:双Token分离,HttpOnly Cookie防护
  3. 简化设计:无RefreshToken黑名单,通过MySQL更新保证安全
  4. 易管理:AccessToken黑名单自动清理,MySQL便于审计

核心要点:RefreshToken验证只查MySQL白名单,不查Redis黑名单;安全事件通过删除MySQL记录使RefreshToken失效。

编辑
2025-12-29
java炒饭
00

从OSS叛逃到安全炼狱:我的全栈开源硬核之路

凌晨1点的账单惊吓

“一个月300块?就存点头像图片?”

我盯着阿里云OSS的测试账单,手抖得比咖啡因过量还厉害。这还只是测试期的费用,真上线了还得了?

“不行,”我咬牙,“这违背开源精神。”

作为一个开源项目的维护者,我有个固执的原则:绝不让用户为开源项目花一分钱。OSS?拜拜了您嘞。

一周内的技术栈大迁移

周一,我做出了那个改变一切的决定:从OSS迁移到本地存储。

“简单,”我想,“Docker + Nginx,文件存本地,零成本。”

周二,我开始改造。后端Java代码好办,但前端上传怎么办?我瞄了一眼Vue组件——一个头像上传组件就400多行代码,各种校验、预览、裁剪。

“让trae帮我弄吧。”我偷了个懒。

trae的天真设计

我对IDE里的trae说:“帮我改一下头像上传,不用OSS了,存本地Nginx。”

trae秒回方案:“收到。使用Vite反向代理,前端直接上传到/uploads目录,Nginx直接服务。”

代码很简单:

javascript
// 上传 POST /uploads // 前端直接处理,不走Java

我测试了一下,速度飞快。Node.js处理文件上传,确实比Java省资源。

但周三,我发现一个问题:用户上传新头像,旧头像还在服务器上躺着!

“这不行,”我的资源洁癖发作了,“如果有人恶意上传,服务器不就炸了?”

我命令trae:“加删除逻辑。用户上传新头像前,先删除旧头像。”

trae很听话,把单次上传改成了两个操作:

  1. 删除旧头像(DELETE /uploads/旧文件
  2. 上传新头像(POST /uploads

“完美。”我当时想。

周四的噩梦降临

周四晚上,预上线前的最后一次安全测试。

我随手在APIFox里试了一下:

DELETE http://test.zymusic.top/uploads/admin.png

200 OK。

我又试了试:

POST http://test.zymusic.top/uploads (随便传个文件)

200 OK。

我的血液凝固了。

这两个接口完全没有权限验证!任何人,不需要登录,就能删文件、传文件!

“我草!trae你……”我骂到一半,突然意识到:trae只是个AI,它懂个屁的权限验证。

问题的核心矛盾

凌晨2点,我盯着架构图,发现了几个致命矛盾:

矛盾一:性能 vs 安全

  • Node.js处理文件IO性能好,但不会解析JWT(Java生成的)
  • Java会解析JWT,但处理文件IO性能差
  • 我既想要Node.js的性能,又想要Java的安全

矛盾二:资源洁癖 vs 安全风险

  • 不删旧文件 → 服务器可能被恶意上传撑爆
  • 删旧文件 → 权限漏洞可能被黑客利用
  • 我的资源洁癖逼我选择了更危险的路

矛盾三:开源理想 vs 工程现实

  • 理想:完全免费,不依赖任何付费服务
  • 现实:安全方案要么花钱(OSS),要么花精力(自研)
  • 我选择了最硬核也最危险的路

周五凌晨的绝地求生

凌晨3点,我必须在几个小时内解决这个问题,否则周一预上线就是个笑话。

方案一:让前端直接问Java权限(不行) 因为最终文件操作还是要和Nginx交互,JavaScript处理IO比Java高效得多。而且,让JS解析JWT?不可接受!JWT是Java生成的,就该Java来解析。

方案二:让Java直接管理文件(不行) 性能下降不说,还要大改前后端,一周时间根本不够。

方案三:在Node.js和Nginx之间加一层验证(可行!)

我的方案渐渐清晰:

  1. 前端调用Node.js的上传/删除接口
  2. Node.js先向Java后端请求授权
  3. Java验证JWT,返回是否允许操作
  4. Node.js根据授权结果执行文件操作

用Redis搭建“操作锁”

但还有个问题:如何确保用户是“先删后传”,而不是只传不删,或者只删不传?

我的解决方案:用Redis记录操作状态

javascript
// Node.js上传服务伪代码 // 删除接口 app.delete('/uploads/:filename', async (req, res) => { // 1. 从header拿到JWT const token = req.headers.authorization; // 2. 问Java:这人有权限删这个文件吗? const canDelete = await javaBackend.verifyDelete(token, filename); if (!canDelete) { return res.status(403).send('滚犊子'); } // 3. 执行删除 await fs.unlink(filepath); // 4. 在Redis标记:这个用户已删除旧头像,可以上传新的了 await redis.setex(`upload_ok:${userId}`, 5, '1'); res.send('删除成功'); }); // 上传接口 app.post('/uploads', async (req, res) => { const token = req.headers.authorization; // 1. 问Java:这人有权限上传吗? const canUpload = await javaBackend.verifyUpload(token); if (!canUpload) { return res.status(403).send('没权限'); } // 2. 检查Redis:他是不是刚删了旧头像? const canProceed = await redis.get(`upload_ok:${userId}`); if (!canProceed) { return res.status(403).send('请先删除旧头像'); } // 3. 执行上传 const newFilename = await saveFile(req.file); // 4. 清理Redis标记 await redis.del(`upload_ok:${userId}`); res.send({filename: newFilename}); });

Java后端的验证逻辑:

java
// Java伪代码 public boolean verifyDelete(String token, String filename) { // 1. 解析JWT,获取用户ID String userId = decodeJWT(token); // 2. 查数据库,这个用户的当前头像是不是这个文件? User user = userDao.findById(userId); if (user.getAvatar().equals(filename)) { return true; // 是自己的头像,可以删 } return false; // 不是自己的头像,滚 } public boolean verifyUpload(String token) { // 管理员?直接放行! if (isAdmin(token)) { return true; } // 普通用户:检查是否有上传权限(比如是不是被封禁了) return userCanUpload(token); }

周六凌晨的缝合怪架构

凌晨4点,我部署完这个“四不像”的架构:

前端Node.js上传服务Java权限验证Redis状态管理Nginx文件服务

绕了地球一圈,但:

  1. 安全了(所有操作经过Java验证)
  2. 性能保留了(Node.js处理文件IO)
  3. 资源洁癖满足了(旧文件会被删除)
  4. 开源精神坚持了(没花一分钱)

“这就是工程现实吗?”我苦笑,“为了不花钱,我造了个比OSS复杂十倍的轮子。”

周日的最后反思

系统终于安全了。我瘫在椅子上,回顾这一周的过山车:

1. 开源不等于免费

我以为“不花钱”就是开源精神,但忽略了“不花钱”可能意味着“花更多精力”。OSS的300块,买的是别人解决好的安全方案。我省了300块,搭进去一周的睡眠。

2. 资源洁癖是种病,但得治

我的担心是对的——恶意上传真的能搞垮服务器。但我的解决方案错了——不应该为了省资源而牺牲安全。应该先保证安全,再考虑资源。

3. AI是工具,不是工程师

trae能写出漂亮的代码,但不懂架构、不懂安全、不懂权衡。它能执行命令,但不能思考后果。把AI当工程师用,就像让厨子开飞机——早晚出事。

4. 性能与安全的永恒战争

我既想要Node.js的IO性能,又想要Java的安全验证。鱼和熊掌不可兼得,但我用Redis和HTTP调用强行兼得了。代价是:复杂度爆炸。

5. 一周能改变什么?

一周前,我以为只是换个存储方案。一周后,我造了个分布式文件权限系统。有时候,工程进度不是按计划走的,是按问题走的。

给所有开源硬核玩家的忠告

如果你也在维护开源项目,想坚持“完全免费”,记住我的教训:

1. 安全永远第一

用户能接受功能少,但不能接受数据丢。安全漏洞是开源项目的死刑。

2. 复杂度是隐形成本

你省了云服务的钱,但付出了开发、维护、调试的精力。这些精力也是成本。

3. 测试,测试,再测试

特别是安全测试。我要是早做安全测试,周二就能发现问题,不用拖到周四凌晨。

4. 承认工具的限制

AI能帮你写代码,但不能帮你做架构决策。你是工程师,它是工具。别搞反了。

5. 有时候,花钱买时间是对的

300块买OSS服务,还是300小时造轮子?在开源项目里,时间也是稀缺资源。

最后

窗外天空泛白,一周的挣扎结束了。

我的开源项目依然坚持“完全免费”,但代价是:一个比商业方案复杂十倍的权限系统。

这值得吗?我不知道。

但我知道的是:当用户不用为这个项目花一分钱时,他们不会知道,有个傻子在深夜用Redis、Node.js、Java和Nginx,造了个丑陋但安全的轮子。

而那个傻子,现在需要睡一觉。

预上线倒计时:12小时。

这次,真的准备好了。🚀