购物车服务-----技术点及亮点
创始人
2024-03-23 16:13:21
0

大技术

使用Redis存储购物车和购物项(亮点1)

购物车和购物项存入redis的结构用的是Hash结构,Hash值为cartKey ,表示购物车,其中的 map结构为: Map,表示购物项

  • cartKey表示格式为saodaimall:cart:key,表示购物车,其中saodaimall:cart:key的saodaimall:cart:是一个固定前缀,key值有两种,如果登录了就是用户id(saodaimall:cart:1),没登录就是名为user-key的cookie的值(例如saodaimall:cart:a191459a-0f24-4e69-8dc7-a3f81de96202)
  • Map表示用户购物车的里的购物项,购物项中skuId为商品id作为key,values作为购物项的详细信息
 @Overridepublic CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {//拿到要操作的购物车信息BoundHashOperations cartOps = getCartOps();//判断Redis是否有该商品的信息String productRedisValue = (String) cartOps.get(skuId.toString());//如果没有就添加数据if (StringUtils.isEmpty(productRedisValue)) {//2、添加新的商品到购物车(redis)//封装购物项CartItemVo cartItemVo = new CartItemVo();//开启第一个异步任务CompletableFuture getSkuInfoFuture = CompletableFuture.runAsync(() -> {//1、远程查询当前要添加商品的信息R productSkuInfo = productFeignService.getInfo(skuId);SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference() {});//数据赋值操作cartItemVo.setSkuId(skuInfo.getSkuId());cartItemVo.setTitle(skuInfo.getSkuTitle());cartItemVo.setImage(skuInfo.getSkuDefaultImg());cartItemVo.setPrice(skuInfo.getPrice());cartItemVo.setCount(num);}, executor);//开启第二个异步任务CompletableFuture getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {//2、远程查询skuAttrValues组合信息List skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);cartItemVo.setSkuAttrValues(skuSaleAttrValues);}, executor);//等待所有的异步任务全部完成CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();String cartItemJson = JSON.toJSONString(cartItemVo);cartOps.put(skuId.toString(), cartItemJson);return cartItemVo;} else {//购物车有此商品,修改数量即可CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);cartItemVo.setCount(cartItemVo.getCount() + num);//修改redis的数据String cartItemJson = JSON.toJSONString(cartItemVo);cartOps.put(skuId.toString(),cartItemJson);return cartItemVo;}}/*** 获取到我们要操作的购物车*/private BoundHashOperations getCartOps() {//先从拦截器中得到当前用户信息UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();//cartKey是存在redis的key值String cartKey = "";if (userInfoTo.getUserId() != null) {//用户登录了就用用户的id号cartKey = CART_PREFIX + userInfoTo.getUserId();} else {//没登录就用浏览器中名为user-key的cookie的值cartKey = CART_PREFIX + userInfoTo.getUserKey();}//绑定要操作的哈希值,也就是cartKeyBoundHashOperations operations = redisTemplate.boundHashOps(cartKey);return operations;}

@Autowired

StringRedisTemplate redisTemplate;

//绑定要操作的哈希值,也就是cartKey

BoundHashOperations operations = redisTemplate.boundHashOps(cartKey);

//拿到要操作的购物车信息

BoundHashOperations cartOps = getCartOps();

//判断Redis是否有该商品的信息

String productRedisValue = (String) cartOps.get(skuId.toString());

//对象转json字符串存到redis中

String cartItemJson = JSON.toJSONString(cartItemVo);

cartOps.put(skuId.toString(), cartItemJson);

线程池来实现异步任务(亮点2)

在config包里配置线程池的配置类

package com.saodai.saodaimall.order.config;import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** 线程池配置类**/@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {return new ThreadPoolExecutor(//线程池核心线程数量pool.getCoreSize(),//线程池最大线程数量pool.getMaxSize(),//线程池(最大线程数量-核心线程数量)的存活时间pool.getKeepAliveTime(),//存活时间单位TimeUnit.SECONDS,//线程堵塞队列new LinkedBlockingDeque<>(100000),//默认线程工厂Executors.defaultThreadFactory(),//拒绝策略new ThreadPoolExecutor.AbortPolicy());}}
package com.saodai.saodaimall.order.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;/***线程池核心配置(方便在application.properties中进行配置)**/@ConfigurationProperties(prefix = "saodaimall.thread")
@Data
public class ThreadPoolConfigProperties {private Integer coreSize;  //线程池核心线程数量private Integer maxSize;   //线程池最大线程数量private Integer keepAliveTime;   //线程池(最大线程数量-核心线程数量)的存活时间}

在application.xml配置文件中配置线程池核心的配置项

# 线程池核心线程数量
saodaimall.thread.coreSize=20
#线程池最大线程数量
saodaimall.thread.maxSize=200
#线程池(最大线程数量-核心线程数量)的存活时间
saodaimall.thread.keepAliveTime=10

实际使用

 @Overridepublic CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {//拿到要操作的购物车信息BoundHashOperations cartOps = getCartOps();//判断Redis是否有该商品的信息String productRedisValue = (String) cartOps.get(skuId.toString());//如果没有就添加数据if (StringUtils.isEmpty(productRedisValue)) {//2、添加新的商品到购物车(redis)//封装购物项CartItemVo cartItemVo = new CartItemVo();//开启第一个异步任务CompletableFuture getSkuInfoFuture = CompletableFuture.runAsync(() -> {//1、远程查询当前要添加商品的信息R productSkuInfo = productFeignService.getInfo(skuId);SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference() {});//数据赋值操作cartItemVo.setSkuId(skuInfo.getSkuId());cartItemVo.setTitle(skuInfo.getSkuTitle());cartItemVo.setImage(skuInfo.getSkuDefaultImg());cartItemVo.setPrice(skuInfo.getPrice());cartItemVo.setCount(num);}, executor);//开启第二个异步任务CompletableFuture getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {//2、远程查询skuAttrValues组合信息List skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);cartItemVo.setSkuAttrValues(skuSaleAttrValues);}, executor);//等待所有的异步任务全部完成CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();String cartItemJson = JSON.toJSONString(cartItemVo);cartOps.put(skuId.toString(), cartItemJson);return cartItemVo;} else {//购物车有此商品,修改数量即可CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);cartItemVo.setCount(cartItemVo.getCount() + num);//修改redis的数据String cartItemJson = JSON.toJSONString(cartItemVo);cartOps.put(skuId.toString(),cartItemJson);return cartItemVo;}}/*** 获取到我们要操作的购物车*/private BoundHashOperations getCartOps() {//先从拦截器中得到当前用户信息UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();//cartKey是存在redis的key值String cartKey = "";if (userInfoTo.getUserId() != null) {//用户登录了就用用户的id号cartKey = CART_PREFIX + userInfoTo.getUserId();} else {//没登录就用浏览器中名为user-key的cookie的值cartKey = CART_PREFIX + userInfoTo.getUserKey();}//绑定要操作的哈希值,也就是cartKeyBoundHashOperations operations = redisTemplate.boundHashOps(cartKey);return operations;}

@Autowired

private ThreadPoolExecutor executor; //线程池

//开启第一个异步任务

CompletableFuture getSkuInfoFuture = CompletableFuture.runAsync(() -> {

//远程查询当前要添加商品的信息操作

}, executor);

//开启第二个异步任务

CompletableFuture getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {

//远程查询skuAttrValues组合信息

}, executor);

//等待所有的异步任务全部完成

CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();

小技术

设置登录拦截器

在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求:(封装UserInfoTo对象)

(1)获得当前登录用户的信息,用户登录了就设置用户id为userInfoTo的id(通过springsession实现了各个服务直接session共享)

(2)判断用户是不是第一次使用本网站(如果之前用过就会有一个浏览器名为user-key的cookie的值),用过就把cookie的值封装到UserInfoTo对象

(3)没有用过这个网站就需要创建一个新的cookie值(临时用户)

(4)把封装好的userInfoTo对象放到ThreadLocal中

注意:用户登录和有没有用过这个网站是两回事,因为可能是第一次登录,那就可以用过也可能没有用过这个网站

补充:

ThreadLocal 叫做本地线程变量,ThreadLocal是解决线程安全问题,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。为什么能够解决变量并发访问的冲突问题呢?因为一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等

理解:这里把用户登录信息(对象)放到ThreadLocal中也是围栏解决线程安全问题,这样每个服务都会有登录信息对象的拷贝,那么用户在用户服务中修改的这个登录信息不会影响到其他服务的登录信息,其他服务都是不同线程的登录信息的一个拷贝

业务执行之后,分配临时用户来浏览器保存

(1)从ThreadLocal中获取当前用户的值(已经经过拦截器了)

(2)如果没有临时用户一定保存一个临时用户(也就是重新创建一个新的名为user-key的cookie)

package com.saodai.saodaimall.cart.interceptor;import com.saodai.common.vo.MemberResponseVo;
import com.saodai.saodaimall.cart.to.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;import static com.saodai.common.constant.AuthServerConstant.LOGIN_USER;
import static com.saodai.common.constant.CartConstant.TEMP_USER_COOKIE_NAME;
import static com.saodai.common.constant.CartConstant.TEMP_USER_COOKIE_TIMEOUT;/***  在执行目标方法之前,判断用户的登录状态.并封装传递给controller目标请求**/public class CartInterceptor implements HandlerInterceptor {public static ThreadLocal toThreadLocal = new ThreadLocal<>();/**** 目标方法执行之前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//拦截器判断用户登录状态的封装类UserInfoTo userInfoTo = new UserInfoTo();HttpSession session = request.getSession();//获得当前登录用户的信息MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(LOGIN_USER);if (memberResponseVo != null) {//表示用户登录了就设置用户id为userInfoTo的id(用于后面做为redis的部分key值)userInfoTo.setUserId(memberResponseVo.getId());}//判断用户是不是第一次使用本网站(如果之前用过就会有一个浏览器名为user-key的cookie的值)Cookie[] cookies = request.getCookies();if (cookies != null && cookies.length > 0) {for (Cookie cookie : cookies) {//user-keyString name = cookie.getName();if (name.equals(TEMP_USER_COOKIE_NAME)) {//浏览器名为user-key的cookie的值userInfoTo.setUserKey(cookie.getValue());//标记为已是临时用户userInfoTo.setTempUser(true);}}}//没有用过这个网站就需要创建一个新的cookie值(临时用户)if (StringUtils.isEmpty(userInfoTo.getUserKey())) {String uuid = UUID.randomUUID().toString();userInfoTo.setUserKey(uuid);}//目标方法执行之前toThreadLocal.set(userInfoTo);return true;}/*** 业务执行之后,分配临时用户来浏览器保存* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {//获取当前用户的值(已经经过拦截器了)UserInfoTo userInfoTo = toThreadLocal.get();//如果没有用过这个网站就新创建一个临时用户,把前面创建的cookie值赋值到这个cookie里if (!userInfoTo.getTempUser()) {//创建一个cookieCookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());//扩大作用域cookie.setDomain("saodaimall.com");//设置过期时间cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);response.addCookie(cookie);}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}
package com.saodai.saodaimall.cart.to;import lombok.Data;/***拦截器判断用户登录状态的封装类**/@Data
public class UserInfoTo {/*** 已经登录用户的id*/private Long userId;/*** 浏览器名为user-key的cookie的值*/private String userKey;/*** 是否临时用户*/private Boolean tempUser = false;}
package com.saodai.common.vo;import lombok.Data;
import lombok.ToString;import java.io.Serializable;
import java.util.Date;/***会员信息**/@ToString
@Data
public class MemberResponseVo implements Serializable {private static final long serialVersionUID = 5573669251256409786L;private Long id;/*** 会员等级id*/private Long levelId;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 昵称*/private String nickname;/*** 手机号码*/private String mobile;/*** 邮箱*/private String email;/*** 头像*/private String header;/*** 性别*/private Integer gender;/*** 生日*/private Date birth;/*** 所在城市*/private String city;/*** 职业*/private String job;/*** 个性签名*/private String sign;/*** 用户来源*/private Integer sourceType;/*** 积分*/private Integer integration;/*** 成长值*/private Integer growth;/*** 启用状态*/private Integer status;/*** 注册时间*/private Date createTime;/*** 社交登录用户的ID*/private String socialId;/*** 社交登录用户的名称*/private String socialName;/*** 社交登录用户的自我介绍*/private String socialBio;}

在config包中配置注册刚才的拦截器

package com.saodai.saodaimall.cart.config;import com.saodai.saodaimall.cart.interceptor.CartInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");}
}

相关内容

热门资讯

原创 打... 打完乌克兰就收手,俄方拍着胸脯承诺北约和欧盟,可以立字据为证。 (俄方向北约承诺,打完乌克兰就收手...
政策纾困与转型升级并举 中国乳... 新华社北京12月27日电 题:政策纾困与转型升级并举 中国乳业迎来破局窗口期 新华社记者谢希瑶 乳制...
男子将女子约至酒店后杀害,女子... 12月27日,南都N视频记者从山东省聊城市中级人民法院获悉,山东男子董某与女子陶某曾是恋人关系,后分...
杨伟民:刺激消费政策应该逐步转... 12月27日,中国财富管理50人论坛2025年会在京举行,第十三届全国政协经济委员会副主任杨伟民在会...
从KS直播异常事件切入,湖南芙... 12月22日晚,针对网络平台直播异常引发的社会关注事件,湖南芙蓉律师事务所围绕“黑灰产攻击、平台责任...
出行观 | 智驾出关“水土不服... (文/观察者网 高莘)据香港《南华早报》12月26日报道,香港有关部门将要调查一名违反“粤车南下”政...
全国人大常委会关于《中华人民共... 全国人民代表大会常务委员会关于 《中华人民共和国刑事诉讼法》 第二百九十二条的解释 (2025年12...
渊亭信息科技申请基于检索增强生... 国家知识产权局信息显示,厦门渊亭信息科技有限公司申请一项名为“基于检索增强生成的智能政策问答方法、系...
政策纾困与转型升级并举,中国乳... 乳制品行业是一二三产业深度融合的重要行业。近日,商务部一则公告引发外界对于这一行业发展形势的关注。 ...
重庆荣豪律师事务所:医疗纠纷处... 推荐指数:★★★★★ 在医疗纠纷频发的当下,如何高效、专业地处理医疗纠纷成为众多患者及其家属、医疗机...