Redis 实现购物车(附源码)

Redis 实现购物车(附源码)Redis 是完全开源的 遵守 BSD 协议 是一个高性能的 key value 数据库 使用 Redis 做购物车存储好处有两点 无需操作数据库 当访问量比较大时 高速读写数据库 对数据库压力比较大

大家好,欢迎来到IT知识分享网。

写在前面

Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库。 Redis 与其他 key – value 缓存产品有以下三个特点:

Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。Redis不仅仅支持简单的key-value类型的数据,同时还提供string、list、set、zset、hash五种数据结构的存储。Redis支持数据的备份,即master-slave模式的数据备份。

使用Redis做购物车存储好处有两点:

无需操作数据库,当访问量比较大时,高速读写数据库,对数据库压力比较大。Redis性能好,并发高。

一、 设计思想

使用Redis来实现购物车功能第一个要解决的问题就是Key的问题。需要我们来判断一对多关系中,谁是唯一谁就是Key。对于购物车来说用户就是唯一,那么就可以使用用户主键来作为购车的Key:项目名:模块名:用户主键,如:WHALE:CART:5。第二个要解决的问题就是,我们要存什么东西,这里有两种方案:

  • 半持久化:直接存店铺id+商品Id作为购物车项Key,前端先查redis获取商品清单ids,然后根据ids异步查询具体数据。优点是如果商品价格改了,前端能够实时知道商品的价格。缺点适应是场景少,对数据库有一定的压力。如果商品只有SKU还好,如果商品还有一些其他属性时,当用户选择商品,选择商品属性时,后台是没有记录用户选了那些属性的。解决方案是把属性Id也加到Key中并且缓存起来,查询时和商品id作为参数查询具体的购物车项的明细。
  • 不持久化:将购物车项的所有必要信息作为值存到Redis中,根据用户购物车Key就可以获取购物车商品集合。优点是性能高,速度快,不操作数据库。缺点是如果商品价格修改了,redis中的商品价格还是未改之前的,商品价格不能同步。也就是缓存中的信息和数据库中的信息会有不同步的情况。解决方案是添加购车时将SKU的id作为Key,价格作为值缓存起来,如果属性包含价格也需要将属性的Id作为Key,价格作为值缓存起来。如果SKU或者属性价格有修改则在修改方法中如果Redis中存在该SKU或属性则更新Redis中对应的数据,微服务架构可以通过消息队列更新Redis中的价格。查询购物车时商品价格根据SKU和属性价格计算出来返回给前端。

二、 购物车时序图

Redis 实现购物车(附源码)

三、代码示例

BaseEntity.class:实体父类

@MappedSuperclass public class BaseEntity<ID> implements Serializable { private static final long serialVersionUID = L; @Id @JsonSerialize( using = ToStringSerializer.class ) private ID id; private Integer status; @JsonDeserialize( using = LocalDateTimeDeserializer.class ) @JsonSerialize( using = LocalDateTimeSerializer.class ) private LocalDateTime createTime; @JsonDeserialize( using = LocalDateTimeDeserializer.class ) @JsonSerialize( using = LocalDateTimeSerializer.class ) private LocalDateTime updateTime; public BaseEntity() { } public ID getId() { return this.id; } public void setId(ID id) { this.id = id; } public Integer getStatus() { return this.status; } public void setStatus(Integer status) { this.status = status; } public LocalDateTime getCreateTime() { return this.createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public LocalDateTime getUpdateTime() { return this.updateTime; } public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; } public String toString() { return "BaseEntity{id=" + this.id + ", status=" + this.status + ", createTime=" + this.createTime + ", updateTime=" + this.updateTime + '}'; } } 

Car.class:购物车实体类

@Data public class Cart { @ApiModelProperty(value = "购物车项Id") private String cartItemId; @ApiModelProperty(value = "商品Id", required = true) @NotNull(message = "productId不能为空") @JsonSerialize(using = ToStringSerializer.class) private Long productId; @ApiModelProperty(value = "skuId", required = true) @NotNull(message = "skuId不能为空") @JsonSerialize(using = ToStringSerializer.class) private Long skuId; @ApiModelProperty(value = "skuPrice", required = true) @NotNull(message = "sku售价不能为空") @JsonSerialize(using = ToStringSerializer.class) private BigDecimal skuPrice; @ApiModelProperty(value = "discountSkuPrice", required = true) @JsonSerialize(using = ToStringSerializer.class) private BigDecimal discountSkuPrice; @ApiModelProperty(value = "购买数量", required = true, example = "1") @NotNull(message = "购买数量不能为空") private Long quantity; @ApiModelProperty(value = "商品名", required = true) @NotBlank(message = "商品名") private String name; @ApiModelProperty(value = "商品图片url", required = true) @NotBlank(message = "商品图片url") private String images; @ApiModelProperty(value = "售价", required = true) @NotNull(message = "商品价格不能为空") private BigDecimal price; @ApiModelProperty(value = "打包费", required = false) private BigDecimal packingFee; @ApiModelProperty(value = "折扣价", required = true) private BigDecimal discountPrice; @ApiModelProperty(value = "是否勾选", required = true) private Boolean check; @ApiModelProperty(value = "购买的商品列表", required = true) @NotEmpty(message = "购买的商品属性列表不能为空") private List<ProductProperty> propertys; @ApiModelProperty(value = "商品属性值", required = true) @NotBlank(message = "商品属性值不能为空") private String propValues; @ApiModelProperty(value = "商品子标题", required = true) private String subtitle; @ApiModelProperty(value = "是否可以外带", required = true) private String canTakeout; @ApiModelProperty(value = "门店id") @NotNull(message = "门店id不能为空") @JsonSerialize(using = ToStringSerializer.class) private Long shopId; } 

ProductProperty.class:商品属性类

@Data @Entity public class ProductProperty extends BaseEntity<Long> implements Comparable<ProductProperty>, Serializable { @ApiModelProperty(value = "商品id") @JsonSerialize(using = ToStringSerializer.class) private Long productId; @ApiModelProperty(value = "属性名id") @JsonSerialize(using = ToStringSerializer.class) private Long propId; @ApiModelProperty(value = "属性名") private String propName; @ApiModelProperty(value = "属性类型") private Integer rule; @ApiModelProperty(value = "属性值") private String propValue; @ApiModelProperty(value = "属性值id") @JsonSerialize(using = ToStringSerializer.class) private Long propValueId; @ApiModelProperty(value = "额外价格") private BigDecimal price; @ApiModelProperty(value = "顺序索引") private Integer sequence; @ApiModelProperty(value = "组序列") private Integer groupSequence; @ApiModelProperty(value = "显示状态 1:显示 0:隐藏") private Integer visible = 1; @Override public int compareTo(ProductProperty p2) { return Comparator.comparing(ProductProperty::getRule) .thenComparing(ProductProperty::getGroupSequence) .thenComparing(ProductProperty::getSequence) .compare(this, p2); } public static boolean isDeletedOrHidden(ProductProperty productProperty) { return productProperty.getStatus().equals(DataStatus.DELETED.getStatus()) || productProperty.getVisible() == 0; } } 

CarController.class:Controller类

@Slf4j @RestController @AllArgsConstructor @Api(value = "ECCart", tags = {"【EC购物车】- ECCart"}) public class EcCartController { private static final String MINI_URL_PREFIX = UrlConstant.URL_EC_API_PREFIX + "cart"; private final CartService cartService; private final ProductSkuService productSkuService; private final ProductService productService; / * 查询我的购物车 * @return */ @ApiOperation(value = "查询我的购物车") @PostMapping(value = MINI_URL_PREFIX + "/list") public Response<List<Cart>> queryCarts(@RequestBody Cart dto){ if(Objects.isNull(dto.getShopId())){ return ResponseUtils.error(1, "请选择门店"); } return ResponseUtils.success(cartService.queryCarts(RequestUserHolder.getUserId(),dto.getShopId())); } / * 添加到购物车 * @param dto * @return */ @ApiOperation(value = "添加到购物车") @PostMapping(value = MINI_URL_PREFIX + "/add") public Response<Boolean> addCart(@RequestBody Cart dto) { // 查询是否删除 long pscount = productService.findByIdAndStatus(dto.getProductId()); if(pscount > 0){ return ResponseUtils.error(1, "商品不存在"); } // 查询是否下架 long pvcount = productService.findByIdAndVisible(dto.getProductId()); if(pvcount > 0){ return ResponseUtils.error(1, "商品已下架"); } // 查询库存 long count = productSkuService.findBySkuIdAndStatus(dto.getSkuId()); // 判断是否有无库存 if(count == 0) { return ResponseUtils.error(1, "暂无库存"); } return cartService.addCart(dto, RequestUserHolder.getUserId()); } / * 添加到购物车 * @param dto * @return */ @ApiOperation(value = "批量添加到购物车") @PostMapping(value = MINI_URL_PREFIX + "/batch/add") public Response batchAddCart(@RequestBody CartDto dto) { Long shopId = dto.getShopId(); List<Cart> carts = new ArrayList<>(); try { for (Cart cart : dto.getCarts()) { // 传递shopId cart.setShopId(shopId); // 查询库存 long count = productSkuService.findBySkuIdAndStatus(cart.getSkuId()); // 判断是否有无库存 if(count == 0) { carts.add(cart); continue; } // 查询是否删除 long pscount = productService.findByIdAndStatus(cart.getProductId()); if(pscount > 0){ return ResponseUtils.error(1, "商品不存在"); } // 查询是否下架 long pvcount = productService.findByIdAndVisible(cart.getProductId()); if(pvcount > 0){ return ResponseUtils.error(1, "商品已下架"); } cartService.addCart(cart, RequestUserHolder.getUserId()); } if(carts.size() > 0) { return ResponseUtils.success(carts); }else { return ResponseUtils.success(); } }catch (Exception e) { e.printStackTrace(); } return ResponseUtils.error(1, "添加购物车失败"); } / * 更新购物车 * @param dto * @return */ @ApiOperation(value = "更新购物车") @PostMapping(value = MINI_URL_PREFIX + "/update") public Response<Boolean> updateCart(@RequestBody Cart dto){ return cartService.updateCart(dto, RequestUserHolder.getUserId()); } / * 删除购物车 * @param cartDto * @return */ @ApiOperation(value = "删除购物车商品", notes = "已选择要删除的商品列表") @PostMapping(value = MINI_URL_PREFIX + "/delete") public Response<Boolean> deleteCart(@RequestBody CartDto cartDto) { return cartService.deleteCart(cartDto.getCartIds(), RequestUserHolder.getUserId(),cartDto.getShopId()); } / * 删除购物车 * @return */ @ApiOperation(value = "删除购物车所有商品") @PostMapping(value = MINI_URL_PREFIX + "/delete/all") public Response<Boolean> deleteAllCart(@RequestBody CartDto cartDto) { return cartService.deleteAllCart(RequestUserHolder.getUserId(),cartDto.getShopId()); } } 

CarService.class:购物车接口类

public interface CartService { / * 查询我的购物车 * @param userId * @return */ List<Cart> queryCarts(Long userId,Long shopId); / * 添加购物车 * @param dto * @param userId * @return */ Response<Boolean> addCart(Cart dto, Long userId); / * 更新购物车 * @param dto * @param userId * @return */ Response<Boolean> updateCart(Cart dto, Long userId); / * 删除购物车 * @param cartIds * @param userId * @return */ Response<Boolean> deleteCart(List<String> cartIds, Long userId,Long shopid); / * 清空购物车 * @param userId * @return */ Response<Boolean> deleteAllCart(Long userId,Long shopId); } 

CarServiceImpl.class:购物车接口实现类

@Slf4j @Service @AllArgsConstructor public class CartServiceImpl implements CartService { private final StringRedisTemplate redisTemplate; / * 查询我的购物车 * @param userId * @return */ @Override public List<Cart> queryCarts(Long userId,Long shopId) { // 用户已登录,查询登录状态的购物车 String key = CacheConstant.RL_CART + userId + ":" + shopId; // 获取登录状态的购物车 BoundHashOperations<String, Object, Object> userIdOps = this.redisTemplate.boundHashOps(key); // 购物车数据 List<Object> userCartJsonList = userIdOps.values(); return userCartJsonList.stream().map(userCartJson-> { Cart cart = JSON.parseObject(userCartJson.toString(), Cart.class); // 查询单价价格 String discountPrice = redisTemplate.opsForValue().get(CacheConstant.RL_DISCOUNT_SKU + cart.getSkuId()); String price = redisTemplate.opsForValue().get(CacheConstant.RL_DISCOUNT_SKU + cart.getSkuId()); BigDecimal propAmount = cart.getPropertys().stream().map(item -> { String propPrice = redisTemplate.opsForValue().get(CacheConstant.RL_PROP + item.getId()); return new BigDecimal(Objects.isNull(propPrice) ? "0" : propPrice); }).reduce(BigDecimal.ZERO, BigDecimal::add); // SKU 价格 + 属性价格 BigDecimal discountTotal = propAmount.add(new BigDecimal(discountPrice)); BigDecimal total = propAmount.add(new BigDecimal(price)); // 单价 x 数量 cart.setDiscountPrice(discountTotal); cart.setPrice(total); return cart; }).collect(Collectors.toList()); } / * 添加到购物车 * @param dto * @return */ @Override public Response<Boolean> addCart(Cart dto, Long userId) { try { // 用户已登录,查询登录状态的购物车 String key = CacheConstant.RL_CART + userId + ":" + dto.getShopId(); // 1. 组装购物车商品Key:商品Id + 排序后的属性Ids String pidsKey = dto.getPropertys().stream().map(ProductProperty::getId).sorted(Long::compareTo).map(Object::toString).collect(Collectors.joining("_")); String cartItemKey = dto.getProductId().toString() + ":" + pidsKey + ":" + dto.getSkuId().toString(); // 缓存购物车项id dto.setCartItemId(cartItemKey); // 2. 查询用户购物车 BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key); // 3. 判断购物车数据组是否已存在该商品 if (hashOps.hasKey(cartItemKey)) { // 购物车已存在该记录,更新数量 String cartJson = hashOps.get(cartItemKey).toString(); Cart cart = JSON.parseObject(cartJson, Cart.class); dto.setQuantity(cart.getQuantity() + dto.getQuantity()); } // 4. 将 SKU 价格写入 Redis // 售价 redisTemplate.opsForValue().set(CacheConstant.RL_SKU + dto.getSkuId(), dto.getSkuPrice().toString()); // 折扣价 redisTemplate.opsForValue().set(CacheConstant.RL_DISCOUNT_SKU + dto.getSkuId(), Objects.isNull(dto.getDiscountSkuPrice()) ? dto.getSkuPrice().toString() : dto.getDiscountSkuPrice().toString()); // 5. 将 属性 价格写入 Redis dto.getPropertys().forEach(item-> redisTemplate.opsForValue().set(CacheConstant.RL_PROP + item.getId().toString(), item.getPrice().toString()) ); // 5. 将购物车记录写入 Redis hashOps.put(cartItemKey, JSON.toJSONString(dto)); return ResponseUtils.success(Boolean.TRUE); } catch (Exception e){ e.printStackTrace(); } return ResponseUtils.error(1, "添加购物车失败"); } / * 更新购物车 * @param dto * @param userId * @return */ @Override public Response<Boolean> updateCart(Cart dto, Long userId) { try { // 判断购物车项id是否为空 if(StringUtils.isBlank(dto.getCartItemId())){ return ResponseUtils.error(1, "购物车项id为空"); } // 用户已登录,查询登录状态的购物车 String key = CacheConstant.RL_CART + userId + ":" + dto.getShopId(); // 获取hash操作对象 BoundHashOperations<String, Object, Object> hashOperations = this.redisTemplate.boundHashOps(key); // 商品Id + 排序后的属性Ids // String pidsKey = dto.getPropertys().stream().map(ProductProperty::getId).sorted(Long::compareTo).map(Object::toString).collect(Collectors.joining(":")); // String pidsKey = dto.getSkuId().toString(); // String cartItemKey = dto.getProductId().toString() + ":" + pidsKey; if(hashOperations.hasKey(dto.getCartItemId())){ // 获取购物车信息 String cartJson = hashOperations.get(dto.getCartItemId()).toString(); final long i = dto.getQuantity(); final boolean check = dto.getCheck(); dto = JSON.parseObject(cartJson, Cart.class); // 更新数量 dto.setQuantity(i); // 更新选择状态 dto.setCheck(check); // 写入redis hashOperations.put(dto.getCartItemId(), JSON.toJSONString(dto)); } return ResponseUtils.success(Boolean.TRUE); } catch (Exception e){ e.printStackTrace(); } return ResponseUtils.error(1, "更新购物车失败"); } / * 删除购物车商品 * @param cartIds * @param userId * @return */ @Override public Response<Boolean> deleteCart(List<String> cartIds, Long userId,Long shopId) { try { // 用户已登录,查询登录状态的购物车 String key = CacheConstant.RL_CART + userId + ":" + shopId; BoundHashOperations<String, Object, Object> hashOperations = this.redisTemplate.boundHashOps(key); cartIds.forEach(hashOperations::delete); return ResponseUtils.success(Boolean.TRUE); } catch (Exception e){ e.printStackTrace(); } return ResponseUtils.error(1, "删除购物车商品失败"); } / * 删除购物车所有商品 * @param userId * @return */ @Override public Response<Boolean> deleteAllCart(Long userId,Long shopId) { try { // 用户已登录,查询登录状态的购物车 String key = CacheConstant.RL_CART + userId + ":" + shopId; // 获取所有ids BoundHashOperations<String, Object, Object> hashOperations = this.redisTemplate.boundHashOps(key); List<String> ids = hashOperations.values().stream().map(userCartJson-> { Cart cart = JSON.parseObject(userCartJson.toString(), Cart.class); return cart.getCartItemId(); }).collect(Collectors.toList()); // 删除 hashOperations.delete(ids.toArray()); return ResponseUtils.success(Boolean.TRUE); } catch (Exception e){ e.printStackTrace(); } return ResponseUtils.error(1, "删除购物车商品失败"); } } 

ProductSkuServiceImpl.class:SKUService

@Service @AllArgsConstructor public class ProductSkuServiceImpl extends CurdServiceImpl<ProductSku, ProductSkuDao> implements ProductSkuService { private final StringRedisTemplate redisTemplate; @Override @Transactional public Boolean batchUpdate(Long productId, List<ProductSku> skuList) { Assert.notNull(productId, "更新的商品id不能为空"); this.batchLogicDelete(productId); skuList.forEach(productSku -> { productSku.setStatus(DataStatus.NORMAL.getStatus()); this.update(productSku); // 判断是否存在这个Key if(redisTemplate.hasKey(CacheConstant.RL_SKU + productSku.getId().toString())){ // 更新redis 中 sku 的价格(折扣价) redisTemplate.opsForValue().set(CacheConstant.RL_SKU + productSku.getId().toString(), productSku.getDiscountPrice().toString()); } }); return true; } } 

ProductPropertyServiceImpl.class:属性Service

@Service @AllArgsConstructor public class ProductPropertyServiceImpl extends CurdServiceImpl<ProductProperty, ProductPropertyDao> implements ProductPropertyService { private final StringRedisTemplate redisTemplate; @Override @Transactional public Boolean batchUpdate(Long productId, List<ProductProperty> properties) { Assert.notNull(productId, "更新的商品id不能为空"); this.batchLogicDelete(productId); properties.forEach(productProperty -> { productProperty.setStatus(DataStatus.NORMAL.getStatus()); this.update(productProperty); // 判断是否存在这个Key if(redisTemplate.hasKey(CacheConstant.RL_PROP + productProperty.getId().toString())){ // 更新redis 中 属性的价格(折扣价) redisTemplate.opsForValue().set(CacheConstant.RL_PROP + productProperty.getId().toString(), productProperty.getPrice().toString()); } }); return true; } } 

前端报文:

{ "discountPrice":"0.10", "images":"https://sh-cdn.xiaoyisz.com/prod/b66562d5-5903-4c-8-.jpg", "name":"拉尔夫滴滤咖啡", "price":"0.10", "productId":"", "propValues":"杯型:标准杯;糖:标准;奶油:标准;特殊要求:标准", "propertys":[ { "id":"", "status":1, "createTime":"2021-04-01T18:21:37", "updateTime":"2021-04-12T19:56:09", "productId":"", "propId":"", "propName":"杯型", "rule":1, "propValue":"标准杯", "price":0, "sequence":1, "groupSequence":1, "visible":1 }, { "id":"", "status":1, "createTime":"2021-04-01T18:21:37", "updateTime":"2021-04-12T19:56:09", "productId":"", "propId":"", "propName":"糖", "rule":2, "propValue":"标准", "price":0, "sequence":1, "groupSequence":1, "visible":1 }, { "id":"", "status":1, "createTime":"2021-04-01T18:21:37", "updateTime":"2021-04-12T19:56:09", "productId":"", "propId":"", "propName":"奶油", "rule":3, "propValue":"标准", "price":0, "sequence":1, "groupSequence":1, "visible":1 }, { "id":"", "status":1, "createTime":"2021-04-01T18:21:37", "updateTime":"2021-04-12T19:56:09", "productId":"", "propId":"", "propName":"特殊要求", "rule":4, "propValue":"标准", "price":0, "sequence":1, "groupSequence":1, "visible":1 } ], "quantity":1, "subtitle":"Ralph’s Coffee", "skuId":"", "canTakeout":1, "packingFee":0, "check":true } 

四、Redis持久化配置

这里采用的AOF持久化(即Append Only File持久化),AOF工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。

# 1. 编辑Redis配置文件 vim redis.conf # 2. 修改配置文件内容 appendonly yes #开启AOF持久化 appendfilename "appendonly.aof" #配置AOF持久化文件名 appendfsync everysec #配置AOF持久化策略 #always 表示只要缓冲区中数据发生更改,则就将该数据写入到aof文件中 #everysec 每秒写入把缓冲区中数据写入aof文件中(redis默认) #no 不做任何策略配置,将策略的配置交给操作系统,一般操作系统是等待缓冲区被占完之后,将数据写入aof文件中 

五、总结

到这里我们就实现了使用Redis实现购物车,大家可以根据自身业务结合使用,代码中基本都添加了注释。其实Redis实现购物车的难点就在于价格计算和价格变更,当商品的价格做了变更时需要根据业务赋予的Key找到对应Redis的数据进行修改,以便在查看购物车时在返回数据做价格计算时是一个正确的价格。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/108732.html

(0)
上一篇 2024-10-03 22:26
下一篇 2024-11-19 17:15

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信