购物车设计与实现

2022-04-28 20:40:35 浏览数 (1)

购物车是电商项目常用的功能,传统的做法可以使用关系型数据库,比如mysql来处理。但在实际使用中,由于购物车的数据量太大,而且修改频繁,会导致数据库的压力增加,所以一般不会直接使用关系型数据库来存储购物车信息。

既然不用关系型数据库,那么很多人就会选择mongodb或者redis来实现存放购物车信息,但考虑到性能方面来说,redis的方案更好。下面就聊聊如何使用redis来完成购物车的思路。

1、redis持久化和集群

在redis的配置文件中,增加AOF的相关配置:

代码语言:javascript复制
appendonly yes # 是否以append only模式作为持久化方式,默认使用的是rdb方式持久化,这种方式在许多应用中已经足够用了
appendfilename "appendonly.aof" # AOF 持久化文件名称
appendfsync everysec # appendfsync aof持久化策略的配置
					 # no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快。
                     # always表示每次写入都执行fsync,以保证数据同步到磁盘。
                     # everysec表示每秒执行一次fsync,可能会导致丢失这1秒的数据。

关于redis的集群搭建可以自行搜索。

2、业务分析

以京东购物车为例,按业务分析,需要完成如下功能:

  • 1、全选功能-获取所有该用户的所有购物车商品
  • 2、商品数量-购物车图标上要显示的购物车里商品的总数
  • 3、删除-要能移除购物车里某个商品
  • 4、增加或减少某个商品sku的数量
  • 5、挑选最合适的优惠券

注:京东的不需要登录就可以添加到购物车的,这采用的前端的缓存。本文主要是登录之后的后端操作。

2、数据结构选择

redis支持多种数据类型,比如string,hash,list,set,zset等。针对于购物车需求,明显选择hash来做更合适。

HSET 将哈希表 key(用户id) field(skuId) value(SKU数量,是否选中,添加时间等)

代码语言:javascript复制
redis 127.0.0.1:6379> HSET cart:userId skuId "{"shopId":"123123","skuId":"342342342","num":2,"selected":1,"addTime":123123213123}"

下面设计一下保存到redis中的购物车相关数据结构:

购物车商品实体:

代码语言:javascript复制
@Data
@ApiModel("购物车-商品-缓存实体")
public class CartItem {

    @ApiModelProperty(value = "店铺id")
    private String shopId;

    @ApiModelProperty(value = "skuId")
    private String skuId;

    @ApiModelProperty(value = "商品数量")
    private Integer num;

    @ApiModelProperty(value = "是否选中,1是,0否")
    private Integer selected;

    @ApiModelProperty(value = "添加到购物车时间")
    private String addTime;

    @ApiModelProperty(value = "修改购物车时间")
    private String updateTime;
}

3、添加商品

添加某商品SKU到购物车中需要考虑是否已存在该sku,如果存在直接将数量往上加即可。

代码语言:javascript复制
    private static final String CART_KEY = "wechat:cart:";

    public void addCart(Long userId , CartItem cartItem) {
        if (cartItem.getNum() <= 0) {
            throw new ApiException("保存购物车商品数量需大于0");
        }
        if (goodsNum(userId) >= MAX_GOODS) {
            throw new ApiException("购物车最多添加99件商品");
        }
        try {
            String key = CART_KEY   userId;
            String cartItemStr = (String) redisTemplate.opsForHash().get(key, cartItem.getSkuId());
            CartItem item = JSON.parseObject(cartItemStr, CartItem.class);
            if (ObjectUtil.isNotNull(item) && ObjectUtil.isNotNull(item.getSkuId())) { 
  // 已存在商品 直接修改数量
                item.setNum(checkStock(item.getNum()   cartItem.getNum() , Long.parseLong(cartItem.getSkuId())));
                item.setUpdateTime(String.valueOf(System.currentTimeMillis()));
                redisTemplate.opsForHash().put(key , cartItem.getSkuId(), JSON.toJSONString(item));
            }else { 
  // 不存在商品
                cartItem.setNum(checkStock(cartItem.getNum() , Long.parseLong(cartItem.getSkuId())));
                redisTemplate.opsForHash().put(key , cartItem.getSkuId(), JSON.toJSONString(cartItem));
            }
        }catch (ApiException e) {
            throw e;
        }catch (Exception e) {
            e.printStackTrace();
            log.error("添加购物车缓存失败 用户id {} , cartItem {} , 错误信息: {}" , userId , cartItem , e.getMessage());
            throw new ApiException(ResultCode.ERROR.getMessage());
        }
    }

4、删除商品

删除购物车商品,直接根据用户id和商品skuId进行删除即可。除了用户手动删除购物车中指定商品,还会在下单的时候需要删除购物车中对应的商品。

代码语言:javascript复制
   /**
     * 删除购物车商品
     */
    public void deleteCart(List<Long> skuIds , Long userId) {
        try {
            String key = CART_KEY   userId;
            redisTemplate.opsForHash().delete(key , skuIds.stream().map(String::valueOf).toArray());
        }catch (Exception e) {
            log.error("删除购物车缓存失败 用户id {} , skuId {} , 错误信息: {}" , userId , skuIds, e.getMessage());
            throw new ApiException(ResultCode.ERROR.getMessage());
        }
    }

5、修改商品

修改购物车中的商品主要涉及修改商品SKU的数量、替换了该商品的其他规格SKU.

代码语言:javascript复制


public void updateSkuCart(Long userId , Long oldSkuId , Long newSkuId , int goodsNum) {
        try {
            if (goodsNum <= 0) {
                return;
            }
            String key = CART_KEY   userId;
            String jsonStr = (String) redisTemplate.opsForHash().get(key, oldSkuId.toString());
            CartItem cartItem = JSON.parseObject(jsonStr, CartItem.class);
            if (ObjectUtil.isNull(cartItem) || ObjectUtil.isNull(cartItem.getSkuId())) {
                throw new ApiException("该商品不存在");
            }
            if (oldSkuId.equals(newSkuId)) {
                // 修改自己数量
                cartItem.setNum(checkStock(goodsNum , oldSkuId));
                cartItem.setUpdateTime(String.valueOf(System.currentTimeMillis()));
                redisTemplate.opsForHash().put(key , cartItem.getSkuId() , JSON.toJSONString(cartItem));
            }else {
                // 修改其他商品
                String newJsonStr = (String) redisTemplate.opsForHash().get(key, newSkuId.toString());
                CartItem newCartItem = JSON.parseObject(newJsonStr, CartItem.class);
                if (ObjectUtil.isNull(newCartItem) || ObjectUtil.isNull(newCartItem.getSkuId())) {
                    // 添加购物车商品不存在
                    cartItem.setSkuId(newSkuId.toString());
                    cartItem.setNum(checkStock(goodsNum , newSkuId));
                    cartItem.setUpdateTime(String.valueOf(System.currentTimeMillis()));
                    redisTemplate.opsForHash().put(key , newSkuId.toString() , JSON.toJSONString(cartItem));
                }else { 
  // 添加购物车商品已经存在 修改数量
                    newCartItem.setNum(checkStock(newCartItem.getNum()   goodsNum , newSkuId));
                    newCartItem.setUpdateTime(String.valueOf(System.currentTimeMillis()));
                    redisTemplate.opsForHash().put(key , newSkuId.toString() , JSON.toJSONString(newCartItem));
                }
                redisTemplate.opsForHash().delete(key , oldSkuId.toString()); // 把旧的商品删除
            }
        }catch (ApiException e){
            log.error("修改购物车失败userId {} , skuId {} , 错误信息: {}" , userId , oldSkuId , e.getMessage());
            e.printStackTrace();
            throw e;
        }catch (Exception e){
            log.error("修改购物车失败userId {} , skuId {} , 错误信息: {}" , userId , oldSkuId , e.getMessage());
            e.printStackTrace();
            throw new ApiException(ResultCode.ERROR.getMessage());
        }
    }

   private Integer checkStock(Integer num , Long skuId) {
        YyShopGoodsSku yyShopGoodsSku = yyShopGoodsSkuMapper.selectByPrimaryKey(skuId);
        if (yyShopGoodsSku.getStock().equals(0)) {
            throw new ApiException(ResultCode.GOODS_STOCK_INSUFFICIENT.getMessage());
        }
        return yyShopGoodsSku.getStock() < num ? yyShopGoodsSku.getStock() : num;
    }

6、获取商品列表

获取购物车的列表,可以通过用户id以及是否选中来进行筛选。

代码语言:javascript复制
  /**
     * 获取购物车 key-skuId  value-CartItem
     */
    public Map<Long , CartItem> listCart(Long userId,Integer selected) {
        String rkey = CART_KEY   userId;
        try(Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(rkey ,
                ScanOptions.NONE)) {
            Map<Long , CartItem> resultMap = new HashMap<>();
            while (cursor.hasNext()) {
                Map.Entry<Object, Object> next = cursor.next();
                Long key = Long.parseLong((String) next.getKey());
                String value = (String) next.getValue();
                if (ObjectUtil.isNull(key) || StringUtils.isBlank(value)) {
                    continue;
                }
                CartItem cartItem = JSON.parseObject(value, CartItem.class);
                if (ObjectUtil.isNull(selected) || ObjectUtil.equal(cartItem.getSelected(),selected)){
                    resultMap.put(key , JSON.parseObject(value , CartItem.class));
                }
            }
            return resultMap;
        }catch (Exception e) {
            log.error("查询购物车缓存失败 用户id {} , 错误信息: {}" , userId , e.getMessage());
            throw new ApiException(ResultCode.ERROR.getMessage());
        }
    }

7、获取商品数量

代码语言:javascript复制
   /**
     * 获取购物车商品数量
     */
    public int goodsNum(Long userId) {
        String key = CART_KEY   userId;
        Long num = redisTemplate.opsForHash().size(key);
        return ObjectUtil.isNull(num) ? 0 : num.intValue();
    }

参考文章

https://wenku.baidu.com/view/89ea17dbcbd376eeaeaad1f34693daef5ff71357.html

0 人点赞