今日学习总结Redis 全局 ID、优惠券秒杀、并发控制、JMeter、Spring AI一、Redis 全局唯一 ID1. 为什么不用数据库自增 ID数据库自增 ID 在单体项目中能用但有一些问题多台数据库分别自增可能生成重复 ID。连续 ID 容易暴露订单量。分库分表后不方便统一生成 ID。大量请求都依赖数据库生成 ID会增加数据库压力。2. ID 的组成项目使用一个Long类型的 64 位整数符号位 时间戳 Redis 自增序号 1位 31位 32位生成方式long id timestamp 32 | count;timestamp当前时间减去自定义起始时间。countRedis 使用INCR生成的自增序号。左移 32 位后时间戳放在高位。使用按位或|将序号放在低 32 位。3. 为什么每天使用一个 Redis Key例如icr:order:2026:07:11优点每天的计数从 1 开始不容易溢出。可以根据 Key 查看每天的订单量。方便设置过期时间和管理数据。注意日期格式DateTimeFormatter.ofPattern(yyyy:MM:dd)不能把分钟、秒误写进日期否则可能出现2026:46:024.final的作用private final StringRedisTemplate stringRedisTemplate;表示这个成员变量只能赋值一次通常在构造方法中完成注入public RedisIdWorker(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate stringRedisTemplate; }final限制的是变量不能重新指向其他对象不代表这个对象的方法不能使用。二、优惠券秒杀基本流程一次秒杀请求的基本步骤接收优惠券 ID → 查询秒杀优惠券 → 判断活动是否开始 → 判断活动是否结束 → 判断库存是否充足 → 扣减库存 → 生成全局订单 ID → 创建订单 → 返回订单 ID注意返回订单 ID 比返回完整订单对象更合适。三、MyBatis-Plus 扣减库存代码类似boolean success seckillVoucherService.update() .setSql(stock stock - 1) .eq(voucher_id, voucherId) .gt(stock, 0) .update();它最终执行的 SQL 类似UPDATE tb_seckill_voucher SET stock stock - 1 WHERE voucher_id ? AND stock 0;这段代码既执行修改也返回结果最后的.update()会真正执行 SQL。boolean success ...不是单纯判断而是执行库存扣减。根据受影响行数生成布尔结果。修改成功时success true。没库存或记录不存在时success false。所以后面不能再写一次stock stock - 1否则会重复扣库存。四、乐观锁与防止超卖1. 什么是超卖假设库存只剩 1请求 A 查询库存1 请求 B 查询库存1 请求 A 扣减库存 请求 B 也扣减库存如果只在 Java 中提前判断两次请求都可能认为库存充足。2.gt(stock, 0)的作用.gt(stock, 0)不是传统意义上的“上锁”而是把判断放进更新 SQLUPDATE ... SET stock stock - 1 WHERE stock 0;数据库执行一条UPDATE时会保证这次修改的原子性。库存已经变成 0 后后续请求无法再更新。因此stock 0 条件更新 → 防止超卖课程把这种思路称为乐观锁不提前锁住数据而是在更新时检查条件是否仍然成立。五、一人一单1. 基本判断创建订单前根据用户和优惠券查询int count query() .eq(user_id, userId) .eq(voucher_id, voucherId) .count(); if (count 0) { return Result.fail(不能重复下单); }目标是保证同一个 userId 同一个 voucherId 只能有一条订单2. 为什么普通查询仍会出问题同一个人也可能产生多个并发请求例如快速重复点击。网络超时后客户端自动重试。刷新或重复提交。使用 JMeter、Postman 或脚本直接请求接口。恶意绕过前端限制。执行顺序可能变成请求 A查询订单 → 没有 请求 B查询订单 → 没有 请求 A扣库存并创建订单 请求 B扣库存并创建订单这里 A、B 是同一个用户产生的两个请求。它们都在对方订单提交前完成了查询因此都认为用户没有下单。3. 解决办法按用户 ID 加锁synchronized (userId.toString().intern()) { // 查询重复订单 // 扣减库存 // 创建订单 }同一用户的请求会变成串行执行A 获得锁 A 查询没有订单 A 创建并提交订单 A 释放锁 B 获得锁 B 查询到已有订单 B 返回不能重复下单锁必须覆盖完整流程查订单 → 扣库存 → 创建订单只给查询部分加锁没有用。4. 两种并发问题不要混淆问题解决手段库存超卖stock 0条件更新同一用户重复下单按userId加锁应用层逻辑意外失效数据库联合唯一索引兜底数据库可以增加UNIQUE KEY uk_user_voucher (user_id, voucher_id)这样数据库最终也会拒绝同一用户、同一优惠券的第二条订单。注意Java 的synchronized只能控制当前 JVM。以后部署多台服务器时需要使用 Redis 分布式锁例如 Redisson。六、JMeter 压力测试1. JMeter 是什么JMeter 是接口压力测试工具.jmx是它的测试计划文件本质上是 XML 配置文件。测试中配置了请求地址localhost后端端口8081请求方式POST秒杀路径/voucher-order/seckill/10请求头authorization: token2. 为什么出现 401Response code: 401说明请求没有通过登录拦截器常见原因没有携带authorization。Token 填错。Token 已过期。Redis 中对应的登录信息不存在。3. 为什么 HTTP 200 仍显示异常JMeter 中的 JSON 断言可能要求{success: true}如果库存为 100但发送 1000 次请求100 次抢购成功 900 次库存不足虽然“库存不足”的响应可能也是 HTTP 200但业务字段是{success: false}因此 JSON 断言将其统计为异常异常率就是900 ÷ 1000 90%这不一定说明系统出错可能说明库存控制正确。若想理论上看到 50%库存100 请求200 成功100 失败100 异常率50%测试前要重置库存、清除旧结果并确认旧订单是否会影响一人一单判断。七、Spring AISpring AI 不是用来替程序员自动写项目的工具它是让 Java 后端系统具备 AI 功能的框架。可以实现AI 聊天助手。流式回答。上下文记忆。调用本地业务工具。结构化输出。RAG 企业知识库。对接向量数据库。RAG 基本流程读取公司文档 → 文档分段 → 生成向量 → 保存到向量数据库 → 用户提问 → 检索最相关的文档片段 → 把片段和问题交给大模型 → 生成有依据的回答不能简单地每次把公司所有 Markdown 文档发给 AI因为文档可能超过模型上下文长度。每次传输全部文档成本高。无关内容会干扰回答。文档更新和权限不好管理。向量数据库负责保存“文档片段对应的向量”例如PGVectorRedis StackMilvusElasticsearchSimpleVectorStore适合本地学习今天最重要的面试结论Redis 全局 ID 使用“时间戳 自增序列”生成。SQL 条件更新stock 0可以防止库存超卖。MyBatis-Plus 最后的.update()会真正执行 SQL。同一个用户也能通过重复点击、重试或脚本产生并发请求。一人一单需要保护“查询、扣库存、创建订单”的整个流程。synchronized只适合单机多台服务器需要分布式锁。数据库联合唯一索引是防止重复订单的最终兜底。JMeter 的异常率可能来自业务断言失败不等同于服务器异常。Spring AI 是给业务系统接入 AI 能力RAG 是其中一种知识库实现方式。