购物车是电商项目常用的功能,传统的做法可以使用关系型数据库,比如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