本文探讨了 Session 的原理及其与 Cookie 和 Token 的区别。Session 通过服务器端存储 Session ID 来识别用户状态,涵盖创建、存储、维护和销毁的完整流程。与 Cookie 和 Token 比较,分析了它们在存储、安全性、生命周期和应用场景上的差异。此外,Session 在高并发场景下可能面临查找效率、代码复杂性、线程安全、网络传输和性能等问题。
为解决这些问题,提出使用 ThreadLocal 替代传统 Session。ThreadLocal 可以减少资源开销、提升代码质量、确保线程安全、减轻传输负担,并有效应对高并发挑战。文中还介绍了 ThreadLocal 的原理及内存泄漏的解决方法。
一、什么是 Session?
Session 是一种在服务器端保存用户状态信息的机制。每个用户在与服务器建立会话时,服务器会为其创建一个唯一的 Session ID,并将该 ID 存储在服务器端的会话存储中(例如内存、数据库或文件)。客户端通过 Cookie 或 URL 参数将 Session ID 发送到服务器,以便服务器可以识别用户并恢复其状态。
二、Session 的工作原理
Session 的工作原理通常包含以下几个步骤:
创建 Session:当用户第一次访问网站时,服务器会为该用户创建一个 Session,并生成一个唯一的标识符,称为 Session ID。
存储 Session ID:服务器会通过 Cookie 或 URL 参数将 Session ID 发送给用户的浏览器。
维护 Session:浏览器会在每次请求时,将 Session ID 发送给服务器,服务器根据这个 ID 找到对应的 Session,进而识别用户的状态。
销毁 Session:当用户注销、关闭浏览器或 Session 过期时,Session 将被销毁,服务器不再保存用户的状态信息。
三、Session 的使用和常用方法
- Session 作用域:拥有存储数据的空间,作用范围是一次会话有效,一次会话是使用同一浏览器发送的多次请求。一旦浏览器关闭,则结束会话。
- 可以将数据存入 Session 中,在一次会话的任意位置进行获取,可传递任何数据(基本数据类型、对象、集合、数组)。
- resquest.getSession():得到请求游览器(客户端)对应的 session。如果没有,那么就创建应该新的 session。如果有那么就返回对应的 session。
- setAttribute(String s, Object o):在 session 存放属性
- getAttribute(String s):从 session 中得到 s 所对应的属性
- removeAttribute(String s):从 session 中删除 s 对应的属性
- getId():得到 session 所对应的 id
- invalidate():使 session 立即无效
- setMaxInactiveInterval(int i):设置 session 最大的有效时间。
注意,这个有效时间是两次访问服务器所间隔的最大时间,如果超过最大的有效时间,那么这个 session 就失效了。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
HttpSession session = request.getSession();
Object token =session.getAttribute("token");
if (token == null) {
response.sendRedirect("/admin/toLogin");
return false;
}
return true;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
}
}
四、Session、Cookie 和 Token 区别?
1. Session
Session是一种在服务器端保存用户状态信息的机制。每个用户在与服务器建立会话时,服务器会为其创建一个唯一的 Session ID,并将该 ID 存储在服务器端的会话存储中(例如内存、数据库或文件)。客户端通过 Cookie 或 URL 参数将 Session ID 发送到服务器,以便服务器可以识别用户并恢复其状态。
- 存储位置:服务器端。
- 安全性:较高,因为敏感数据存储在服务器端。
- 生命周期:通常在用户关闭浏览器或会话超时后失效。
- 使用场景:适用于需要在服务器端保持用户会话状态的场景,例如购物车、用户登录状态等。
2. Cookie
Cookie是一种在客户端(通常是浏览器)存储数据的小文件。服务器通过 HTTP 响应头 Set-Cookie 将 Cookie 发送到客户端,客户端会在后续请求中自动包含这些 Cookie。Cookie 可以用于存储用户的会话信息、偏好设置等。
- 存储位置:客户端(浏览器)。
- 安全性:较低,容易被窃取和篡改。可以通过设置 HttpOnly 和 Secure 属性提高安全性。
- 生命周期:可以设置为会话 Cookie(浏览器关闭后失效)或持久 Cookie(设置过期时间)。
- 使用场景:适用于需要在客户端存储少量数据的场景,例如用户偏好设置、跟踪用户活动等。
3. Token
Token是一种用于身份验证的字符串,通常由服务器生成并发送给客户端。Token 常用于无状态的身份验证机制,如 JSON Web Token (JWT)。客户端在每次请求时将 Token 发送到服务器,服务器通过验证 Token 来识别用户身份。
- 存储位置:客户端(可以存储在 Cookie、LocalStorage 或 SessionStorage 中)。
- 安全性:较高,Token 通常包含签名和加密信息,可以防止篡改。JWT 中的签名可以验证 Token 的完整性和真实性。
- 生命周期:可以设置过期时间,通常需要定期刷新 Token(如使用 Refresh Token)。
- 使用场景:适用于分布式系统和微服务架构中无状态的身份验证,特别是需要跨域的场景。
总结 - Session:服务器端存储用户会话状态,通过 Session ID 识别用户。适用于需要在服务器端保持用户状态的场景。
- Cookie:客户端存储少量数据,可以用于会话管理和用户偏好设置。安全性较低,需要注意保护敏感信息。
- Token:客户端存储的身份验证字符串,常用于无状态的身份验证机制。适用于分布式系统和需要跨域的场景。
五、为什么不能依赖 Session 存储用户信息?
在项目设计和开发中,Session 是用于存储用户会话信息的重要机制。用户在登录后,通常会将用户信息存储在 Session 中,以便在后续的请求中可以访问到这些信息。
如果想在其它地方获取 session 中的用户信息,我们需要先获取 HttpServletRequest,再通过 request.getSession 得到 HttpSession。例如下代码:
public static User getSessionUser(HttpServletRequest request)
{
if(request.getSession().getAttribute( "sessionuser" ) != null)
{
return (User)request.getSession().getAttribute( "sessionuser" );
}
return null;
}
然而,直接通过 Session 来访问用户信息在高并发和大规模系统中会带来一些问题和挑战。
1. Session 查找的开销
Session 通常存储在服务器的内存中,尽管它提供了快速访问机制,但每次从 Session 中查找用户信息也会产生开销。
- 内存存储和查找:每次调用 request.getSession().getAttribute("sessionuser"),实际上是向服务器的内存或持久化存储查询信息,尤其在多个请求并发时,频繁访问 Session 会对性能产生影响。
- Session 存储机制:对于大规模分布式系统,Session 有可能存储在分布式缓存中(如 Redis、Memcached 等)。这就引入了网络请求的延迟,进一步加剧了性能问题。
2. 代码的可读性和维护性
直接在代码中获取 Session 中的信息,通常会使代码显得简单,但实际上这种方式的可维护性和可读性差。
- 重复代码:如你提到的代码示例,每次都需要调用 request.getSession() 和 request.getSession().getAttribute(),这种方式会导致代码重复、冗长,且当项目中有多个地方需要获取 Session 时,可能会导致维护困难。
- 封装性差:直接操作 Session 使得代码不具备良好的封装性。每个地方都直接依赖于 HttpServletRequest 和 Session,这违背了代码解耦的原则。
3. 线程安全问题
Session 在多线程环境下可能会导致线程安全问题。
- 并发访问:在高并发的环境中,多个请求可能同时访问或修改 Session 中的数据。如果 Session 对象不是线程安全的,就需要额外的同步机制来避免数据冲突或竞争。
- 同步开销:为了确保线程安全,可能需要在代码中加入同步锁等机制,这不仅增加了开发复杂性,也可能带来额外的性能开销。使用同步机制可能导致线程阻塞,影响性能。
4. 不必要的网络传输
在分布式系统中,Session 通常需要在不同的服务器之间同步,尤其是在负载均衡的场景下,用户的请求可能被分配到不同的服务器。
- Session 同步问题:如果使用的是基于内存的 Session 存储,当一个请求的 Session 数据不在当前处理请求的服务器上时,可能需要访问其他服务器的 Session。这种跨服务器的通信会引入网络延迟,增加网络传输的开销。
- 负载均衡影响:多个服务器共享 Session 时,需要实现 Session 复制或共享机制(如 sticky session、Redis 共享 Session 等)。这些机制通常会增加额外的网络传输成本。
5. 面对高并发场景
在高并发的场景下,Session 的频繁访问和更新可能会导致性能瓶颈,特别是在分布式环境下。
- Session 频繁读写:在高并发的环境下,多个请求可能会频繁地访问 Session。这不仅会增加服务器的内存负担,还可能导致网络传输延迟,进而影响整体系统性能。
- Session 集中存储压力:如果所有用户信息都存储在一个集中的 Session 存储中,随着用户量的增加,这种集中存储可能会成为瓶颈,特别是在高负载时,访问 Session 会变得缓慢,影响响应时间。
六、使用 ThreadLocal 替代 Session 存储用户信息
1、减少 session 查找的开销
session 一般都是存储在服务器端的,如果每次都要从 session 中查找用户信息,那都要去服务器的内存或存储系统去查找对应的 session 对象,这肯定会带来一定的性能开销。
那你用 TheadLocal 就直接把对象放在当前线程里面,想用直接在当前线程找,效率肯定就会高很多。
2、提高代码的可读性和维护性
直接在 session 里面拿用户信息,听着很直接很简单,实则需要调用多个层级才能找到,代码特别复杂且冗余,直接用 TheadLocal 拿用户信息肯定就更方便且直观。
3、线程安全
ThreadLocal 为每个线程提供了一个独立的变量副本,这意味着在多线程环境下,每个线程都可以安全地访问自己的用户信息,而不会与其他线程发生冲突。如果直接操作 Session,需要额外的同步机制来保证线程安全。
4、减少不必要的网络传输
当你的用户量特别多,你不先拿到 TheadLocal 里面,那 session 里面的用户信息会越来越多,Session 可能需要在多个服务器之间同步,这会增加网络传输的开销。
5、面对高并发场景
在高并发的应用场景下,频繁地访问 Session 可能会导致性能瓶颈。而 ThreadLocal 由于其线程局部性,可以提供更好的性能表现。
七、ThreadLocal 原理
ThreadLocal 的实现依赖于每个线程内部维护的一个 ThreadLocalMap 对象。每个线程都有自己的 ThreadLocalMap,而 ThreadLocalMap 中存储了所有 ThreadLocal 变量及其对应的值。
主要组成部分
- ThreadLocal 类:提供了 set()、get()、remove()等方法,用于操作线程局部变量。
- ThreadLocalMap 类:是 ThreadLocal 的内部静态类,用于存储 ThreadLocal 变量及其值。
- Thread 类:每个线程内部都有一个 ThreadLocalMap 实例。
工作机制
- 创建 ThreadLocal 变量:当创建一个 ThreadLocal 变量时,实际上并没有分配存储空间。
- 获取值 (get()方法):当调用 get()方法时,当前线程会通过自己的 ThreadLocalMap 获取 ThreadLocal 变量的值。如果不存在,则调用 initialValue()方法获取初始值。
- 设置值 (set()方法):当调用 set()方法时,当前线程会通过自己的 ThreadLocalMap 设置 ThreadLocal 变量的值。
- 删除值 (remove()方法):当调用 remove()方法时,当前线程会通过自己的 ThreadLocalMap 删除 ThreadLocal 变量的值。
使用 ThreadLocal 的方法
一般使用 ThreadLocal 的方法,就是建立一个 ThreadLocal 的工具类,在存储和使用用户信息时,能方便地调用。
/**
* @ Author:
* @ CreateTime: 2025-01-07
* @ Description: 登录上下文对象
* @ Version: 1.0
*/
public class ThreadLocal {
private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL
= new InheritableThreadLocal<>();
public static void set(String key, Object val) {
Map<String, Object> map = getThreadLocalMap();
map.put(key, val);
}
public static Object get(String key){
Map<String, Object> threadLocalMap = getThreadLocalMap();
return threadLocalMap.get(key);
}
public static String getLoginId(){
return (String) getThreadLocalMap().get("loginId");
}
public static void remove(){
THREAD_LOCAL.remove();
}
public static Map<String, Object> getThreadLocalMap() {
Map<String, Object> map = THREAD_LOCAL.get();
if (Objects.isNull(map)) {
map = new ConcurrentHashMap<>();
THREAD_LOCAL.set(map);
}
return map;
}
}
八、ThreadLocal 的内存泄漏问题
在使用 ThreadLocal 时,要注意 ThreadLocal 地内存泄漏问题。
ThreadLocal 的内存泄漏的原因
ThreadLocal 的内存泄漏问题主要源于 ThreadLocalMap 中使用的弱引用(WeakReference)机制和线程生命周期管理不当这两个原因。
- 弱引用机制:
ThreadLocalMap 使用 Entry 类来存储键值对,其中键是 ThreadLocal 对象的弱引用(WeakReference<ThreadLocal<?>>)
,值是实际存储的数据。当 ThreadLocal 对象被垃圾回收时,弱引用会被清除,但 Entry 中的值对象仍然存在,导致内存无法及时释放。 - 线程生命周期:
在一些长生命周期的线程(如线程池中的线程)中,如果不显式地清除 ThreadLocal 变量,ThreadLocalMap 中的值对象会一直存在,导致内存泄漏。线程池中的线程不会在任务完成后立即销毁,而是会被复用。如果 ThreadLocal 变量没有被显式清除,下一个使用该线程的任务可能会意外地访问到上一个任务遗留的数据。
如何避免 ThreadLocal 的内存泄漏?
最直接和有效的方法是在使用完 ThreadLocal 变量后,显式调用 remove()方法清除变量。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
String loginId = request.getHeader("loginId");
if (loginId != null && loginId != "") {
ThreadLocal.set("loginId", loginId);
}
return true;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
ThreadLocal.remove();
}
}
就业陪跑训练营学员投稿
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:思否面试群。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。