前进的轮胎 發表於 2026-1-30 13:19:00

keycloak~分布式部署中会话过期清理机制

<h2 id="keycloak-分布式部署中会话过期清理机制">Keycloak 分布式部署中会话过期清理机制</h2>
<p>在 Keycloak 分布式部署(使用外部独立部署的 Infinispan)的架构下,<code>sessions</code> 和 <code>clientSessions</code> 的过期清理涉及<strong>两种不同的部署模式</strong>,机制略有不同:</p>
<h3 id="架构模式-1embedded--remote-store嵌入式缓存--远程存储">架构模式 1:Embedded + Remote Store(嵌入式缓存 + 远程存储)</h3>
<p>这种模式下,Keycloak 节点有<strong>本地嵌入式缓存</strong>,同时配置了<strong>远程存储(Remote Store)</strong>连接到外部 Infinispan 集群。</p>
<pre><code class="language-3:8:model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java">/**
* @author &lt;a href="mailto:mposolda@redhat.com"&gt;Marek Posolda&lt;/a&gt;
*/
@ClientListener
public class RemoteCacheSessionListener&lt;K, V extends SessionEntity&gt;{
</code></pre>
<p><strong>清理机制:</strong></p>
<ol>
<li><strong>本地缓存自动过期</strong>:
<ul>
<li>当会话数据写入本地嵌入式缓存(<code>DefaultSegmentedDataContainer</code>)时,会同时设置 <code>lifespan</code> 和 <code>maxIdle</code> 参数</li>
<li>Infinispan 的内置过期机制会自动清除过期条目</li>
</ul>
</li>
</ol>
<p><img src="https://img2024.cnblogs.com/blog/118538/202601/118538-20260130125841196-1124658537.png" alt="图片" loading="lazy"></p>
<ol start="2">
<li><strong>远程缓存事件同步</strong>:
<ul>
<li><code>RemoteCacheSessionListener</code> 通过 Hot Rod Client Listener 机制监听远程缓存事件</li>
<li>当远程 Infinispan 中条目被删除时,会触发 <code>@ClientCacheEntryRemoved</code> 事件:</li>
</ul>
</li>
</ol>
<pre><code class="language-226:240:model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remotestore/RemoteCacheSessionListener.java">    @ClientCacheEntryRemoved
    public void removed(ClientCacheEntryRemovedEvent event) {
      K key = (K) event.getKey();

      if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {

            this.executor.submit(event, () -&gt; {

                // 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);

            });
      }
    }
</code></pre>
<ol start="3">
<li><strong>重要限制</strong>:
<ul>
<li><strong>Infinispan 不发送过期事件(Expiration Event)</strong>给 Hot Rod 客户端监听器!</li>
<li>远程 Infinispan 中的条目过期时,<strong>不会</strong>主动通知 Keycloak 节点</li>
<li>本地缓存的清理<strong>完全依赖于本地 Infinispan 的自动过期机制</strong></li>
</ul>
</li>
</ol>
<h3 id="架构模式-2remote-only纯远程模式">架构模式 2:Remote Only(纯远程模式)</h3>
<p>这种模式下,Keycloak <strong>不维护本地会话缓存</strong>,所有会话数据都直接存储在外部 Infinispan 集群中。</p>
<pre><code class="language-186:194:model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/remote/RemoteUserSessionProvider.java">    @Override
    public void removeAllExpired() {
      //rely on Infinispan expiration
    }

    @Override
    public void removeExpired(RealmModel realm) {
      //rely on Infinispan expiration
    }
</code></pre>
<p><strong>清理机制</strong>:</p>
<ul>
<li><strong>完全依赖远程 Infinispan 的过期机制</strong></li>
<li>Keycloak 本地 JVM 中<strong>没有 <code>DefaultSegmentedDataContainer</code></strong>,因为不使用嵌入式缓存</li>
<li>所有读取操作直接访问远程缓存,过期数据自然不会被读取到</li>
</ul>
<h3 id="关键代码过期时间计算">关键代码:过期时间计算</h3>
<p>无论哪种模式,会话的过期时间都通过 <code>SessionTimeouts</code> 计算:</p>
<pre><code class="language-52:67:model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/util/SessionTimeouts.java">    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 &amp;&amp; lifespan == IMMORTAL_FLAG) {
            return IMMORTAL_FLAG;
      }
      lifespan = lifespan - Time.currentTimeMillis();
      if (lifespan &lt;= 0) {
            return ENTRY_EXPIRED_FLAG;
      }
      return lifespan;
    }
</code></pre>
<h3 id="总结本地-jvm-中-defaultsegmenteddatacontainer-对象的清理方式">总结:本地 JVM 中 <code>DefaultSegmentedDataContainer</code> 对象的清理方式</h3>
<table>
<thead>
<tr>
<th>场景</th>
<th>清理机制</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>本地条目自然过期</strong></td>
<td>Infinispan 嵌入式缓存的<strong>内置过期 Reaper 线程</strong>自动清理</td>
</tr>
<tr>
<td><strong>远程条目被删除</strong></td>
<td>通过 <code>RemoteCacheSessionListener</code> 的 <code>@ClientCacheEntryRemoved</code> 事件同步删除本地条目</td>
</tr>
<tr>
<td><strong>远程条目自然过期</strong></td>
<td><strong>不会主动通知</strong>!本地条目依赖自身的过期时间自动失效</td>
</tr>
<tr>
<td><strong>Failover 事件</strong></td>
<td>触发 <code>onFailover</code> 回调,清空整个本地缓存(<code>ispnCache::clear</code>)以保证一致性</td>
</tr>
</tbody>
</table>
<h3 id="潜在问题">潜在问题</h3>
<p>由于远程 Infinispan 的过期事件<strong>不会</strong>通知本地缓存,在以下情况下可能存在<strong>短暂的数据不一致</strong>:</p>
<ol>
<li>远程条目已过期被清除</li>
<li>但本地缓存的过期时间还没到</li>
<li>此时本地可能返回一个"幽灵会话"</li>
</ol>
<p><strong>Keycloak 的解决方案</strong>:</p>
<ul>
<li>在每次从本地缓存获取会话时,都会检查 <code>Expiration.isExpired()</code></li>
<li>如果计算出的过期时间已经过了,即使缓存条目存在也会被视为无效</li>
</ul>
<pre><code class="language-29:31:model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/remote/updater/Expiration.java">    public boolean isExpired() {
      return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
    }
</code></pre>
<pre><code class="language-java">/**
* @author &lt;a href="mailto:mposolda@redhat.com"&gt;Marek Posolda&lt;/a&gt;
*/
@ClientListener
public class RemoteCacheSessionListener&lt;K, V extends SessionEntity&gt;{
</code></pre>
<pre><code class="language-java">    @ClientCacheEntryRemoved
    public void removed(ClientCacheEntryRemovedEvent event) {
      K key = (K) event.getKey();

      if (shouldUpdateLocalCache(event.getType(), key, event.isCommandRetried())) {

            this.executor.submit(event, () -&gt; {

                // 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);

            });
      }
    }
</code></pre>
<pre><code class="language-java">    @Override
    public void removeAllExpired() {
      //rely on Infinispan expiration
    }

    @Override
    public void removeExpired(RealmModel realm) {
      //rely on Infinispan expiration
    }
</code></pre>
<pre><code class="language-java">    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 &amp;&amp; lifespan == IMMORTAL_FLAG) {
            return IMMORTAL_FLAG;
      }
      lifespan = lifespan - Time.currentTimeMillis();
      if (lifespan &lt;= 0) {
            return ENTRY_EXPIRED_FLAG;
      }
      return lifespan;
    }
</code></pre>
<pre><code class="language-java">    public boolean isExpired() {
      return maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG || lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG;
    }
</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    <p></p>
<div class="navgood">
<p>作者:仓储大叔,张占岭,<br>
荣誉:微软MVP<br>QQ:853066980</p>

<p><strong>支付宝扫一扫,为大叔打赏!</strong>
<br><img src="https://images.cnblogs.com/cnblogs_com/lori/237884/o_IMG_7144.JPG"></p>
</div><br><br>
来源:https://www.cnblogs.com/lori/p/19552889
頁: [1]
查看完整版本: keycloak~分布式部署中会话过期清理机制