Tomcat-session的实现:线程安全与管理

getWarnOnSessionAttributeFilterFailure())) {
Integer count = (Integer) ois.readObject();
int n = count.intValue();
if (log.isDebugEnabled())
log.debug(“Loading " + n + " persisted sessions”);
for (int i = 0; i < n; i++) {
StandardSession session = getNewSession();
session.readObjectData(ois);
session.setManager(this);
sessions.put(session.getIdInternal(), session);
session.activate();
if (!session.isValidInternal()) {
// If session is already invalid,
// expire session to prevent memory leak.
// 主动调用 expire
session.setValid(true);
session.expire();
}
sessionCounter++;
}
} finally {
// Delete the persistent storage file
if (file.exists()) {
file.delete();
}
}
}
} catch (FileNotFoundException e) {
if (log.isDebugEnabled()) {
log.debug(“No persisted data file found”);
}
return;
}

if (log.isDebugEnabled()) {
log.debug(“Finish: Loading persisted sessions”);
}
}
// stopInternal() 事件到达时清理 sessions
/**

  • Save any currently active sessions in the appropriate persistence
  • mechanism, if any. If persistence is not supported, this method
  • returns without doing anything.
  • @exception IOException if an input/output error occurs
    */
    protected void doUnload() throws IOException {

if (log.isDebugEnabled())
log.debug(sm.getString(“standardManager.unloading.debug”));

if (sessions.isEmpty()) {
log.debug(sm.getString(“standardManager.unloading.nosessions”));
return; // nothing to do
}

// Open an output stream to the specified pathname, if any
File file = file();
if (file == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug(sm.getString(“standardManager.unloading”, pathname));
}

// Keep a note of sessions that are expired
ArrayList list = new ArrayList<>();

try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream oos = new ObjectOutputStream(bos)) {

synchronized (sessions) {
if (log.isDebugEnabled()) {
log.debug(“Unloading " + sessions.size() + " sessions”);
}
// Write the number of active sessions, followed by the details
oos.writeObject(Integer.valueOf(sessions.size()));
for (Session s : sessions.values()) {
StandardSession session = (StandardSession) s;
list.add(session);
session.passivate();
session.writeObjectData(oos);
}
}
}

// Expire all the sessions we just wrote
// 将所有session失效,实际上应用即将关闭,失不失效的应该也无所谓了
if (log.isDebugEnabled()) {
log.debug(“Expiring " + list.size() + " persisted sessions”);
}
for (StandardSession session : list) {
try {
session.expire(false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
session.recycle();
}
}

if (log.isDebugEnabled()) {
log.debug(“Unloading complete”);
}
}

接下来我们看下具体如何清理过期的会话?实际应该就是一个remove的事。

// org.apache.catalina.session.StandardSession#isValid
/**

  • Return the isValid flag for this session.
    */
    @Override
    public boolean isValid() {

if (!this.isValid) {
return false;
}

if (this.expiring) {
return true;
}

if (ACTIVITY_CHECK && accessCount.get() > 0) {
return true;
}
// 超过有效期,主动触发清理
if (maxInactiveInterval > 0) {
int timeIdle = (int) (getIdleTimeInternal() / 1000L);
if (timeIdle >= maxInactiveInterval) {
expire(true);
}
}

return this.isValid;
}

// org.apache.catalina.session.StandardSession#expire(boolean)
/**

  • Perform the internal processing required to invalidate this session,
  • without triggering an exception if the session has already expired.
  • @param notify Should we notify listeners about the demise of
  • this session?
    */
    public void expire(boolean notify) {

// Check to see if session has already been invalidated.
// Do not check expiring at this point as expire should not return until
// isValid is false
if (!isValid)
return;
// 上锁保证线程安全
synchronized (this) {
// Check again, now we are inside the sync so this code only runs once
// Double check locking - isValid needs to be volatile
// The check of expiring is to ensure that an infinite loop is not
// entered as per bug 56339
if (expiring || !isValid)
return;

if (manager == null)
return;

// Mark this session as “being expired”
expiring = true;

// Notify interested application event listeners
// FIXME - Assumes we call listeners in reverse order
Context context = manager.getContext();

// The call to expire() may not have been triggered by the webapp.
// Make sure the webapp’s class loader is set when calling the
// listeners
if (notify) {
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
Object listeners[] = context.getApplicationLifecycleListeners();
if (listeners != null && listeners.length > 0) {
HttpSessionEvent event =
new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
int j = (listeners.length - 1) - i;
if (!(listeners[j] instanceof HttpSessionListener))
continue;
HttpSessionListener listener =
(HttpSessionListener) listeners[j];
try {
context.fireContainerEvent(“beforeSessionDestroyed”,
listener);
listener.sessionDestroyed(event);
context.fireContainerEvent(“afterSessionDestroyed”,
listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
context.fireContainerEvent(
“afterSessionDestroyed”, listener);
} catch (Exception e) {
// Ignore
}
manager.getContext().getLogger().error
(sm.getString(“standardSession.sessionEvent”), t);
}
}
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}

if (ACTIVITY_CHECK) {
accessCount.set(0);
}

// Remove this session from our manager’s active sessions
// 从ManagerBase 中删除
manager.remove(this, true);

// Notify interested session event listeners
if (notify) {
fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
}

// Call the logout method
if (principal instanceof TomcatPrincipal) {
TomcatPrincipal gp = (TomcatPrincipal) principal;
try {
gp.logout();
} catch (Exception e) {
manager.getContext().getLogger().error(
sm.getString(“standardSession.logoutfail”),
e);
}
}

// We have completed expire of this session
setValid(false);
expiring = false;

// Unbind any objects associated with this session
String keys[] = keys();
ClassLoader oldContextClassLoader = null;
try {
oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
for (int i = 0; i < keys.length; i++) {
removeAttributeInternal(keys[i], notify);
}
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
}
}

}

// org.apache.catalina.session.ManagerBase#remove(org.apache.catalina.Session, boolean)
@Override
public void remove(Session session, boolean update) {
// If the session has expired - as opposed to just being removed from
// the manager because it is being persisted - update the expired stats
if (update) {
long timeNow = System.currentTimeMillis();
int timeAlive =
(int) (timeNow - session.getCreationTimeInternal())/1000;
updateSessionMaxAliveTime(timeAlive);
expiredSessions.incrementAndGet();
SessionTiming timing = new SessionTiming(timeNow, timeAlive);
synchronized (sessionExpirationTiming) {
sessionExpirationTiming.add(timing);
sessionExpirationTiming.poll();
}
}
// 从sessions中移除session
if (session.getIdInternal() != null) {
sessions.remove(session.getIdInternal());
}
}

清理工作的核心任务没猜错,还是进行remove对应的session, 但作为框架必然会设置很多的扩展点,为各监听器接入的机会。这些点的设计,直接关系到整个功能的好坏了。

4. session如何保证线程安全?

实际是废话,前面已经明显看出,其使用一个 ConcurrentHashMap 作为session的管理容器,而ConcurrentHashMap本身就是线程安全的,自然也就保证了线程安全了。

不过需要注意的是,上面的线程安全是指的不同客户端间的数据是互不影响的。然而对于同一个客户端的重复请求,以上实现并未处理,即可能会生成一次session,也可能生成n次session,不过实际影响不大,因为客户端的状态与服务端的状态都是一致的。

5. 使用持久化方案的session管理实现

默认情况使用内存作为session管理工具,一是方便,二是速度相当快。但是最大的缺点是,其无法实现持久化,即可能停机后信息就丢失了(虽然上面有在停机时做了持久化操作,但仍然是不可靠的)。

所以就有了与之相对的存储方案了:Persistent,它有一个基类 PersistentManagerBase 继承了 ManagerBase,做了些特别的实现:

// 1. session的添加
// 复用 ManagerBase

// 2. session的查找
// org.apache.catalina.session.PersistentManagerBase#findSession
/**

  • {@inheritDoc}
  • This method checks the persistence store if persistence is enabled,
  • otherwise just uses the functionality from ManagerBase.
    */
    @Override
    public Session findSession(String id) throws IOException {
    // 复用ManagerBase, 获取Session实例
    Session session = super.findSession(id);
    // OK, at this point, we’re not sure if another thread is trying to
    // remove the session or not so the only way around this is to lock it
    // (or attempt to) and then try to get it by this session id again. If
    // the other code ran swapOut, then we should get a null back during
    // this run, and if not, we lock it out so we can access the session
    // safely.
    if(session != null) {
    synchronized(session){
    session = super.findSession(session.getIdInternal());
    if(session != null){
    // To keep any external calling code from messing up the
    // concurrency.
    session.access();
    session.endAccess();
    }
    }
    }
    if (session != null)
    return session;

// See if the Session is in the Store
// 如果内存中找不到会话信息,从存储中查找,这是主要的区别
session = swapIn(id);
return session;
}
// org.apache.catalina.session.PersistentManagerBase#swapIn
/**

  • Look for a session in the Store and, if found, restore
  • it in the Manager’s list of active sessions if appropriate.
  • The session will be removed from the Store after swapping
  • in, but will not be added to the active session list if it
  • is invalid or past its expiration.
  • @param id The id of the session that should be swapped in
  • @return restored session, or {@code null}, if none is found
  • @throws IOException an IO error occurred
    */
    protected Session swapIn(String id) throws IOException {

if (store == null)
return null;

Object swapInLock = null;

/*

  • The purpose of this sync and these locks is to make sure that a
  • session is only loaded once. It doesn’t matter if the lock is removed
  • and then another thread enters this method and tries to load the same
  • session. That thread will re-create a swapIn lock for that session,
  • quickly find that the session is already in sessions, use it and
  • carry on.
    */
    // 额,总之就是有点复杂
    synchronized (this) {
    swapInLock = sessionSwapInLocks.get(id);
    if (swapInLock == null) {
    swapInLock = new Object();
    sessionSwapInLocks.put(id, swapInLock);
    }
    }

Session session = null;

synchronized (swapInLock) {
// First check to see if another thread has loaded the session into
// the manager
session = sessions.get(id);

if (session == null) {
Session currentSwapInSession = sessionToSwapIn.get();
try {
if (currentSwapInSession == null || !id.equals(currentSwapInSession.getId())) {
// 从存储中查找session
session = loadSessionFromStore(id);
sessionToSwapIn.set(session);

if (session != null && !session.isValid()) {
log.error(sm.getString(“persistentManager.swapInInvalid”, id));
session.expire();
removeSession(id);
session = null;
}
// 重新加入到内存 sessions 中
if (session != null) {
reactivateLoadedSession(id, session);
}
}
} finally {
sessionToSwapIn.remove();
}
}
}

// Make sure the lock is removed
synchronized (this) {
sessionSwapInLocks.remove(id);
}

return session;

}
private Session loadSessionFromStore(String id) throws IOException {
try {
if (SecurityUtil.isPackageProtectionEnabled()){
return securedStoreLoad(id);
} else {
// 依赖于store的实现了,比如 file, jdbc…
return store.load(id);
}
} catch (ClassNotFoundException e) {
String msg = sm.getString(
“persistentManager.deserializeError”, id);
log.error(msg, e);
throw new IllegalStateException(msg, e);
}
}
// store 实现样例: fileStore
// org.apache.catalina.session.FileStore#load
/**

  • Load and return the Session associated with the specified session
  • identifier from this Store, without removing it. If there is no
  • such stored Session, return null.
  • @param id Session identifier of the session to load
  • @exception ClassNotFoundException if a deserialization error occurs
  • @exception IOException if an input/output error occurs
    */
    @Override
    public Session load(String id) throws ClassNotFoundException, IOException {
    // Open an input stream to the specified pathname, if any
    File file = file(id);
    if (file == null) {
    return null;
    }

if (!file.exists()) {
return null;
}

Context context = getManager().getContext();
Log contextLog = context.getLogger();

if (contextLog.isDebugEnabled()) {
contextLog.debug(sm.getString(getStoreName()+“.loading”, id, file.getAbsolutePath()));
}

ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null);

try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
ObjectInputStream ois = getObjectInputStream(fis)) {

StandardSession session = (StandardSession) manager.createEmptySession();
session.readObjectData(ois);
session.setManager(manager);
return session;
} catch (FileNotFoundException e) {
if (contextLog.isDebugEnabled()) {
contextLog.debug(“No persisted data file found”);
}
return null;
} finally {
context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL);
}
}

private void reactivateLoadedSession(String id, Session session) {
if(log.isDebugEnabled())
log.debug(sm.getString(“persistentManager.swapIn”, id));

session.setManager(this);
// make sure the listeners know about it.
((StandardSession)session).tellNew();
// 添加回sessions
add(session);
((StandardSession)session).activate();
// endAccess() to ensure timeouts happen correctly.
// access() to keep access count correct or it will end up
// negative
session.access();
session.endAccess();
}
// 3. session 的移除
@Override
public void remove(Session session, boolean update) {

super.remove (session, update);
// 和内存的实现差别就是,还要多一个对外部存储的管理维护
if (store != null){
removeSession(session.getIdInternal());
}
}

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
img

学习分享,共勉

题外话,毕竟我工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

提升又不知道该从何学起的朋友,同时减轻大家的负担。**

[外链图片转存中…(img-3Ee76HAq-1710692687793)]
[外链图片转存中…(img-tCSNtDn9-1710692687794)]
[外链图片转存中…(img-aWBCIlUN-1710692687795)]
[外链图片转存中…(img-e4eZKyZx-1710692687795)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
[外链图片转存中…(img-iU9UClIH-1710692687796)]

学习分享,共勉

题外话,毕竟我工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值