1. 购物车需求背景与业务整体设计

1.1 写在前面

1.1.1 需求背景

商城购物车模拟了传统的现实世界中真实存在的购物车的功能,便于用户挑选心仪商品统一结算等。同时还能在这个点上加以创新,加一些其他的功能。比如:比价,推荐(可作为商家的竞价广告位)等,甚至还可以统计数据告诉卖家,有多少人添加了购物车(代表有购物意向),结果没有付款(尝试分析原因)。

1.1.2 购物车的妙用

购物车在实际使用中对用户来说,兼具凑单、促销、收藏的功能。
在这里插入图片描述

  1. 凑单

在用户浏览商品详情页的时候,有两种选项:一种是“立即购买”, 另一种是“加入购物车”。当用户本身需求较多,想一次购买多种商品, 或者参与到优惠活动中(如满减、满赠等),这时候会将商品加入购物车进行凑单。

  1. 促销

购物车还有促销方面的功能,用于提高客单价。当有促销活动(满减、满赠)时,用户将商品加入购物车之后,可以查看是否满足优惠条件和优惠之后的金额(不包含优惠券)。

  1. 收藏

对于大部分用户来说,购物车发挥更多的是收藏的作用:“这东西看着不错,等以后再下单。”另外还有筛选的作用。比如笔者网购时, 会先加入购物车收藏,后面有时间再在购物车中筛选之后购买。

1.2 业务设计

在这里插入图片描述

1.2.1 通用显示

  1. 商品信息

  2. 促销信息

  3. 选中状态

默认全选、默认全不选、继承上次选中

  1. 结算

1.2.2 离线购物车

离线购物车指的是用户在未登录状态下把商品加入购物车,一般通过创建虚拟用户实现。为了更好的用户体验,需要让用户在下单之前,允许未登录先将商品加入购物车。

用户登录之后,涉及离线购物车和在线购物车合并。首先判断当前是否有离线购物车,然后将离线购物车的数据和在线购物车的数据进行合并。

1.2.3 商品监控

  1. 库存监控

设置库存提醒值,判断当前商品的数量,当库存数大于0并 小于提醒值时,提醒用户库存不足,请尽快下单;当库存数等于0时, 提醒无货。

  1. 状态监控

当商品下架后,提示商品无效;
无效商品进入无效商品列表中,可批量清除。

  1. 价格监控

购物车的商品价格变动时给用户提示,比如降价20元,会对用户的消费决策产生影响。

1.2.4 分类排序

  1. 商家店铺,将不同店铺的商品分开;
  2. 优惠不同,在购物车中将优惠活动相同的商品聚合在一起;
  3. 加入时间,按照加入购物车的时间倒序排列,最近添加的商品排列在 前。

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 需求大小与价值衡量

  1. 需求大小

从我自身的使用上,我目前还未遇到过购物车超过上限的情况,但是基于淘宝天猫这么大超过8亿的用户群体,碰到购物车超过上限的比例如果只有1%,那绝对值也有庞大的800万用户。

  1. 价值衡量

电商购物最终一定是以交易成交为目的的,也就是解决这个问题能否提升用户下单率和成交件数。这个其实是不一定的,当购物车上限过多时,有可能用户的购买决策会更难,周期可能会更长,同时购物车过多商品用户也会表示很难找到自己想结算的商品,所以在需求的价值上来说是需要慎重衡量的。

  1. 解决方案

(1)引导用户结算或者清理;
(2)引导用户先添加收藏夹再添加购物车;
(3)类似花呗限时提额的操作;
(4)类似美团外卖多个购物车的场景;
(5)在大促是限时提高购物车的上限。

3.1.3 技术和性能

在大促时,系统本来已经面临千万级上亿级别的并发量,为了避免购物车商品图片、价格等信息加载不出来,出现卡顿等情况,设置上限是保证用户体验的一个手段,当用户不能添加购物车需要清理购物车,和进入购物车出现卡顿或加载不出来相比,影响相对较小。

购物车结算涉及到跨店铺满减、商品库存、优惠券、订单金额计算等多达几十个服务的调用
因此技术上为了避免宕机或者影响用户无法结算等糟糕的体验,设置购物车上限是一种产品上的妥协。

3.2 购物车需要设置哪些上限

  1. 购物车单个SKU上限
  2. 购物车加购SKU上限

3.3 重复加入

  1. 不校验重入逻辑

  2. 校验重入逻辑

@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();
        }
    }
}

4. 购物车加入商品多线程并发问题解决

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐