购物车服务-----技术点及亮点
大技术
使用Redis存储购物车和购物项(亮点1)
购物车和购物项存入redis的结构用的是Hash结构,Hash值为cartKey ,表示购物车,其中的 map结构为: Map<String skuId,String cartItem>,表示购物项
- 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<String skuId,String cartItem>表示用户购物车的里的购物项,购物项中skuId为商品id作为key,values作为购物项的详细信息
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//判断Redis是否有该商品的信息
String productRedisValue = (String) cartOps.get(skuId.toString());
//如果没有就添加数据
if (StringUtils.isEmpty(productRedisValue)) {
//2、添加新的商品到购物车(redis)
//封装购物项
CartItemVo cartItemVo = new CartItemVo();
//开启第一个异步任务
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
//1、远程查询当前要添加商品的信息
R productSkuInfo = productFeignService.getInfo(skuId);
SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
//数据赋值操作
cartItemVo.setSkuId(skuInfo.getSkuId());
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setCount(num);
}, executor);
//开启第二个异步任务
CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
//2、远程查询skuAttrValues组合信息
List<String> 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<String, Object, Object> 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();
}
//绑定要操作的哈希值,也就是cartKey
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
@Autowired
StringRedisTemplate redisTemplate;
//绑定要操作的哈希值,也就是cartKey
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> 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 {
@Bean
public 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
实际使用
@Override
public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
//拿到要操作的购物车信息
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
//判断Redis是否有该商品的信息
String productRedisValue = (String) cartOps.get(skuId.toString());
//如果没有就添加数据
if (StringUtils.isEmpty(productRedisValue)) {
//2、添加新的商品到购物车(redis)
//封装购物项
CartItemVo cartItemVo = new CartItemVo();
//开启第一个异步任务
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
//1、远程查询当前要添加商品的信息
R productSkuInfo = productFeignService.getInfo(skuId);
SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
//数据赋值操作
cartItemVo.setSkuId(skuInfo.getSkuId());
cartItemVo.setTitle(skuInfo.getSkuTitle());
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setCount(num);
}, executor);
//开启第二个异步任务
CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
//2、远程查询skuAttrValues组合信息
List<String> 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<String, Object, Object> 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();
}
//绑定要操作的哈希值,也就是cartKey
BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);
return operations;
}
@Autowired
private ThreadPoolExecutor executor; //线程池
//开启第一个异步任务
CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
//远程查询当前要添加商品的信息操作
}, executor);
//开启第二个异步任务
CompletableFuture<Void> 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<UserInfoTo> toThreadLocal = new ThreadLocal<>();
/***
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public 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-key
String 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
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//获取当前用户的值(已经经过拦截器了)
UserInfoTo userInfoTo = toThreadLocal.get();
//如果没有用过这个网站就新创建一个临时用户,把前面创建的cookie值赋值到这个cookie里
if (!userInfoTo.getTempUser()) {
//创建一个cookie
Cookie cookie = new Cookie(TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
//扩大作用域
cookie.setDomain("saodaimall.com");
//设置过期时间
cookie.setMaxAge(TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
@Override
public 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 {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}