分布式session(服务通信数据共享问题)


前言

现在大多数登录技术都使用的是JWT技术,去Redis当中进行验证。
而谷粒商城项目依旧使用的是Session验证,这里记录一下Session登录及一些常见问题


Session分布式问题的解决

Session原理

session存储在服务端,jsessionId存在客户端。jsessionid就是用来判断当前用户对应于哪个session。
事实上 jsessionid ==request.getSession().getId()
在这里插入图片描述
但是这就产生了一个大问题!!
如果我有多个服务器,那这次请求发A服务器(订单服务),保存了Session,下次发B服务器(库存服务),还要保存一遍??
显然不合适,于是我们想到可以使用Redis去统一存储Session,不就只要存一次就好了?
在这里插入图片描述

SpringSession 整合Redis

1.准备工作

引入依赖

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改配置

spring.session.store-type=redis  #session存储类型
server.servlet.session.timeout=30m #过期事件
spring.redis.host=192.168.56.10 #redis地址

添加注解

@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {

2.解决Session序列化问题、作用域问题

Session默认的是JDK序列化方式

每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。

通过导入RedisSerializer修改为json序列化

 @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

Session作用域
cookie对象的domain属性设置了cookie的作用域。domain本身以及domain的子域名可以访问到相关cookie。

在对cookie的domain进行设置时,不能讲domain指定为除当前域名或者其父域名之外的其他域名,即cookie无法跨域设置

一个有效的cookie的作用域为: domain本身以及domain下的所有子域名。

 @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }

整体设置为一个组件

@Configuration
public class GulimallSessionConfig {

    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}

把这个配置放到每个微服务下

3. Session的保存

@GetMapping({"/login.html","/","/index","/index.html"}) // auth
public String loginPage(HttpSession session){
    // 从会话从获取loginUser
    Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);// "loginUser";
    System.out.println("attribute:"+attribute);
    if(attribute == null){
        return "login";
    }
    System.out.println("已登陆过,重定向到首页");
    return "redirect:http://gulimall.com";
}


@PostMapping("/login") // auth
public String login(UserLoginVo userLoginVo,
                    RedirectAttributes redirectAttributes,
                    HttpSession session){
    // 远程登录
    R r = memberFeignService.login(userLoginVo);
    if(r.getCode() == 0){
        // 登录成功
        MemberRespVo respVo = r.getData("data", new TypeReference<MemberRespVo>() {});
        // 放入session  // key为loginUser
        session.setAttribute(AuthServerConstant.LOGIN_USER, respVo);//loginUser
        log.info("\n欢迎 [" + respVo.getUsername() + "] 登录");
        // 登录成功重定向到首页
        return "redirect:http://gulimall.com";
    }else {
        HashMap<String, String> error = new HashMap<>();
        // 获取错误信息
        error.put("msg", r.getData("msg",new TypeReference<String>(){}));
        redirectAttributes.addFlashAttribute("errors", error);
        return "redirect:http://auth.gulimall.com/login.html";
    }
}

拦截器获得用户信息

订单系统为例,必须需要用户登录,我们可以使用拦截器去判断用户是否登陆了

先注入拦截器HandlerInterceptor组件

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
// 加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查
	public static ThreadLocal<MemberRespVo> threadLocal = new ThreadLocal<>();

	@Override
	public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {

		String uri = request.getRequestURI();
		// 这个请求直接放行
		boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
		if(match){
			return true;
		}
		// 获取session
		HttpSession session = request.getSession();
		// 获取登录用户
		MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
		if(memberRespVo != null){
			threadLocal.set(memberRespVo);
			return true;
		}else{
			// 没登陆就去登录
			session.setAttribute("msg", AuthServerConstant.NOT_LOGIN);
			response.sendRedirect("http://auth.gulimall.com/login.html");
			return false;
		}
	}
}

在config类中实现WebMvcConfigurer接口.addInterceptor()方法添加拦截器

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**"); //所有路径的请求
    }
}

加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查

	//组件的拦截器中代码
 	public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
 	loginUser.set(attribute);
	
	//serviceImpl中的实现通过线程获取用户信息的代码
	MemberRespVo memberRespVo = LoginUserIntercepter.loginUser.get(); //通过过滤器拿到用户Id
	

使用实例

    @Autowired
    MemberFeignService memberFeignService;
    @Autowired
    CartFeignService cartFeignService;
    @Autowired
    ThreadPoolExecutor executor; //异步线程池
    @Autowired
    WmsFeignService wmsFeignService;
    @Autowired
    StringRedisTemplate redisTemplate;

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserIntercepter.loginUser.get(); //通过过滤器拿到用户Id
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();  // 拿到原请求的  RequestAttributes
        // 第一个异步任务
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes); // 每一个请求都共享原请求的 RequestAttributes
            //远程查询收货地址
            List<MemberReceiveAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);

        //第二个异步任务
        CompletableFuture<Void> getCartFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes); // 每一个请求都共享原请求的 RequestAttributes
            //获得购物车中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setOrderItems(items);
        }, executor).thenRunAsync(()->{
            // 判断库存
            List<OrderItemVo> items = confirmVo.getOrderItems();
            List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
            R hasStock = wmsFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = (List<SkuStockVo>) hasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if (data!=null){
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(map);
            }
        },executor);


        //查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);

        //其他数据自动计算

        CompletableFuture.allOf(getAddressFuture,getCartFuture).get(); //等待异步任务全部完成!
        //防重令牌

        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);//前缀 , 令牌 ,
        confirmVo.setOrderToken(token);
        return confirmVo;
    }

看的很懵对不对,这里有三个重要点

RequestContextHolder

RequestContextHolder顾名思义,持有上下文的Request容器。
我们先看使用场景(GuliFeignConfig(为Feign请求保持Request的配置类)当中)

   ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (attributes!=null){  // 防止未登录状态下报空指针异常
                    HttpServletRequest request = attributes.getRequest(); // 拿到上文的请求
                    if (request != null){  //防止空指针异常
                        //同步请求数据  cookie
                        String cookie = request.getHeader("cookie");
                        //给新请求(调用Feign服务的请求一个cookie)
                        requestTemplate.header("Cookie",cookie);
                    }
                }
  • 正常来说在service层是没有request和response的,然而直接从controlller传过来的话解决方法太粗暴。解决方法是SpringMVC提供的RequestContextHolder
  • 用线程池执行任务时非主线程是没有请求数据的,可以通过该方法设置线程中的request数据,原理还是用的threadlocal(此时就要注意new thread就获取不到请求了)

在spring mvc中,为了随时都能取到当前请求的request对象,可以通过RequestContextHolder的静态方法getRequestAttributes()获取Request相关的变量,如request,response等

//两个方法在没有使用JSF的项目中是没有区别的
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
//                                    RequestContextHolder.getRequestAttributes();
//从session里面获取对应的值
String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION);
// 拿到请求
HttpServletRequest  request  = ((ServletRequestAttributes)requestAttributes).getRequest();
// 响应
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

rpc丢失用户信息

众所周知,feign远程调用的请求头中没有含有JSESSIONIDcookie,所以也就不能得到服务端的session数据,也就没有用户数据

怎么解决呢?
在这里插入图片描述
feign的调用过程中,会使用容器中的RequestInterceptorRequestTemplate进行处理,因此我们可以通过向容器中导入定制的RequestInterceptor为请求加上cookie

public class GuliFeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1. 使用RequestContextHolder拿到老请求的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (requestAttributes != null) {
                    HttpServletRequest request = requestAttributes.getRequest();
                    if (request != null) {
                        //2. 将老请求得到cookie信息放到feign请求上
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
    }
}
//注意:上面在封装cookie的时候要拿到原来请求的`cookie`,设置到新的请求中

线程异步丢失上下文问题

在讨论第一个问题的时候,有说明: RequestContextHolder为SpingMVC中共享request数据的上下文,底层由ThreadLocal实现,也就是说该请求只对当前访问线程有效
因此上面的实例引用了多线程,肯定无法共享cookie了
在这里插入图片描述
解决: 我们需要在开启异步时候就将老请求的RequestContextHolder的数据设置进去

// 从主线程获取用户数据 放到局部变量中
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
    // 把旧RequestAttributes放到新线程的RequestContextHolder中
    RequestContextHolder.setRequestAttributes(attributes);
    // 远程查询所有的收获地址列表
    List<MemberAddressVo> address;
    try {
        address = memberFeignService.getAddress(MemberRespVo.getId());

总结

这些就是基本的使用session进行数据共享中知识点
苦厄难夺凌云志 不死终有出头日

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值