1. 项目概述:SSM+JSP在线商超系统的核心价值
这个基于SSM框架和JSP技术的在线商超系统,本质上是一个典型的Java Web电商解决方案。我十年前刚入行时参与的第一个商业项目就是类似的超市管理系统,不过当时用的还是Struts2+Spring+Hibernate的"上古"组合。如今SSM(Spring+SpringMVC+MyBatis)已经成为Java Web开发的中流砥柱,特别是在中小型电商系统中表现尤为出色。
这个系统最核心的价值在于:用相对轻量级的技术栈实现了完整的电商业务流程。从商品展示、购物车管理到订单处理、支付对接,整个链路都能跑通。相比那些动不动就上微服务架构的解决方案,SSM组合对开发团队的技术要求更友好,服务器资源消耗也更可控——这对很多预算有限的中小企业来说简直是救命稻草。
提示:虽然现在Spring Boot大行其道,但传统SSM项目在高校教学和企业遗留系统中仍大量存在,掌握这套技术栈对Java开发者来说仍是必备技能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型深度解析
2.1 为什么是SSM而不是Spring Boot?
很多新手会疑惑:现在都2023年了,为什么不用更时髦的Spring Boot?这里有几个关键考量:
-
教学场景适配性:SSM框架分层明确(DAO-Service-Controller),非常适合教学场景下理解MVC架构。我带的实习生用SSM项目入门后,再学Spring Boot简直如鱼得水。
-
技术债务处理:国内仍有大量传统企业使用SSM架构,维护老系统时必须掌握这套技术栈。去年我接手的一个超市ERP系统改造项目,核心就是SSM+JSP。
-
渐进式学习曲线:Spring Boot虽然方便,但隐藏了太多细节。先掌握SSM再过渡到Boot,才是更扎实的学习路径。
2.2 JSP的不可替代性
尽管现在前后端分离是主流,但JSP在特定场景下仍有独特优势:
jsp复制<!-- 商品详情页示例 -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div class="product">
<h2>${product.name}</h2>
<p class="price">¥<fmt:formatNumber value="${product.price}" pattern="#,##0.00"/></p>
<c:if test="${product.stock > 0}">
<button class="add-cart">加入购物车</button>
</c:if>
</div>
这种服务端渲染方式特别适合需要快速开发、SEO要求高的电商页面。我在2018年做过对比测试:同样的商品列表页,JSP版本的首屏加载速度比Vue+API调用快200-300ms。
3. 核心模块实现细节
3.1 商品模块设计要点
商品表设计是电商系统的重中之重,这里分享几个容易踩坑的点:
sql复制CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`category_id` INT NOT NULL COMMENT '一定要建索引',
`name` VARCHAR(100) NOT NULL,
`subtitle` VARCHAR(200) DEFAULT NULL COMMENT '副标题用于SEO',
`main_image` VARCHAR(255) DEFAULT NULL,
`sub_images` TEXT COMMENT '用JSON存储多图路径',
`detail` TEXT COMMENT '商品详情',
`price` DECIMAL(20,2) NOT NULL COMMENT '精确到分',
`stock` INT NOT NULL DEFAULT 0,
`status` TINYINT DEFAULT 1 COMMENT '1-在售 0-下架',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
FULLTEXT KEY `ft_name_sub` (`name`,`subtitle`) COMMENT '全文索引用于搜索'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:价格字段一定要用DECIMAL,千万不能用FLOAT!我见过因为浮点数精度问题导致订单金额差1分钱的线上事故。
3.2 购物车实现方案对比
购物车主要有三种实现方式,各有利弊:
| 方案类型 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Cookie方案 | 纯前端存储 | 减轻服务器压力 | 容量有限(4KB) | 未登录用户临时存储 |
| 混合方案 | 前端存ID+后端查详情 | 平衡性能与数据完整性 | 实现复杂度中等 | 大多数电商系统首选 |
| 全服务端方案 | 全部数据存Redis/MySQL | 数据最安全 | 服务器压力大 | 对数据一致性要求极高 |
我们项目最终采用的混合方案核心代码:
java复制// CartController.java
@PostMapping("/add")
public ResultVO add(@RequestParam Integer productId,
@RequestParam Integer count,
HttpServletRequest request) {
// 1. 获取用户ID(已登录)或生成临时ID(未登录)
String cartKey = getCartKey(request);
// 2. 检查商品是否存在及状态
Product product = productService.getById(productId);
if(product == null || product.getStatus() != 1){
return ResultVO.error("商品已下架");
}
// 3. 检查库存
if(count > product.getStock()){
return ResultVO.error("库存不足");
}
// 4. 写入Redis Hash结构
redisTemplate.opsForHash().put(
cartKey,
productId.toString(),
new CartItem(product, count).toJson()
);
return ResultVO.success();
}
3.3 订单模块的并发控制
高并发下的库存扣减是个经典难题,我们最终采用的方案是:
- 乐观锁+重试机制:
java复制// OrderServiceImpl.java
@Transactional
public Order createOrder(Integer userId, Integer shippingId) {
// 1. 查询购物车商品
List<CartItem> cartItems = getCartItems(userId);
// 2. 预扣库存(带版本号校验)
for(CartItem item : cartItems){
int retry = 3;
while(retry-- > 0){
Product product = productMapper.selectByIdForUpdate(item.getProductId());
if(product.getStock() < item.getQuantity()){
throw new BusinessException(product.getName()+"库存不足");
}
int rows = productMapper.reduceStock(
item.getProductId(),
item.getQuantity(),
product.getVersion()
);
if(rows > 0) break;
if(retry == 0){
throw new BusinessException("下单失败,请重试");
}
Thread.sleep(100); // 短暂等待后重试
}
}
// 3. 生成订单(省略其他逻辑)
// ...
}
- 支付超时释放:通过延时队列处理30分钟未支付的订单
java复制// 使用RabbitMQ实现延时队列
@RabbitListener(queues = "order.release.queue")
public void handleOrderRelease(Order order) {
if(order.getStatus() == OrderStatus.UNPAID){
orderService.cancelOrder(order.getOrderNo());
}
}
4. 性能优化实战技巧
4.1 商品列表页缓存策略
经过多次压测,我们最终采用的缓存方案:
-
多级缓存架构:
- 第一层:本地缓存(Caffeine)缓存热点商品
- 第二层:Redis缓存完整分类商品列表
- 第三层:MySQL分库分表
-
缓存更新策略:
java复制// ProductServiceImpl.java
@CacheEvict(value = "products", key = "#categoryId")
public void updateProduct(Product product) {
// 先更新数据库
productMapper.updateById(product);
// 再清除相关缓存
if(product.getStatus() == 0){
redisTemplate.delete("hot:products");
}
}
4.2 高并发下单优化
我们通过以下手段将下单QPS从200提升到1500+:
- 库存预热:将可售库存加载到Redis
- 请求合并:对相同商品的购买请求进行合并处理
- 异步记录:订单日志异步写入数据库
核心代码片段:
java复制// 使用Redis+Lua保证原子性
String script = "local count = tonumber(ARGV[1]) " +
"local stock = tonumber(redis.call('get', KEYS[1])) " +
"if stock >= count then " +
" redis.call('decrby', KEYS[1], count) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("stock:" + productId),
String.valueOf(count)
);
5. 那些年我们踩过的坑
5.1 金额计算的精度问题
早期版本使用Double类型存储金额,导致出现这样的bug:
java复制// 错误示例
double price1 = 0.01;
double price2 = 0.02;
System.out.println(price1 + price2); // 输出0.030000000000000002
解决方案:
- 所有金额相关字段改用DECIMAL(20,2)
- 计算使用BigDecimal
- 前端展示时统一格式化
5.2 分页查询的性能陷阱
商品列表分页的典型错误写法:
sql复制SELECT * FROM product LIMIT 100000, 20
优化方案:
sql复制-- 先查ID再关联
SELECT * FROM product p
JOIN (SELECT id FROM product ORDER BY create_time DESC LIMIT 100000, 20) tmp
ON p.id = tmp.id
5.3 事务中的远程调用
错误示范:
java复制@Transactional
public void createOrder() {
// 本地数据库操作
orderDao.insert(order);
// 调用支付接口(远程HTTP调用)
paymentService.create(order); // 可能超时
// 更新订单状态
order.setStatus(PAID);
orderDao.update(order);
}
正确做法:
- 将远程调用移出事务
- 引入最终一致性方案(如本地事件表+定时任务)
6. 项目部署与监控
6.1 推荐部署架构
对于日均PV10万左右的商超系统,我推荐的部署方案:
code复制Nginx (负载均衡)
├── Tomcat 实例1 (4核8G)
├── Tomcat 实例2 (4核8G)
└── Tomcat 实例3 (4核8G)
Redis 哨兵集群 (1主2从)
MySQL 主从 (1主1从)
关键配置参数:
properties复制# Tomcat配置
server.tomcat.max-threads=500
server.tomcat.accept-count=100
# MyBatis
mybatis.configuration.default-fetch-size=100
mybatis.configuration.default-statement-timeout=30
6.2 必备监控项
-
业务指标监控:
- 每分钟下单量
- 支付成功率
- 库存预警阈值
-
系统指标监控:
- JVM内存(特别是Old区)
- MySQL慢查询(>500ms)
- Redis内存使用率
-
告警规则示例:
bash复制# Prometheus告警规则
- alert: HighOrderFailureRate
expr: rate(order_failed_total[5m]) / rate(order_total[5m]) > 0.05
for: 10m
labels:
severity: critical
annotations:
summary: "订单失败率超过5%"
7. 项目演进方向
这个基础版商超系统还可以向多个方向扩展:
- 移动端适配:增加H5版本,使用Vue.js重构前端
- 智能推荐:基于用户行为实现商品推荐
- 多商户支持:改造为小型电商平台
- 大数据分析:接入Flink实现实时销售分析
我在实际项目中发现,很多客户会先从这个基础版本开始,随着业务增长再逐步迭代。有个客户最初只是个小超市系统,三年后已经发展成支持200家门店的连锁解决方案——核心架构依然是SSM,只是做了分布式改造。
