在传统单体架构的电商系统中,购物车功能通常与订单、商品等模块耦合在一起。随着业务规模扩大,这种架构会面临几个典型问题:大促期间购物车操作频繁导致整个系统响应变慢、库存校验逻辑变更需要全量发布、不同业务线对购物车有差异化需求但难以灵活扩展。
去年我们系统在双11期间就遇到了这样的困境——由于购物车和订单服务共享数据库连接池,高峰期购物车的频繁更新操作直接拖垮了整个订单创建流程。这促使我们下决心对系统进行微服务化改造,而购物车作为高频核心业务成为首批拆分对象。
采用SpringCloud Alibaba全家桶作为技术底座:
特别说明选择Nacos而非Apollo的考量:购物车需要频繁读取促销配置(如满减规则),Nacos的监听机制能实现毫秒级配置推送,避免Apollo需要轮询带来的延迟。
采用多级存储架构:
java复制// 存储策略伪代码
public CartData getCart(String userId) {
// 第一层:本地缓存(Caffeine)
CartData cache = localCache.get(userId);
if (cache != null) return cache;
// 第二层:Redis集群(带分片)
cache = redisTemplate.opsForValue().get(buildRedisKey(userId));
if (cache != null) {
localCache.put(userId, cache);
return cache;
}
// 第三层:MySQL分库(按用户ID哈希)
return cartRepository.findByUserId(userId);
}
Redis采用CRC16分片算法,将不同用户的购物车数据分散到多个节点。实测数据显示,这种方案在100万用户并发访问时,Redis集群负载仍能保持在70%以下。
采用JSON+关系型的混合存储方案:
json复制// Redis中存储的主体结构
{
"cartId": "cart_123456",
"userId": "u10001",
"items": [
{
"skuId": "sku_8888",
"quantity": 2,
"selected": true,
"price": 3999,
"specs": {"color":"red","memory":"128G"},
"timestamp": 1634567890
}
],
"extras": {
"couponId": "coupon_666",
"giftWrap": true
}
}
MySQL中仅存储关键索引字段:
sql复制CREATE TABLE `cart_index` (
`cart_id` varchar(32) NOT NULL,
`user_id` varchar(32) NOT NULL,
`last_update` timestamp NOT NULL,
PRIMARY KEY (`cart_id`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
采用乐观锁+补偿机制处理并发修改:
java复制@Transactional
public void updateItemQuantity(String userId, String skuId, int newQty) {
// 1. 获取当前版本号
long version = redisTemplate.opsForValue().increment("cart_version:" + userId);
try {
// 2. 更新操作
Cart cart = getCart(userId);
cart.updateItem(skuId, newQty);
saveCart(userId, cart, version);
} catch (VersionConflictException e) {
// 3. 冲突时自动重试
log.warn("Cart version conflict, retrying...");
updateItemQuantity(userId, skuId, newQty);
}
}
实测发现当冲突率低于15%时,这种方案性能优于悲观锁。我们在压测时模拟了2000TPS的更新请求,平均延迟控制在50ms以内。
采用"先更新数据库再删除缓存"的策略,配合消息队列确保最终一致:
java复制// 更新购物车后的处理
@Transactional
public void saveCart(Cart cart) {
// 1. 更新MySQL
cartRepository.update(cart);
// 2. 发送MQ事件
mqProducer.send(new Message(
"CART_UPDATE_TOPIC",
new CartUpdateEvent(cart.getUserId()).toString()
));
}
// 消费者逻辑
@RocketMQMessageListener(topic = "CART_UPDATE_TOPIC")
public class CartUpdateListener {
public void onMessage(CartUpdateEvent event) {
// 3. 删除Redis缓存
redisTemplate.delete("cart:" + event.getUserId());
// 4. 删除本地缓存
localCache.invalidate(event.getUserId());
}
}
商品服务调用采用多级降级策略:
java复制public ItemInfo getItemInfo(String skuId) {
try {
// 1. 实时调用
return productFeignClient.getSkuInfo(skuId);
} catch (Exception e) {
log.error("商品服务调用失败", e);
// 2. 读取本地缓存
ItemInfo cache = itemCache.get(skuId);
if (cache != null) return cache;
// 3. 降级返回
return new ItemInfo(skuId, "价格可能已变更", -1);
}
}
针对秒杀商品加入购物车的场景,采用本地缓存+Redis缓存的二级过滤:
lua复制-- redis_scripts/check_seckill.lua
local stockKey = KEYS[1]
local boughtKey = KEYS[2]
local userId = ARGV[1]
local maxLimit = tonumber(ARGV[2])
-- 检查库存
local stock = redis.call('GET', stockKey)
if not stock or tonumber(stock) <= 0 then
return 0
end
-- 检查购买限制
local bought = redis.call('HGET', boughtKey, userId)
if bought and tonumber(bought) >= maxLimit then
return 0
end
-- 扣减库存
redis.call('DECR', stockKey)
redis.call('HINCRBY', boughtKey, userId, 1)
return 1
购物车查询接口采用以下优化手段:
java复制public CartVO getCartDetail(String userId) {
// 1. 并行获取基础数据
CompletableFuture<Cart> cartFuture = CompletableFuture
.supplyAsync(() -> getCart(userId), poolA);
CompletableFuture<List<Promotion>> promoFuture = CompletableFuture
.supplyAsync(() -> promotionService.getPromotions(userId), poolB);
// 2. 合并结果
return CompletableFuture.allOf(cartFuture, promoFuture)
.thenApply(v -> {
Cart cart = cartFuture.join();
List<Promotion> promos = promoFuture.join();
return assembler.assemble(cart, promos);
}).join();
}
优化后接口响应时间从平均220ms降低到80ms,99线控制在150ms以内。
核心监控指标包括:
Grafana监控看板配置示例:
sql复制-- 购物车转化率计算
SELECT
COUNT(DISTINCT order_id) / COUNT(DISTINCT cart_id) AS conversion_rate
FROM
cart_events
WHERE
time > NOW() - INTERVAL '1 DAY'
采用ELK体系收集关键日志:
日志格式规范:
log复制2023-08-20 14:30:45 [INFO] [traceId=abc123] CartService - User u10001 updated cart
items: [{"skuId":"sku888","qty":3}], source=MOBILE
针对恶意刷购物车行为,采用以下防护策略:
防护代码示例:
java复制public void addItem(String userId, String skuId) {
// 1. 检查操作频率
if (rateLimiter.exceedsLimit(userId)) {
throw new BizException("操作过于频繁");
}
// 2. 设备指纹验证
String deviceId = SecurityUtils.getDeviceId();
if (blacklistService.isBlocked(deviceId)) {
throw new BizException("可疑操作已被拦截");
}
// 正常业务逻辑
cartService.addItem(userId, skuId);
}
采用RBAC模型进行数据隔离:
AOP实现示例:
java复制@Before("execution(* com..cart.*.*(..)) && args(userId, ..)")
public void checkCartAccess(String userId) {
String currentUser = SecurityUtils.getCurrentUser();
if (!currentUser.equals(userId)) {
throw new AccessDeniedException("无权访问该购物车");
}
}
在初期上线时遇到过Redis集群大面积超时的情况,排查发现是缓存Key集中过期导致。解决方案:
曾尝试用Seata处理购物车清空和订单创建的强一致性,发现性能无法满足要求。最终方案:
历史数据迁移时直接全量导入导致Redis阻塞,改进方案:
迁移脚本关键参数:
bash复制# 迁移工具启动参数
./migrate-tool \
--qps 5000 \ # 每秒最大查询量
--batch-size 200 \ # 每批处理记录数
--threads 16 # 并发线程数