网站首页 > 精选文章 / 正文
写在前面
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:334522354345。第二个要解决的问题就是,我们要存什么东西,这里有两种方案:
- 半持久化:直接存店铺id+商品Id作为购物车项Key,前端先查redis获取商品清单ids,然后根据ids异步查询具体数据。优点是如果商品价格改了,前端能够实时知道商品的价格。缺点适应是场景少,对数据库有一定的压力。如果商品只有SKU还好,如果商品还有一些其他属性时,当用户选择商品,选择商品属性时,后台是没有记录用户选了那些属性的。解决方案是把属性Id也加到Key中并且缓存起来,查询时和商品id作为参数查询具体的购物车项的明细。
- 不持久化:将购物车项的所有必要信息作为值存到Redis中,根据用户购物车Key就可以获取购物车商品集合。优点是性能高,速度快,不操作数据库。缺点是如果商品价格修改了,redis中的商品价格还是未改之前的,商品价格不能同步。也就是缓存中的信息和数据库中的信息会有不同步的情况。解决方案是添加购车时将SKU的id作为Key,价格作为值缓存起来,如果属性包含价格也需要将属性的Id作为Key,价格作为值缓存起来。如果SKU或者属性价格有修改则在修改方法中如果Redis中存在该SKU或属性则更新Redis中对应的数据,微服务架构可以通过消息队列更新Redis中的价格。查询购物车时商品价格根据SKU和属性价格计算出来返回给前端。
二、 购物车时序图
三、代码示例
BaseEntity.class:实体父类
@MappedSuperclass
public class BaseEntity<ID> implements Serializable {
private static final long serialVersionUID = 3087254371468631854L;
@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":"1377566813235318784",
"propValues":"杯型:标准杯;糖:标准;奶油:标准;特殊要求:标准",
"propertys":[
{
"id":"1377566813252096000",
"status":1,
"createTime":"2021-04-01T18:21:37",
"updateTime":"2021-04-12T19:56:09",
"productId":"1377566813235318784",
"propId":"1377469870433767424",
"propName":"杯型",
"rule":1,
"propValue":"标准杯",
"price":0,
"sequence":1,
"groupSequence":1,
"visible":1
},
{
"id":"1377566813264678912",
"status":1,
"createTime":"2021-04-01T18:21:37",
"updateTime":"2021-04-12T19:56:09",
"productId":"1377566813235318784",
"propId":"1377471375027081216",
"propName":"糖",
"rule":2,
"propValue":"标准",
"price":0,
"sequence":1,
"groupSequence":1,
"visible":1
},
{
"id":"1377566813285650432",
"status":1,
"createTime":"2021-04-01T18:21:37",
"updateTime":"2021-04-12T19:56:09",
"productId":"1377566813235318784",
"propId":"1377550487712305152",
"propName":"奶油",
"rule":3,
"propValue":"标准",
"price":0,
"sequence":1,
"groupSequence":1,
"visible":1
},
{
"id":"1377566813294039040",
"status":1,
"createTime":"2021-04-01T18:21:37",
"updateTime":"2021-04-12T19:56:09",
"productId":"1377566813235318784",
"propId":"1357232629233094656",
"propName":"特殊要求",
"rule":4,
"propValue":"标准",
"price":0,
"sequence":1,
"groupSequence":1,
"visible":1
}
],
"quantity":1,
"subtitle":"Ralph’s Coffee",
"skuId":"1377566813243707392",
"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的数据进行修改,以便在查看购物车时在返回数据做价格计算时是一个正确的价格。
Tags:hashoperations
猜你喜欢
- 2024-12-03 操作系统之文件管理,万字长文让你彻底弄懂
- 2024-12-03 数据异构优化:通过Redis缓存提升接口性能
- 2024-12-03 我从未见过的牛逼解说方式!Redis五种数据结构,看一遍就懂了
- 2024-12-03 .NET 9 的10 个必知亮点
- 2024-12-03 SpringBoot + Redis 实现点赞功能的缓存和定时持久化(附源码)
- 2024-12-03 分布式缓存-Redis 之 结合SpringBoot2.xx初始化配置redis 第一章
- 2024-12-03 接口也会累着!Spring Boot + Redis 实现接口限流和防刷
- 2024-12-03 SpringBoot中整合Redis(缓存篇)
- 2024-12-03 Springboot2.0学习12 使用Redis
- 2024-12-03 记一次因 Redis 使用不当导致应用卡死 bug 的排查及解决