购物车和购物项存入redis的结构用的是Hash结构,Hash值为cartKey ,表示购物车,其中的 map结构为: Map
@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
//拿到要操作的购物车信息
BoundHashOperations
//判断Redis是否有该商品的信息
String productRedisValue = (String) cartOps.get(skuId.toString());
//对象转json字符串存到redis中
String cartItemJson = JSON.toJSONString(cartItemVo);
cartOps.put(skuId.toString(), cartItemJson);
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; //线程池(最大线程数量-核心线程数量)的存活时间}
# 线程池核心线程数量
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
//远程查询当前要添加商品的信息操作
}, executor);
//开启第二个异步任务
CompletableFuture
//远程查询skuAttrValues组合信息
}, executor);
//等待所有的异步任务全部完成
CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();
(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;}
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("/**");}
}