持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【19】分布式下Session共享问题
session原理
分布式下session共享问题
问题1、不能跨不同域名共享
问题2、就算是相同域名,一个进程就对应一个session
相同域名下Session共享问题解决
session复制
优点 :web-server(Tomcat)原生支持,只需要修改配置 文件
缺点 :
- session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
- 任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server
- 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。
客户端存储
优点
- 服务器不需存储session,用户保存自己的 session 信息到 cookie 中。节省服务端资源
缺点
- 都是缺点,这只是一种思路。
- 具体如下:
- 每次http请求,携带用户在cookie中的完整信息, 浪费网络带宽
- session数据放在cookie中,cookie有长度限制 4 K,不能保存大量信息
- session数据放在cookie中,存在泄漏、篡改、 窃取等安全隐患
- 这种方式不会使用。
hash一致性
优点:
- 只需要改nginx配置,不需要修改应用代码
- 负载均衡,只要hash属性的值分布是均匀的,多台 web-server的负载是均衡的
- 可以支持web-server水平扩展(session同步法是不行的,受内存限制)
缺点:
- session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
- 如果web-server水平扩展,rehash 后session 重新分布, 也会有一部分用户路由不到正确的session
- 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用
统一存储
优点:
- 没有安全隐患
- 可以水平扩展,数据库/缓存水平切分即可
- web-server重启或者扩容都不会有 session 丢失
不足:
- 增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。
- redis获取数据比内存慢很多
- 上面缺点可以用SpringSession完美解决
Session共享问题解决—不同服务,子域session共享
jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案:放大Cookie作用域
手动设置Cookie,手动拿取Cookie
gulimall-auth:OAuth2Controller
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session,
HttpServletResponse httpServletResponse) throws Exception {
Map<String, String> headers = new HashMap<>();
Map<String, String> bodys = new HashMap<>();
bodys.put("client_id", "3276999101");
bodys.put("client_secret", "452bbefff4680ac8554b97799a8c12cb");
bodys.put("grant_type", "authorization_code");
bodys.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
bodys.put("code", code);
//1、根据code换取accessToken;
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, null, bodys);
if (response.getStatusLine().getStatusCode() == 200) {
//2、获取到了 socialUserAccessToken 进行处理
String json = EntityUtils.toString(response.getEntity());
SocialUserAccessToken socialUserAccessToken = JSON.parseObject(json, SocialUserAccessToken.class);
// String uid = socialUserAccessToken.getUid();
// 通过uid就知道当前是哪个社交用户
//1)、当前用户如果是第一次进网站,进行自动注册(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员账号)
R r = memberFeignService.socialLogin(socialUserAccessToken);
if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//登录或者注册这个社交用户
//2)、登录成功就跳回首页
/**
* 手动设置Cookie
*/
// 注意:
// Redis中不应该以loginUser作为用户的key,而是应该以UUID作为key来存储用户信息。
// 而且,我们在这儿实现是将用户转为JSON字符串存起来,为了后续方便取出用户这个对象信息,应该直接以对象的方式将用户保存起来,这样从Redis中取出来的数据对象直接就能使用
MemberRespVo loginUser = r.getData(new TypeReference<MemberRespVo>() {
});
stringRedisTemplate.opsForValue().set("loginUser", JSON.toJSONString(loginUser));
Cookie cookie = new Cookie("GULIMALL", "loginUser");
cookie.setDomain("gulimall.com");
cookie.setMaxAge(24 * 60 * 60);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
session.setAttribute("loginUser", loginUser);
return "redirect:http://gulimall.com";
}
}
return "redirect:http://auth.gulimall.com/login.html";
}
gulimall-product:IndexController
@GetMapping({"/", "/index.html"})
public String indexPage(Model model, HttpServletRequest httpServletRequest, HttpSession session) {
/**
* 手动获取Cookie
*/
Cookie[] cookies = httpServletRequest.getCookies();
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("GULIMALL")) {
String loginUserKey = cookie.getValue();
String json = stringRedisTemplate.opsForValue().get(loginUserKey);
MemberRespVo loginUser = JSON.parseObject(json, new TypeReference<MemberRespVo>(){});
session.setAttribute("loginUser", loginUser);
}
}
}
List<CategoryEntity> categorys = categoryService.listLevel1Categorys();
model.addAttribute("categorys", categorys);
return "index";
}
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage() {
if(stringRedisTemplate.opsForValue().get("loginUser") != null) return "redirect:http://gulimall.com";
return "login";
}
}
手动设置session,手动拿取session
一个用户,一个浏览器,对应一次会话,也就是一个session
Servlet的Session原理是,在内存中创建一个session对象,这个session对象有一个id,我们让浏览器保存一个叫jsessionid=该session对象id
的一个cookie,以后浏览器访问服务器都会带上这个cookie,那么自然也就可以获取到这个session对象,该session对象的attribute属性当然也就可以拿到。
那么如果我们要将session保存在redis中,同理,给redis中创建一个session对象,然后将这个session对象的key作为cooike的值让浏览器保存,浏览器以后访问我们的服务器,带上这个cookie,就可以拿到redis中的session对象,自然也可以拿到该session对象的attribute
在Java中,获取session对象的时候是看有没有一个叫jsessionid
的cookie,如果没有创建一个,如果有,拿出来让你用
那么,在redis中同理,看你有没有一个叫rsessionid
的cookie,如果没有,创建一个,如果有,拿出来让你用
代码实现:
auth服务:
@Component
public class GulimallAuthSessionInterceptor implements HandlerInterceptor {
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断浏览器有没有带来rsessionid这个Cookie
// rsessionid起任何名都行,我在这儿模仿jsessionid
boolean flag = false;
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AuthServerConstant.REDIS_SESSION_ID_KEY)) {
// 如果浏览器带来了rsessionid这个Cookie,代表Redis已经存着这个session对象了
THREAD_LOCAL.set(cookie.getValue());
flag = true;
break;
}
}
}
if (!flag) { // 如果没有带来rsessionid这个Cookie,给Redis中创建一个
String s = UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set(s, "");
THREAD_LOCAL.set(s);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
Cookie cookie = new Cookie(AuthServerConstant.REDIS_SESSION_ID_KEY, THREAD_LOCAL.get());
cookie.setDomain("gulimall.com");
response.addCookie(cookie); // 让浏览器保存这个rsessionid这个Cookie
}
}
@PostMapping("/login")
public String login(@Valid UserLoginVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session) {
if (result.hasErrors()) { //数据校验不通过
Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.gulimall.com/reg.html";
}
R login = memberFeignService.login(vo);
if (login.getCode() == BizCodeEnume.SUCCESS.getCode()) {
MemberRespVo loginUser = login.getData(new TypeReference<MemberRespVo>() {
});
/**
* 手动设置Session
*/
String rsessionId = GulimallAuthSessionInterceptor.THREAD_LOCAL.get();
String rsessionJson = stringRedisTemplate.opsForValue().get(rsessionId);
HashMap<String, String> rsession;
if (StringUtils.isEmpty(rsessionJson)) {
rsession = new HashMap<>();
} else {
rsession = JSON.parseObject(rsessionJson, HashMap.class);
}
rsession.put(AuthServerConstant.LOGIN_USER, JSON.toJSONString(loginUser));
stringRedisTemplate.opsForValue().set(rsessionId, JSON.toJSONString(rsession));
session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
return "redirect:http://gulimall.com";
}
return "redirect:http://auth.gulimall.com/login.html";
}
其他服务:
@GetMapping({"/", "/index.html"})
public String indexPage(Model model, HttpServletRequest httpServletRequest, HttpSession session) {
Cookie[] cookies = httpServletRequest.getCookies();
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AuthServerConstant.REDIS_SESSION_ID_KEY)) {
String rsessionId = cookie.getValue();
String rsessionJson = stringRedisTemplate.opsForValue().get(rsessionId);
HashMap<String, String> rsession;
if (StringUtils.isEmpty(rsessionJson)) {
rsession = new HashMap<>();
} else {
rsession = JSON.parseObject(rsessionJson, HashMap.class);
}
String s = rsession.get(AuthServerConstant.LOGIN_USER);
if (!StringUtils.isEmpty(s)) {
MemberRespVo loginUser = JSON.parseObject(s, new TypeReference<MemberRespVo>() {
});
session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
}
}
}
}
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
model.addAttribute(AuthServerConstant.LOGIN_USER, attribute);
System.out.println("thymeleaf可以直接获取session中的属性进行使用:" + attribute);
List<CategoryEntity> categorys = categoryService.listLevel1Categorys();
model.addAttribute("categorys", categorys);
return "index";
}
可以优化的地方:
cookie和session都有过期时间
看一看Java的sessionid是怎么生成的,那么我们redis中放置的sessionid可以以同样的方法生成
整合SpringSession
<!-- 1 整合SpringSession完成session共享问题 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
# 2 整合SpringSession
spring.session.store-type=redis
#server.servlet.session.timeout=60m
# 3 配置Redis的连接信息(之前配过)
#spring.redis.host=xxx
#spring.redis.port=xxx
#spring.redis.password=xxx
@EnableRedisHttpSession // 4 整合Redis作为session存储
// 5 使用SpringSession【跟以前使用session的写法一样】
//第一次使用session;命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间; gulimall.com auth.gulimall.com order.gulimall.com
//应该做到:发卡的时候(指定域名为父域名),那么,即使是子域系统发的卡,也能让父域直接使用。
// 1、默认发的令牌。session=xxxxxxx。作用域:当前域;(SpringSession默认没有解决子域session共享问题)
// 2、使用JSON的序列化方式来序列化对象数据到redis中
R r = memberFeignService.socialLogin(socialUserAccessToken);
if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//登录或者注册这个社交用户
//2)、登录成功就跳回首页
MemberRespVo loginUser = r.getData(new TypeReference<MemberRespVo>() {
});
session.setAttribute("loginUser", loginUser);
//6 配置序列化 + Cookie domain
// 解决子域session共享问题
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
// cookieSerializer.setCookieMaxAge(); // 默认是浏览器的session级别,关闭浏览器就失效
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
<!-- 7 给其他服务也整合好SpringSession后,直接取session中的数据即可 -->
<a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
// 登录页面
@GetMapping("/login.html")
public String loginPage(HttpSession session) {
// if(stringRedisTemplate.opsForValue().get("loginUser") != null) return "redirect:http://gulimall.com";
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute != null) return "redirect:http://gulimall.com";
return "login";
}
SpringSession核心原理
/**
* SpringSession 核心原理 装饰者模式;
* @EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
* 1、给容器中添加了一个组件
* SessionRepository = 》》》【RedisOperationsSessionRepository】==》redis操作session。session的增删改查封装类
* 2、SessionRepositoryFilter == 》Filter: session'存储过滤器;每个请求过来都必须经过filter
* 1、创建的时候,就自动从容器中获取到了SessionRepository;
* 2、原始的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
* 3、以后获取session。SessionRepositoryRequestWrapper.getSession();
* 4、wrappedRequest.getSession();===> SessionRepository 中获取到的。
*
自动延期;用户只要没有关闭浏览器,SpringSession会自动续期,当然,用户关闭了浏览器,redis中的数据也是有过期时间的。
*/
参考
雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!