购物车功能看似简单,实际做起来却涉及数据结构选型、会话管理、并发控制等多个知识点。一个设计良好的购物车,需要支持商品添加、数量修改、条目删除、总价计算,还要考虑用户未登录时的暂存和登录后的合并。本文以Java为例,从数据结构设计讲起,逐步给出可运行的完整实现,并对比不同存储方案的优劣。

一、购物车的数据结构设计
购物车的本质是一个容器,存储用户挑选的商品。最直觉的做法是用一个List来装商品对象,但这种方案有个明显缺陷:用户重复点击添加同一件商品时,列表里会出现多条相同记录,结算和展示都会很别扭。更好的方式是用HashMap,以商品ID作为key,以购物车条目作为value,天然实现去重,添加重复商品时只需累加数量即可。
先定义两个实体类。商品类Product承载基本信息,购物车条目类CartItem记录商品和数量的对应关系:
public class Product {
private Long id;
private String name;
private double price;
private String image;
// 省略构造方法和getter/setter
}
public class CartItem {
private Product product;
private int quantity;
public double getSubtotal() {
return product.getPrice() * quantity;
}
// 省略getter/setter
}条目类里封装一个getSubtotal小方法,把小计计算内聚到对象内部,避免在Servlet或Service层写一堆零散的乘法运算。这是面向对象设计的基本功,代码可读性和可维护性都会好很多。
接着定义购物车类本身,它包装了一个Map并提供一组业务方法:
public class Cart {
// key为商品ID,value为购物车条目
private Map<Long, CartItem> items = new HashMap<>();
public Map<Long, CartItem> getItems() {
return items;
}
// 添加商品,已存在则累加数量
public void addItem(Product product, int quantity) {
CartItem item = items.get(product.getId());
if (item == null) {
item = new CartItem();
item.setProduct(product);
item.setQuantity(quantity);
items.put(product.getId(), item);
} else {
item.setQuantity(item.getQuantity() + quantity);
}
}
// 修改数量
public void updateQuantity(Long productId, int quantity) {
CartItem item = items.get(productId);
if (item != null && quantity > 0) {
item.setQuantity(quantity);
}
}
// 删除单个条目
public void removeItem(Long productId) {
items.remove(productId);
}
// 清空购物车
public void clear() {
items.clear();
}
// 计算总金额
public double getTotal() {
double total = 0;
for (CartItem item : items.values()) {
total += item.getSubtotal();
}
return total;
}
}这套设计的要点在于:所有对购物车的操作都收敛到Cart类的方法里,外部调用者不需要知道内部用的是HashMap还是别的结构。将来如果要把存储换成Redis,业务代码几乎不用动,只需要替换Cart的实现方式。
二、基于Session的会话级购物车实现
有了Cart类,接下来要解决的是存哪里的问题。对于中小型网站,把购物车对象放进HttpSession是最简单直接的方案。Session由Servlet容器管理,每个用户独享一份,天然隔离,代码量也最少。
以添加商品到购物车为例,在Servlet中的处理流程是:先从Session取出Cart对象,如果不存在就新建一个并放入Session,然后调用addItem方法。以下是核心代码:
@WebServlet("/cart/add")
public class AddToCartServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取商品ID和数量(实际项目中应从数据库查询商品信息)
Long productId = Long.parseLong(request.getParameter("productId"));
int quantity = Integer.parseInt(request.getParameter("quantity"));
Product product = productService.findById(productId);
// 从Session获取购物车,不存在则创建
HttpSession session = request.getSession();
Cart cart = (Cart) session.getAttribute("cart");
if (cart == null) {
cart = new Cart();
session.setAttribute("cart", cart);
}
cart.addItem(product, quantity);
// 跳转回购物车页面
response.sendRedirect(request.getContextPath() + "/cart/list");
}
}如果使用Spring MVC,写法会更简洁一些,可以直接把Cart通过@SessionAttribute注解绑定到会话中,控制器方法里直接操作即可:
@Controller
@SessionAttributes("cart")
public class CartController {
@ModelAttribute("cart")
public Cart getCart() {
return new Cart();
}
@PostMapping("/cart/add")
public String addToCart(Long productId, int quantity, Cart cart) {
Product product = productService.findById(productId);
cart.addItem(product, quantity);
return "redirect:/cart/list";
}
@GetMapping("/cart/list")
public String listCart(Cart cart, Model model) {
model.addAttribute("items", cart.getItems().values());
model.addAttribute("total", cart.getTotal());
return "cart";
}
}Session方案的局限也要清楚:用户关闭浏览器或长时间不操作,Session过期后购物车数据就丢了;服务器重启同样会导致数据丢失。此外分布式部署时,请求被负载均衡到不同节点,Session不共享就会出问题,需要引入Session复制或Spring Session这样的集中式存储来弥补。
三、Session方案与Redis方案的对比及选型建议
当系统规模上来之后,购物车通常要迁移到Redis中存储。两种方案各有适用场景,下面从几个维度做对比:
| 对比维度 | Session存储 | Redis存储 |
|---|---|---|
| 实现复杂度 | 低,几行代码搞定 | 中,需设计key结构和序列化 |
| 数据持久性 | 会话过期即丢失 | 可长期保存,跨设备可见 |
| 分布式支持 | 弱,需额外方案 | 天然支持集群部署 |
| 数据库压力 | 无 | 无,但占用内存资源 |
| 适合场景 | 小型网站、练习项目 | 中大型电商、需要购物车持久化的业务 |
采用Redis方案时,一般以用户ID作为key的一部分,例如cart:10086,value存储商品ID与数量的映射,使用Hash结构最合适。添加商品的核心操作可以借助HINCRBY命令原子性地累加数量,不用担心并发覆盖问题。用Jedis客户端的示例代码如下:
public class RedisCartService {
private final Jedis jedis;
public RedisCartService(Jedis jedis) {
this.jedis = jedis;
}
private String key(Long userId) {
return "cart:" + userId;
}
// 添加或累加商品数量,HINCRBY保证原子性
public void addItem(Long userId, Long productId, int quantity) {
jedis.hincrBy(key(userId), String.valueOf(productId), quantity);
}
// 修改数量,直接覆盖
public void updateQuantity(Long userId, Long productId, int quantity) {
jedis.hset(key(userId), String.valueOf(productId), String.valueOf(quantity));
}
// 删除单个商品
public void removeItem(Long userId, Long productId) {
jedis.hdel(key(userId), String.valueOf(productId));
}
// 获取整个购物车
public Map<String, String> getItems(Long userId) {
return jedis.hgetAll(key(userId));
}
}还有一个实际业务中绕不开的细节:游客浏览时把商品加入了购物车,注册登录后怎么处理?常见做法是登录成功后检查Session中是否存在临时购物车,如果有就把里面的条目合并到该用户在Redis中的购物车里,合并完成后清空临时购物车。合并时注意以Redis中的数据为准做数量覆盖还是叠加,需要根据业务规则明确下来。
最后提一点容易被忽略的问题:购物车里存的商品价格只是快照,结算时必须以数据库中的实时价格为准重新计算,防止用户长时间挂机期间价格变动导致结算金额与实际不符。同时下单前要校验库存,避免超卖。这些边界处理做好了,购物车功能才算真正完整。
Java购物车Session购物车购物车功能实现修改时间:2026-09-03 16:41:16