keycloak~分布式部署中会话过期清理机制
Keycloak 分布式部署中会话过期清理机制
在 Keycloak 分布式部署(使用外部独立部署的 Infinispan)的架构下,sessions 和 clientSessions 的过期清理涉及两种不同的部署模式,机制略有不同:
架构模式 1:Embedded + Remote Store(嵌入式缓存 + 远程存储)
这种模式下,Keycloak 节点有本地嵌入式缓存,同时配置了远程存储(Remote Store)连接到外部 Infinispan 集群。
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ClientListener
public class RemoteCacheSessionListener<K, V extends SessionEntity> {
清理机制:
- 本地缓存自动过期:
- 当会话数据写入本地嵌入式缓存(
DefaultSegmentedDataContainer)时,会同时设置lifespan和maxIdle参数 - Infinispan 的内置过期机制会自动清除过期条目
- 当会话数据写入本地嵌入式缓存(

- 远程缓存事件同步:
RemoteCacheSessionListener通过 Hot Rod Client Listener 机制监听远程缓存事件- 当远程 Infinispan 中条目被删除时,会触发
@ClientCacheEntryRemoved事件:
@ClientCacheEntryRemoved
public void removed(ClientCacheEntryRemovedEvent event) {
K key = (K) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
this.executor.submit(event, () -> {
// We received event from remoteCache, so we won't update it back
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.remove(key);
});
}
}
- 重要限制:
- Infinispan 不发送过期事件(Expiration Event)给 Hot Rod 客户端监听器!
- 远程 Infinispan 中的条目过期时,不会主动通知 Keycloak 节点
- 本地缓存的清理完全依赖于本地 Infinispan 的自动过期机制
架构模式 2:Remote Only(纯远程模式)
这种模式下,Keycloak 不维护本地会话缓存,所有会话数据都直接存储在外部 Infinispan 集群中。
@Override
public void removeAllExpired() {
//rely on Infinispan expiration
}
@Override
public void removeExpired(RealmModel realm) {
//rely on Infinispan expiration
}
清理机制:
- 完全依赖远程 Infinispan 的过期机制
- Keycloak 本地 JVM 中没有
DefaultSegmentedDataContainer,因为不使用嵌入式缓存 - 所有读取操作直接访问远程缓存,过期数据自然不会被读取到
关键代码:过期时间计算
无论哪种模式,会话的过期时间都通过 SessionTimeouts 计算:
public static long getUserSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
return getUserSessionLifespanMs(realm, false, userSessionEntity.isRememberMe(), userSessionEntity.getStarted());
}
public static long getUserSessionLifespanMs(RealmModel realm, boolean offline, boolean rememberMe, int started) {
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(offline, rememberMe,
TimeUnit.SECONDS.toMillis(started), realm);
if (offline && lifespan == IMMORTAL_FLAG) {
return IMMORTAL_FLAG;
}
lifespan = lifespan - Time.currentTimeMillis();
if (lifespan <= 0) {
return ENTRY_EXPIRED_FLAG;
}
return lifespan;
}
总结:本地 JVM 中 DefaultSegmentedDataContainer 对象的清理方式
| 场景 | 清理机制 |
|---|---|
| 本地条目自然过期 | Infinispan 嵌入式缓存的内置过期 Reaper 线程自动清理 |
| 远程条目被删除 | 通过 RemoteCacheSessionListener 的 @ClientCacheEntryRemoved 事件同步删除本地条目 |
| 远程条目自然过期 | 不会主动通知!本地条目依赖自身的过期时间自动失效 |
| Failover 事件 | 触发 onFailover 回调,清空整个本地缓存(ispnCache::clear)以保证一致性 |
潜在问题
由于远程 Infinispan 的过期事件不会通知本地缓存,在以下情况下可能存在短暂的数据不一致:
- 远程条目已过期被清除
- 但本地缓存的过期时间还没到
- 此时本地可能返回一个"幽灵会话"
Keycloak 的解决方案:
- 在每次从本地缓存获取会话时,都会检查
Expiration.isExpired() - 如果计算出的过期时间已经过了,即使缓存条目存在也会被视为无效
public boolean isExpired() {
return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
}
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ClientListener
public class RemoteCacheSessionListener<K, V extends SessionEntity> {
@ClientCacheEntryRemoved
public void removed(ClientCacheEntryRemovedEvent event) {
K key = (K) event.getKey();
if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {
this.executor.submit(event, () -> {
// We received event from remoteCache, so we won't update it back
cache.getAdvancedCache().withFlags(Flag.SKIP_CACHE_STORE, Flag.SKIP_CACHE_LOAD, Flag.IGNORE_RETURN_VALUES)
.remove(key);
});
}
}
@Override
public void removeAllExpired() {
//rely on Infinispan expiration
}
@Override
public void removeExpired(RealmModel realm) {
//rely on Infinispan expiration
}
public static long getUserSessionLifespanMs(RealmModel realm, ClientModel client, UserSessionEntity userSessionEntity) {
return getUserSessionLifespanMs(realm, false, userSessionEntity.isRememberMe(), userSessionEntity.getStarted());
}
public static long getUserSessionLifespanMs(RealmModel realm, boolean offline, boolean rememberMe, int started) {
long lifespan = SessionExpirationUtils.calculateUserSessionMaxLifespanTimestamp(offline, rememberMe,
TimeUnit.SECONDS.toMillis(started), realm);
if (offline && lifespan == IMMORTAL_FLAG) {
return IMMORTAL_FLAG;
}
lifespan = lifespan - Time.currentTimeMillis();
if (lifespan <= 0) {
return ENTRY_EXPIRED_FLAG;
}
return lifespan;
}
public boolean isExpired() {
return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
}
浙公网安备 33010602011771号