1. 购物车需求背景与业务整体设计
1.1 写在前面
1.1.1 需求背景
商城购物车模拟了传统的现实世界中真实存在的购物车的功能,便于用户挑选心仪商品统一结算等。同时还能在这个点上加以创新,加一些其他的功能。比如:比价,推荐(可作为商家的竞价广告位)等,甚至还可以统计数据告诉卖家,有多少人添加了购物车(代表有购物意向),结果没有付款(尝试分析原因)。
1.1.2 购物车的妙用
购物车在实际使用中对用户来说,兼具凑单、促销、收藏的功能。
- 凑单
在用户浏览商品详情页的时候,有两种选项:一种是“立即购买”, 另一种是“加入购物车”。当用户本身需求较多,想一次购买多种商品, 或者参与到优惠活动中(如满减、满赠等),这时候会将商品加入购物车进行凑单。
- 促销
购物车还有促销方面的功能,用于提高客单价。当有促销活动(满减、满赠)时,用户将商品加入购物车之后,可以查看是否满足优惠条件和优惠之后的金额(不包含优惠券)。
- 收藏
对于大部分用户来说,购物车发挥更多的是收藏的作用:“这东西看着不错,等以后再下单。”另外还有筛选的作用。比如笔者网购时, 会先加入购物车收藏,后面有时间再在购物车中筛选之后购买。
1.2 业务设计
1.2.1 通用显示
- 商品信息
- 促销信息
- 选中状态
默认全选、默认全不选、继承上次选中
- 结算
1.2.2 离线购物车
离线购物车指的是用户在未登录状态下把商品加入购物车,一般通过创建虚拟用户实现。为了更好的用户体验,需要让用户在下单之前,允许未登录先将商品加入购物车。
用户登录之后,涉及离线购物车和在线购物车合并。首先判断当前是否有离线购物车,然后将离线购物车的数据和在线购物车的数据进行合并。
1.2.3 商品监控
- 库存监控
设置库存提醒值,判断当前商品的数量,当库存数大于0并 小于提醒值时,提醒用户库存不足,请尽快下单;当库存数等于0时, 提醒无货。
- 状态监控
当商品下架后,提示商品无效;
无效商品进入无效商品列表中,可批量清除。
- 价格监控
购物车的商品价格变动时给用户提示,比如降价20元,会对用户的消费决策产生影响。
1.2.4 分类排序
- 商家店铺,将不同店铺的商品分开;
- 优惠不同,在购物车中将优惠活动相同的商品聚合在一起;
- 加入时间,按照加入购物车的时间倒序排列,最近添加的商品排列在 前。
1.2.5 促销信息
购物车中显示促销相关信息,类似满减、满赠、赠品等信息。例如在购物车中显示“满500减100”、“全场满减”、“商品的赠品有哪些”。还可以引导客户去店铺领取优惠券。在购物车中展示促销信息对提高客单价有良好效果,笔者认为目前最好用的购物车非京东莫属。
1.2.6 商品推荐
在购物车底部,是最好的商品宣传位,可以添加为商品推荐区域。 至于商品推荐的内容,会根据用户数据做定向推荐,这里不做扩展。
1.2.7 编辑
编辑购物车时主要可以进行的操作:删除商品、加减商品数量、更改选中状态、更改商品规格等。
1.2.8 结算
在购物车选中商品时,会实时算出订单金额。在购物车中计算时,需要将优惠金额算进去。
计算优惠金额还需要考虑满减促销、多种优惠券同时满足订单时用户自主选择还是推荐最优券等等。
2. 购物车核心链路和技术方案设计
2.1 核心链路
2.2 技术方案设计
部分电商公司对于购物车数据会选择不落库处理,只做Redis缓存;本次案例以Redis为主,同时通过RocketMQ将购物车数据异步更新到MySQL,不需要去保证Redis缓存和MySQL持久化的数据一致性,数据库只是作为数据备份。
业务上来说,购物车其实是临时性的数据。仅仅是把一些商品再购物车里进行暂存,迟早会购买(从购物车里删除),或者长时间没有购买,已经遗忘了加购过什么商品。就算在极端情况下丢失数据,也影响不大,这时用户重新加入就好了。
2.2.1 Redis
- 缓存穿透
当请求穿透缓存时,通过写入空缓存、分布式锁限制MySQL的负载。
2.2.2 表结构
-- auto-generated definition create table cart ( id bigint auto_increment comment '主键ID' primary key, user_id bigint default 0 not null comment '用户ID', product_sku_id bigint default 0 not null comment '商品SKU ID', product_sku_num int default 0 not null comment '商品SKU数量', add_amount decimal(10, 2) default 0.00 not null comment '加购价格', selected_status tinyint default 1 not null comment '选中状态 0:未选中 1:已选中', is_deleted tinyint default 0 not null comment '是否删除 0:未删除 1:已删除', create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改时间' ) comment '购物车表'; create index idx_product_sku_id on cart (product_sku_id); create index idx_user_id on cart (user_id);
2.2.3 MQ
加入购物车/更新购物车通过RocketMQ异步入库
3. 购物车的阈值检查与重复加入逻辑
3.1 为什么淘宝购物车要设置上限?
3.1.1 回归原始 - 拟物
实体购物车因为有物理的空间限制,理论上可以购买的商品件数和空间是有限的,因此电商的购物车其实在拟物化上也沿袭了这一设计。
站在平台和商家的立场考虑,购物车上限的提示是在一定程度上引导用户去结算或者清理的,提升购物车商品的曝光量,同时也是希望用户把购物车和收藏夹的场景区分开来的,保留购物车的核心功能。
3.1.2 需求大小与价值衡量
- 需求大小
从我自身的使用上,我目前还未遇到过购物车超过上限的情况,但是基于淘宝天猫这么大超过8亿的用户群体,碰到购物车超过上限的比例如果只有1%,那绝对值也有庞大的800万用户。
- 价值衡量
电商购物最终一定是以交易成交为目的的,也就是解决这个问题能否提升用户下单率和成交件数。这个其实是不一定的,当购物车上限过多时,有可能用户的购买决策会更难,周期可能会更长,同时购物车过多商品用户也会表示很难找到自己想结算的商品,所以在需求的价值上来说是需要慎重衡量的。
- 解决方案
(1)引导用户结算或者清理;
(2)引导用户先添加收藏夹再添加购物车;
(3)类似花呗限时提额的操作;
(4)类似美团外卖多个购物车的场景;
(5)在大促是限时提高购物车的上限。
3.1.3 技术和性能
在大促时,系统本来已经面临千万级上亿级别的并发量,为了避免购物车商品图片、价格等信息加载不出来,出现卡顿等情况,设置上限是保证用户体验的一个手段,当用户不能添加购物车需要清理购物车,和进入购物车出现卡顿或加载不出来相比,影响相对较小。
购物车结算涉及到跨店铺满减、商品库存、优惠券、订单金额计算等多达几十个服务的调用;
因此技术上为了避免宕机或者影响用户无法结算等糟糕的体验,设置购物车上限是一种产品上的妥协。
3.2 购物车需要设置哪些上限
- 购物车单个SKU上限
- 购物车加购SKU上限
3.3 重复加入
- 不校验重入逻辑
- 校验重入逻辑
@Slf4j @Component public class RedissonUtils { @Resource private RedissonClient redissonClient; public boolean lock(String key, long waitTime, long leaseTime) { RLock lock = redissonClient.getLock(key); try { return lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { log.error("[RedissonUtils][lock] - tryLock异常", e); } return false; } public void unLock(String key) { RLock lock = redissonClient.getLock(key); if (ObjectUtils.isNotEmpty(lock)) { lock.unlock(); } } }