
简介这份资源面向具备SpringBoot与Java Web基础的开发者聚焦企业级电商项目从单机向集群演进过程中的两个核心难题Tomcat集群部署与Redis分布式锁落地。内容基于注解式开发风格配套Nginx负载均衡配置涵盖分布式锁流程图、虚拟节点、一致性hash算法、Spring与SpringMVC互补配置等关键图示帮助读者理解集群环境下会话共享、请求分发与并发控制的完整思路。压缩包共99个文件以75个java源码为主体辅以properties、xml配置、conf与html等部署文件另有png流程图、md说明及docx附赠资料整体约620KB结构紧凑便于按模块查阅。目前已有95人学习下载。通过源码与配置对照读者可掌握Nginx反向代理与负载均衡策略、Redis分布式锁的实现细节及一致性hash在缓存节点分配中的应用适合作为电商项目集群化改造的实战参考。1. 从单机到集群这套电商项目到底解决了什么真问题很多 Java 工程师写过的第一个“企业级”项目都是单机 SpringBoot 商城一个 jar 包、一个 MySQL、一个 Redis本地跑得飞起。可一旦把 Tomcat 部署成两台以上用 Nginx 做负载均衡问题立刻冒出来——用户登录态在 A 机器上请求被转发到 B 机器就失效了秒杀下单时两台机器同时读到库存 1各自扣减最后超卖。这不是代码写得烂而是单机思维没切换到分布式思维。这份资源围绕的正是这个切换过程基于 SpringBoot 注解式开发的企业级电商平台把 Tomcat 集群、Nginx 负载均衡、Redis 分布式锁三块拼在一起形成一个可复现的演进路径。它适合已经会写 CRUD、但对“多机部署后数据一致性怎么保证”没有实战经验的 Java 工程师也适合想补一段能讲清楚原理的集群项目放进简历的人。下面我按“先跑起来、再拆原理、最后避坑”的顺序把这份资源里真正值钱的部分拆开讲。2. Tomcat 集群与 Nginx 负载均衡会话共享怎么落地2.1 为什么单机 Session 一到集群就废单机时代用户登录后HttpSession存在当前 JVM 的堆里浏览器带着JSESSIONID回来Tomcat 从本地内存取出会话一切正常。集群之后Nginx 默认轮询转发第一次请求落在 8081第二次可能落到 8082。8082 的 JVM 里根本没有这个 session于是用户被踢回登录页。这就是最典型的“登录态丢失”。常见解法有三条路Session 粘滞ip_hash、Session 复制Tomcat 集群广播、Session 集中存储Redis。前两条在机器扩容、重启时都有明显短板——粘滞会导致负载不均复制在节点多时广播风暴严重。所以这份资源选的是第三条把 Session 交给 RedisTomcat 只做无状态计算。这也是目前企业里最主流的做法。2.2 Nginx 负载均衡配置与 upstream 参数Nginx 这一层负责把流量分到多个 Tomcat 实例。核心是upstream块下面是一份可直接抄的配置假设两台 Tomcat 分别跑在 8081、8082# nginx.conf 片段 upstream ecommerce_cluster { # 默认轮询weight 越大分到的请求越多 server 127.0.0.1:8081 weight1 max_fails2 fail_timeout30s; server 127.0.0.1:8082 weight1 max_fails2 fail_timeout30s; # 保持长连接减少握手开销 keepalive 32; } server { listen 80; server_name shop.local; location / { proxy_pass http://ecommerce_cluster; proxy_http_version 1.1; # 这两行是 keepalive 生效的前提 proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 把真实客户端 IP 透传给后端日志和风控都要用 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 会话超时避免后端处理慢时连接被过早掐断 proxy_read_timeout 60s; } }逻辑说明upstream定义后端池weight控制权重max_fails和fail_timeout决定某台机器连续失败几次后被暂时摘除这是最基础的熔断。keepalive 32让 Nginx 与后端保持最多 32 个空闲长连接配合proxy_http_version 1.1和清空Connection头才生效少了任何一项长连接都不成立。X-Forwarded-For必须透传否则后端拿到的全是 Nginx 的 IP做限流和审计时会翻车。参数怎么改如果两台机器配置不同把性能好的weight调大fail_timeout默认 10s生产上建议 30s 起步太短会导致抖动时频繁摘除又恢复。改完用nginx -t校验语法再nginx -s reload平滑生效不要直接 restart。2.3 SpringBoot 侧接入 Redis 共享 SessionNginx 配好只是第一步Tomcat 还得把 Session 写进 Redis。SpringBoot 里最省事的做法是引入 spring-session-data-redis几行配置就能接管# application.yml spring: redis: host: 127.0.0.1 port: 6379 # 生产环境一定要设密码别裸奔 password: your_password database: 0 timeout: 3000ms session: store-type: redis # 会话过期时间单位秒默认 1800 timeout: 1800 redis: # 指定命名空间避免和其他业务 key 冲突 namespace: ecommerce:session// 启动类加注解即可无需手写 SessionRepository SpringBootApplication EnableRedisHttpSession(maxInactiveIntervalInSeconds 1800) public class EcommerceApplication { public static void main(String[] args) { SpringApplication.run(EcommerceApplication.class, args); } }逻辑说明EnableRedisHttpSession会注册一个RedisIndexedSessionRepository把原本存在 JVM 里的 session 序列化后写进 Rediskey 形如ecommerce:session:sessions:xxx。任意一台 Tomcat 收到请求都从同一个 Redis 读会话登录态自然共享。maxInactiveIntervalInSeconds是过期时间和 yml 里的timeout保持一致避免两处配置打架。参数说明namespace强烈建议自定义默认前缀太通用多个项目共用一个 Redis 时会互相覆盖。序列化默认用 JDK 序列化存进去是二进制用 redis-cli 看是乱码如果想让运维能直接读可以换成 Jackson 序列化但要额外配置Bean这份资源里保留了默认方式够用且稳定。2.4 验证集群是否真的生效配置完别急着庆祝按下面三步验证第一启动两个 Tomcat 实例端口分别 8081、8082确认都能独立访问。第二通过 Nginx 的 80 端口登录然后在 Redis 里keys ecommerce:session*能看到 session key 就说明写进去了。第三用浏览器开发者工具看JSESSIONID反复刷新页面如果请求在 8081 和 8082 之间跳转但始终不掉登录说明会话共享成功。这一步是血泪经验——很多人只测了单机就以为集群通了上线才发现问题。3. Redis 分布式锁秒杀扣库存为什么必须加锁3.1 从超卖说起并发下的经典事故假设库存剩 1 件两台 Tomcat 同时收到下单请求。线程 A 读到库存 1线程 B 也读到库存 1各自判断“还有货”然后都执行扣减数据库里库存变成 -1。这就是超卖。单机时可以用synchronized或ReentrantLock挡住但集群里锁只在单个 JVM 内有效A 机器的锁管不了 B 机器。所以必须把锁放到一个所有节点都能访问的地方——Redis。分布式锁要满足三个基本条件互斥同一时刻只有一个线程持有、防死锁持锁者宕机后锁能自动释放、防误删不能删掉别人持有的锁。下面按这三点逐步实现。3.2 用 SET NX EX 实现基础锁最核心的命令是SET key value NX EX seconds。NX表示 key 不存在才设置保证互斥EX设置过期时间保证宕机后锁能自动释放。用 SpringBoot 的StringRedisTemplate写出来是这样public boolean tryLock(String lockKey, String requestId, long expireSeconds) { // setIfAbsent 对应 SET NX同时设置过期时间保证原子性 Boolean success stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireSeconds, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } public boolean releaseLock(String lockKey, String requestId) { // 用 Lua 脚本保证“判断 删除”的原子性防止误删别人的锁 String lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Long result stringRedisTemplate.execute( new DefaultRedisScript(lua, Long.class), Collections.singletonList(lockKey), requestId); return result ! null result 0; }逻辑说明requestId是每个线程唯一的标识通常是 UUID用来标记“这把锁是谁加的”。释放时不能直接del因为如果线程 A 的业务执行超过了过期时间锁自动释放线程 B 拿到了锁此时 A 执行完直接del就会删掉 B 的锁。用 Lua 脚本先比对 value 再删除保证只有锁的持有者才能释放。参数说明expireSeconds是锁的过期时间设太短业务没跑完锁就没了设太长持锁者宕机后其他线程要等很久。常见做法是预估业务耗时取其 2 到 3 倍。requestId一定要在加锁前生成并贯穿整个业务不能中途重新生成。3.3 锁续期与 Redisson 的取舍上面的基础锁有个隐患如果业务执行时间超过过期时间锁提前释放其他线程就能进来互斥被破坏。解决办法是“看门狗”续期——持锁线程定期检查业务是否还在跑是就把过期时间延长。手写续期逻辑复杂且容易出错所以生产上更常见的是直接用 RedissonConfiguration public class RedissonConfig { Bean public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(your_password) .setDatabase(0); return Redisson.create(config); } }// 业务中使用 RLock lock redissonClient.getLock(lock:stock: productId); try { // tryLock 参数等待时间、持有时间、时间单位 // 不传 leaseTime 时启用看门狗默认 30s 并自动续期 boolean locked lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException(获取锁失败请稍后重试); } // 扣库存业务 stockService.decrease(productId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 只有当前线程持有才释放避免 IllegalMonitorStateException if (lock.isHeldByCurrentThread()) { lock.unlock(); } }逻辑说明Redisson 的tryLock不传leaseTime时会启动看门狗默认锁 30 秒后台线程每 10 秒续一次直到业务结束或 JVM 关闭。这解决了业务超时导致锁提前释放的问题。isHeldByCurrentThread判断是必须的否则在获取锁失败时执行unlock会抛异常。参数说明tryLock的等待时间设 3 秒意味着最多等 3 秒拿不到锁就放弃避免请求堆积。如果业务本身耗时很长建议显式传leaseTime并配合业务超时控制不要完全依赖看门狗。选型上如果只是简单互斥且能控制业务时长基础锁够用如果业务复杂、时长不确定直接上 Redisson别自己造轮子。3.4 锁粒度与库存扣减的完整链路锁的粒度直接决定性能。用lock:stock一把大锁锁住所有商品并发直接退化成串行正确做法是按商品维度加锁lock:stock:1001和lock:stock:1002互不影响。完整链路是先查缓存判断有货再获取分布式锁锁内重新查一次数据库库存双重检查确认有货后扣减并更新缓存最后释放锁。锁内那次数据库查询不能省因为缓存可能不准这是防超卖的最后一道防线。4. 避坑与排查集群和锁最容易翻车的地方4.1 现象登录后刷新就掉线Redis 里却查不到 session原因通常是EnableRedisHttpSession没生效或者 Redis 连接配置写错但被 SpringBoot 静默忽略。还有一种情况是 Nginx 配置了ip_hash但后端某台机器挂了会话粘滞到挂掉的节点。解决先确认启动日志里有没有RedisHttpSessionConfiguration相关输出再用redis-cli手动set/get验证连通性最后检查 Nginx 是否误配了粘滞策略。集群下不要用ip_hash交给 Redis 统一管理。4.2 现象分布式锁偶尔失效还是超卖原因多半是加锁和设置过期时间分成了两条命令比如先setnx再expire中间如果宕机锁就永远不过期后续请求全被挡死或者反过来锁没设过期程序异常后死锁。解决必须用SET key value NX EX一条命令完成保证原子性。用 Redisson 的话检查是否误传了leaseTime为 0 或负数那会直接关闭看门狗。4.3 现象释放锁时报IllegalMonitorStateException原因是当前线程并没有持有这把锁却调用了unlock。常见于tryLock返回 false 后没有 return继续走到 finally 里释放。解决在 finally 里加isHeldByCurrentThread判断或者把unlock放在if (locked)分支内。这个异常本身不致命但说明代码逻辑有漏洞必须修。4.4 现象Nginx 转发后后端拿到的 IP 全是 127.0.0.1原因是没配X-Real-IP和X-Forwarded-For或者后端读取时用错了 header。解决Nginx 侧按第 2 章的配置透传后端用request.getHeader(X-Forwarded-For)读取注意这个值可能是逗号分隔的多个 IP取第一个才是真实客户端。做限流时如果拿错会把所有用户当成同一个人。4.5 现象Redis 内存暴涨session 和锁的 key 越积越多原因是 session 过期时间设得太长或者锁的 key 因为异常没释放。解决session 过期时间按业务定一般 30 分钟到 2 小时锁必须保证 finally 释放配合过期时间兜底。另外给 Redis 设置maxmemory-policy allkeys-lru内存满时淘汰最久未用的 key避免直接 OOM。定期用redis-cli --bigkeys排查大 key。5. 进阶技巧把锁和集群压测一遍再上线配置跑通只是及格线真正上线前我习惯做两件事压测和故障演练。压测用 JMeter 或 wrk 对秒杀接口发并发观察库存是否精确扣减、有没有超卖、响应时间是否随并发线性恶化。故障演练则是手动 kill 掉一台 Tomcat看 Nginx 是否在fail_timeout后摘除节点登录态是否因为 Redis 共享而不受影响。下面这段 JMeter 命令行压测可以直接抄假设秒杀接口是/api/seckill# 100 并发循环 50 次共 5000 请求 jmeter -n -t seckill.jmx -Jthreads100 -Jloops50 -l result.jtl -e -o report逻辑说明-n非 GUI 模式-t指定测试计划-l输出结果文件-e -o生成 HTML 报告。跑完后重点看两个指标吞吐量和错误率。如果错误率随并发升高多半是锁等待超时或 Redis 连接池不够。连接池参数在spring.redis.lettuce.pool下配置max-active建议设为并发数的 1.5 倍。验证锁是否真的生效有个简单粗暴的办法在扣库存前后打日志记录当前库存值和线程名压测后 grep 日志看有没有两个线程读到同一个库存值。如果有说明锁没起作用回去检查锁的 key 是否按商品维度隔离、Lua 释放脚本是否正确。还有一个容易被忽略的点Redis 单点故障。如果只有一台 Redis它挂了整个集群的 session 和锁全没了。生产上至少做主从加哨兵或者用集群模式。这份资源里是单机 Redis方便本地复现但上线前一定要把高可用补上否则分布式锁反而成了单点。从那以后我每次做集群项目都会先把 session 共享和分布式锁在本地两台实例上跑通再压测一轮确认没有超卖和掉线才敢往预发环境推。这套流程帮我挡掉过好几次上线事故希望帮到你。本文还有配套的精品资源点击获取