Appearance
CAS + Redis 分布式票据注册表高可用部署:从单机到集群的完整实践
作者: 必码 | bima.cc
前言
在企业级单点登录(Single Sign-On,SSO)系统的架构设计中,票据注册表(Ticket Registry)是整个CAS协议运转的核心枢纽。当企业从单节点部署向多节点集群演进时,票据的存储和共享方式直接决定了系统的可用性、一致性和性能表现。
Apereo CAS作为业界最成熟的开源SSO解决方案之一,其票据注册表经历了从内存到分布式、从简单到高可用的完整演进路径。本文将基于作者在实际CAS Overlay项目中从5.3到7.3三个大版本的深度实践,全面解析CAS + Redis分布式票据注册表的架构设计、配置演进、连接池调优、会话一致性保障以及高可用部署策略。
本文不提供可直接复制粘贴的完整生产配置,而是将所有配置示例转化为教学用途的简化版本,旨在帮助读者理解配置背后的设计思想和架构决策逻辑。
第一章 CAS票据注册表架构概述
1.1 票据注册表的核心定位
在CAS协议的完整认证流程中,票据注册表扮演着"中央票据存储仓库"的角色。每当用户发起登录请求、获取服务票据、执行单点登出等操作时,CAS服务端都需要与票据注册表进行交互。理解票据注册表的架构,是掌握CAS分布式部署的关键前提。
从CAS协议的角度来看,票据注册表承担以下核心职责:
第一,票据的创建与存储。 当用户通过身份认证后,CAS服务端会生成一个TGT(Ticket Granting Ticket),并将其存储在票据注册表中。TGT是用户后续获取各类服务票据的基础凭证,其安全性直接关系到整个SSO系统的安全边界。
第二,票据的查询与验证。 当客户端拿着ST(Service Ticket)访问某个业务应用时,业务应用需要通过CAS的验证接口来确认该ST的有效性。此时CAS服务端会从票据注册表中查找对应的ST记录,验证其是否存在、是否过期、是否已被使用。
第三,票据的生命周期管理。 票据不是永久有效的,每种票据都有其预设的过期时间。票据注册表需要负责清理过期的票据,释放系统资源。在分布式环境下,这一机制尤为关键——过期的票据如果不能及时清理,会导致Redis内存持续增长,最终影响系统稳定性。
第四,单点登出的票据级联失效。 当用户主动登出或管理员强制终止某个会话时,CAS需要根据TGT找到所有关联的ST,并逐一使其失效。在分布式部署中,这一操作需要跨多个CAS节点协调完成,票据注册表的实现方式直接决定了登出操作的效率和一致性。
从架构设计的角度来看,票据注册表需要满足以下非功能性需求:
- 高性能: 在高并发登录场景下,票据的读写操作必须具备亚毫秒级的响应延迟。
- 高可用: 票据注册表不能成为系统的单点故障,必须支持故障转移和数据冗余。
- 数据一致性: 在多节点部署下,票据的创建、查询和失效操作需要保证最终一致性。
- 可扩展性: 随着业务规模的增长,票据注册表应能水平扩展以应对更大的吞吐量。
1.2 DefaultTicketRegistry:内存单机方案
DefaultTicketRegistry是CAS框架中最基础的票据注册表实现,它使用JVM内存中的ConcurrentHashMap来存储所有票据。这种实现方式简单直接,在单节点部署场景下具有天然的性能优势。
DefaultTicketRegistry的工作原理非常简洁:所有的票据对象被序列化后存储在一个线程安全的Map结构中。当需要查询票据时,直接从内存中读取;当需要清理过期票据时,遍历Map中的所有条目,检查其过期时间并移除已过期的票据。
这种实现方式的优势在于:
- 零网络开销: 所有操作都在JVM进程内完成,没有网络IO延迟。
- 极致的读写性能: ConcurrentHashMap的读写操作时间复杂度接近O(1),在亿级票据量下仍能保持稳定的性能表现。
- 零外部依赖: 不需要部署任何外部存储组件,降低了系统的运维复杂度。
- 调试便利: 票据数据直接存储在内存中,便于在开发和测试环境中进行问题排查。
然而,DefaultTicketRegistry的局限性也非常明显,这些局限性正是我们在生产环境中选择Redis的根本原因:
- 不支持多节点共享: 票据数据仅存在于单个JVM进程中,当部署多个CAS节点时,节点A创建的TGT无法被节点B识别,导致用户在负载均衡切换节点后需要重新登录。
- 服务重启数据丢失: 所有票据存储在内存中,一旦CAS服务重启,所有用户的登录状态将全部丢失,用户体验极差。
- 内存容量受限: 票据数据占用JVM堆内存,在高并发场景下可能导致GC压力增大,甚至引发OOM(Out of Memory)问题。
- 水平扩展困难: 无法通过增加节点来提升票据存储的容量和可用性。
在实际项目中,DefaultTicketRegistry通常仅用于以下场景:
- 本地开发和功能测试环境。
- 部署规模极小(单节点、用户量有限)且对高可用性没有要求的内部系统。
- 作为性能基准,用于评估分布式票据注册表引入的性能损耗。
1.3 RedisTicketRegistry:分布式高可用方案
RedisTicketRegistry是CAS官方提供的分布式票据注册表实现,它将票据数据存储在Redis中,从而天然支持多节点共享和高可用部署。在我们的实际项目中,从CAS 5.3到CAS 7.3,RedisTicketRegistry一直是生产环境的首选方案。
RedisTicketRegistry的核心设计思路是:利用Redis的高性能键值存储能力,将CAS票据序列化后以String类型存储在Redis中。每个票据对应一个Redis Key,Key的命名规则通常包含票据ID的前缀,Value则是序列化后的票据对象。
选择Redis作为票据存储后端的技术决策基于以下考量:
第一,亚毫秒级的读写延迟。 Redis作为内存数据库,其单次GET/SET操作的延迟通常在0.1毫秒以内,远低于关系型数据库。对于CAS认证流程中频繁的票据读写操作,Redis能够提供足够的性能保障。
第二,丰富的数据结构支持。 虽然CAS票据注册表主要使用String类型进行存储,但Redis的Key过期机制(TTL)天然适配票据的过期管理需求。每个票据在写入Redis时可以设置对应的TTL,Redis会在票据过期后自动清理,无需CAS服务端主动执行清理逻辑。
第三,成熟的高可用方案。 Redis提供了Sentinel哨兵和Cluster集群两种高可用部署模式,可以满足不同规模和可用性要求的生产场景。Sentinel模式提供自动故障转移能力,Cluster模式则同时提供了数据分片和高可用保障。
第四,广泛的技术生态。 Redis在企业级技术栈中的普及率极高,大多数企业都有成熟的Redis运维经验和基础设施。选择Redis作为票据存储后端,可以复用现有的运维工具和监控体系。
第五,Spring Data Redis的天然集成。 CAS框架基于Spring Boot构建,而Spring Data Redis提供了完善的Redis客户端抽象层。通过简单的配置即可完成Redis连接的建立和管理,开发成本极低。
从架构演进的角度来看,RedisTicketRegistry的引入使得CAS服务从"有状态"转变为"无状态"——CAS节点本身不再存储任何票据数据,所有的状态信息都外置到Redis中。这种架构使得CAS节点可以随意水平扩展,通过负载均衡将请求分发到任意节点,用户的登录状态都不会丢失。
1.4 其他票据注册表实现对比
除了DefaultTicketRegistry和RedisTicketRegistry之外,CAS框架还提供了多种票据注册表实现,每种实现都有其适用的场景和局限性。了解这些实现的差异,有助于架构师在实际项目中做出合理的技术选型。
HazelcastTicketRegistry
Hazelcast是一个开源的内存数据网格(In-Memory Data Grid,IMDG)解决方案,它提供了分布式Map、队列、主题等数据结构。HazelcastTicketRegistry利用Hazelcast的分布式Map来存储票据数据。
Hazelcast的核心优势在于其"嵌入模式"——Hazelcast节点可以嵌入到CAS应用中运行,无需部署独立的外部服务。当多个CAS节点启动时,它们会自动组成一个Hazelcast集群,票据数据在集群节点之间自动复制和分片。
然而,HazelcastTicketRegistry在实际生产中的使用存在以下挑战:
- 内存开销大: 每个CAS节点都需要预留额外的堆内存用于Hazelcast的数据存储,这在高并发场景下会显著增加JVM的内存压力。
- 集群管理复杂: Hazelcast集群的节点发现、分裂脑(Split-Brain)处理、数据再平衡等机制增加了运维复杂度。
- GC压力: 大量票据对象存储在JVM堆中,会增加垃圾回收的频率和停顿时间。
- 与CAS版本耦合: Hazelcast的版本需要与CAS版本严格匹配,升级时可能遇到兼容性问题。
EhCacheTicketRegistry
EhCache是一个广泛使用的Java缓存框架,EhCacheTicketRegistry利用EhCache的分布式缓存能力来存储票据。EhCache支持本地缓存和分布式缓存两种模式,在分布式模式下可以通过Terracotta服务器实现多节点间的缓存同步。
EhCacheTicketRegistry的局限性主要体现在:
- 分布式能力依赖Terracotta: EhCache的开源版本在分布式缓存方面的能力有限,完整的分布式功能需要商业版的Terracotta支持。
- 社区活跃度下降: 随着Redis等方案的普及,EhCache在分布式缓存领域的市场份额持续下降,社区活跃度不如Redis。
- 性能不如Redis: 在分布式场景下,EhCache的网络通信开销和序列化效率都不如Redis。
JpaTicketRegistry
JpaTicketRegistry将票据数据持久化到关系型数据库中,通过JPA(Java Persistence API)进行数据访问。这种方案的优势在于数据持久化——即使数据库重启,票据数据也不会丢失。
但JpaTicketRegistry的性能瓶颈非常明显:
- 磁盘IO开销: 每次票据的读写都需要经过数据库的磁盘IO,延迟远高于内存存储。
- 并发能力有限: 关系型数据库在高并发写入场景下的性能受限,容易成为系统瓶颈。
- 清理效率低: 过期票据的清理需要执行DELETE操作,在大数据量下可能产生锁竞争。
方案选型对比总结
| 特性维度 | DefaultTicketRegistry | RedisTicketRegistry | HazelcastTicketRegistry | EhCacheTicketRegistry | JpaTicketRegistry |
|---|---|---|---|---|---|
| 部署模式 | 单机 | 分布式 | 分布式(嵌入) | 分布式(需Terracotta) | 分布式 |
| 读写性能 | 极高(内存) | 高(网络+内存) | 高(内存网格) | 中高 | 低(磁盘IO) |
| 数据持久化 | 否(重启丢失) | 可选(RDB/AOF) | 否(重启丢失) | 可选 | 是 |
| 高可用 | 不支持 | Sentinel/Cluster | 内置 | 需额外组件 | 数据库HA |
| 运维复杂度 | 极低 | 低 | 中 | 中高 | 中 |
| 水平扩展 | 不支持 | 支持 | 支持 | 有限 | 有限 |
| 推荐场景 | 开发/测试 | 生产首选 | 特定场景 | 遗留系统 | 审计要求严格 |
基于以上对比分析,RedisTicketRegistry在生产环境中具有最均衡的综合表现,这也是本文选择其作为深度解析对象的原因。
1.5 CAS票据类型体系详解
在深入Redis配置之前,有必要全面理解CAS协议中定义的各类票据。票据是CAS协议的核心概念,不同类型的票据承担着不同的职责,其生命周期和存储策略也各不相同。
TGT(Ticket Granting Ticket)
TGT是CAS协议中最核心的票据类型,也被称为"票据授予票据"。当用户成功通过身份认证后,CAS服务端会生成一个TGT,并通过浏览器Cookie(TGC,Ticket Granting Cookie)将其返回给客户端。
TGT的本质是一个代表用户登录状态的凭证。只要TGT有效,用户就可以在不需要重新输入用户名和密码的情况下,获取访问任意已注册业务应用的ST。TGT的存在是SSO"单点"体验的技术基础。
TGT的关键属性包括:
- 唯一标识: TGT的ID通常以"TGT-"前缀开头,后跟一串随机字符串。
- 认证信息: TGT中包含了用户的认证信息,包括用户名、认证时间、认证方式、客户端IP等。
- 关联的ST集合: 每个TGT维护了一个已发放的ST列表,用于单点登出时的级联失效。
- 过期时间: TGT的默认有效期通常较长(数小时到数天),具体值由CAS配置决定。
- 已认证属性: TGT中可以携带用户的各种属性信息(如角色、部门等),这些属性会在后续的ST验证过程中传递给业务应用。
在Redis中,TGT的存储结构大致如下:
Key: CAS_TICKET:TGT-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Value: <序列化后的TicketGrantingTicketImpl对象>
TTL: 7200秒(默认值,可配置)1
2
3
2
3
ST(Service Ticket)
ST是"服务票据",用于用户访问特定业务应用时的一次性认证凭证。当用户持有效TGT访问某个业务应用时,CAS服务端会根据TGT和目标服务URL生成一个ST,并通过HTTP重定向将ST传递给业务应用。
ST的关键特性包括:
- 一次性使用: ST是一次性票据,验证成功后立即失效。即使被截获,也无法被重复使用。
- 服务绑定: 每个ST都与特定的服务URL绑定,只能在对应的服务上使用。
- 短有效期: ST的有效期通常很短(默认10-30秒),过期后需要重新获取。
- 前缀标识: ST的ID通常以"ST-"前缀开头。
在Redis中,ST的存储结构与TGT类似,但其TTL通常设置为10-30秒:
Key: CAS_TICKET:ST-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Value: <序列化后的ServiceTicketImpl对象>
TTL: 10秒(默认值,可配置)1
2
3
2
3
PGT(Proxy Granting Ticket)
PGT是"代理授予票据",用于CAS的代理(Proxy)认证场景。在某些复杂的企业架构中,后端服务需要代表用户访问另一个后端服务,此时就需要使用代理认证机制。
PGT的获取流程如下:
- 业务应用(前端服务)持ST向CAS验证,在验证请求中声明自己是代理(Proxy)。
- CAS验证ST成功后,生成一个PGT,并通过回调URL(PGT Callback URL)将PGT传递给前端服务。
- 前端服务持有PGT后,可以向CAS申请PT(Proxy Ticket),用于访问后端服务。
PGT的存储方式与TGT类似,但通常具有更长的有效期。
PT(Proxy Ticket)
PT是"代理票据",由PGT派生而来,功能类似于ST,但用于代理认证场景。后端服务收到PT后,向CAS验证PT的有效性,CAS会返回原始用户的身份信息以及代理链信息。
PT同样是一次性票据,有效期通常与ST一致。
票据生命周期关系图
用户认证 --> [TGT] --> 获取ST --> [ST] --> 业务应用验证
|
+--> 代理场景 --> [PGT] --> 获取PT --> [PT] --> 后端服务验证
单点登出 --> [TGT失效] --> 级联失效所有关联的ST1
2
3
4
5
2
3
4
5
理解票据类型体系对于Redis配置至关重要,因为不同类型的票据具有不同的TTL设置、不同的存储频率和不同的清理策略。在实际配置中,我们需要根据票据类型的特性来优化Redis的内存使用和性能表现。
第二章 CAS 5.3 Redis配置实战
2.1 项目依赖与版本锁定
在CAS 5.3版本中,引入Redis票据注册表需要在项目的构建配置中添加相应的依赖。CAS 5.3基于Spring Boot 2.x构建,使用的是较早期的Spring Data Redis版本。在实际项目中,我们使用Gradle作为构建工具,同时维护了Maven版本的配置。
核心依赖包括两个部分:CAS的Redis票据注册表模块和Spring Data Redis客户端库。
在Gradle构建配置中,Redis相关的依赖声明如下(简化教学版):
groovy
dependencies {
// CAS Redis票据注册表核心模块
implementation 'org.apereo.cas:cas-server-support-redis-ticket-registry'
// 显式指定与CAS 5.3兼容的Spring Data Redis版本
implementation 'org.springframework.data:spring-data-redis:1.8.23.RELEASE'
implementation 'redis.clients:jedis:2.9.3'
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
在Maven的pom.xml中,对应的依赖声明为:
xml
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-redis-ticket-registry</artifactId>
</dependency>1
2
3
4
2
3
4
这里有一个非常重要的工程实践:显式版本锁定。在CAS 5.3的时代,Spring Data Redis的版本兼容性是一个常见的踩坑点。CAS 5.3.16内部依赖的Spring Boot版本为2.7.18,但其Redis模块对Spring Data Redis的版本有特殊要求。如果不显式指定版本,Gradle/Maven的依赖解析可能会引入不兼容的版本,导致运行时出现NoSuchMethodError或ClassNotFoundException。
在我们的实际项目中,经过反复测试验证,确定了以下版本组合是稳定可用的:
- CAS版本: 5.3.16
- Spring Boot版本: 2.7.18
- Spring Data Redis版本: 1.8.23.RELEASE
- Jedis版本: 2.9.3
这个版本组合看起来有些"古老"——Spring Data Redis 1.8.x是相当早期的版本,但它与CAS 5.3的内部API完全兼容。如果强行升级到Spring Data Redis 2.x,会面临大量的API变更和兼容性问题。
2.2 Redis直连配置详解
CAS 5.3的Redis配置位于cas.ticket.registry.redis命名空间下。这是一个CAS专属的配置路径,与Spring Boot标准的spring.redis配置路径不同。这种设计使得CAS可以在不影响Spring Boot其他Redis自动配置的前提下,独立管理票据注册表的Redis连接。
以下是CAS 5.3中Redis直连配置的教学简化版:
yaml
cas:
ticket:
registry:
redis:
# Redis服务器地址
host: redis.example.com
# Redis服务器端口
port: 6379
# 连接超时时间(毫秒)
timeout: 2000
# Redis数据库索引(0-15)
database: 0
# 是否启用SSL连接
useSsl: false
# 是否启用连接池
usePool: true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
逐项解析这些配置参数的设计考量:
host和port: 这是最基本的连接参数,指定Redis服务器的地址和端口。在开发环境中通常使用单机Redis,在生产环境中则可能指向Sentinel或Cluster的入口地址。
timeout: 连接超时时间设置为2000毫秒(2秒)。这个值的选择需要在"快速失败"和"容忍网络抖动"之间取得平衡。在局域网环境中,2秒的超时已经足够宽松;在跨机房或跨可用区部署时,可能需要适当增大此值。
database: Redis支持0-15共16个数据库实例。将CAS票据存储在独立的数据库中(如database 0),可以与其他业务数据隔离,便于运维管理和数据清理。需要注意的是,在Redis Cluster模式下,database参数会被忽略,因为Cluster模式只支持database 0。
useSsl: 在生产环境中,如果CAS与Redis之间跨越不安全的网络(如跨机房、跨公网),应启用SSL加密传输。但在同一数据中心内部署时,通常不需要启用SSL,以避免不必要的性能开销。
usePool: 启用连接池是生产环境的标配配置。连接池可以避免频繁创建和销毁Redis连接带来的开销,显著提升高并发场景下的性能表现。
2.3 连接池参数配置
在CAS 5.3中,Redis连接池的配置位于cas.ticket.registry.redis.pool命名空间下。连接池参数的合理设置对于系统性能和稳定性至关重要。
yaml
cas:
ticket:
registry:
redis:
pool:
# 连接池最大活动连接数
max-active: 20
# 连接池最大空闲连接数
max-idle: 10
# 连接池最小空闲连接数
min-idle: 5
# 获取连接最大等待时间(毫秒),-1表示无限等待
max-wait: -11
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
max-active: 20 -- 这个参数定义了连接池中同时处于活动状态的最大连接数。当所有活动连接都在使用中时,新的请求将进入等待队列。设置为20意味着连接池最多可以同时处理20个并发Redis操作。
这个值的选择需要考虑以下因素:
- CAS节点的并发登录请求数。在高峰期,每个登录请求可能涉及2-3次Redis操作(写入TGT、写入ST、查询服务注册信息)。
- Redis服务器的最大连接数限制(默认10000)。
- CAS集群的节点数量。如果有4个CAS节点,每个节点20个连接,总共80个连接,远低于Redis的默认限制。
max-idle: 10 -- 连接池中允许保持的最大空闲连接数。当连接使用完毕归还到池中时,如果当前空闲连接数已经达到max-idle,多余的连接将被销毁。这个参数的作用是防止连接池在低负载期间积累过多的空闲连接,浪费Redis服务器的连接资源。
min-idle: 5 -- 连接池中保持的最小空闲连接数。当空闲连接数低于此值时,连接池会主动创建新的连接以维持这个最小数量。设置min-idle的目的是在流量突增时,避免"冷启动"延迟——连接已经预先创建好,可以直接使用。
max-wait: -1 -- 当连接池中所有连接都在使用中时,新请求获取连接的最大等待时间。-1表示无限等待,即请求会一直阻塞直到有可用连接。在生产环境中,通常建议设置一个合理的超时值(如3000毫秒),以避免线程长时间阻塞导致级联故障。
2.4 Spring Session Redis会话存储
在分布式CAS部署中,除了票据数据需要共享之外,HTTP会话(Session)也需要在多个CAS节点之间共享。CAS 5.3通过Spring Session + Redis来实现会话共享。
在CAS 5.3的配置中,会话存储类型通过以下方式指定:
yaml
server:
session:
# 会话超时时间(秒)
timeout: 300
# Cookie安全配置
cookie:
http-only: true
# 会话追踪模式:仅使用Cookie
tracking-modes: COOKIE
# 会话存储类型:使用Redis
store-type: redis1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
timeout: 300 -- 会话超时时间设置为300秒(5分钟)。这个值在CAS 5.3中是一个相对保守的设置。会话超时与TGT超时是两个不同的概念:会话超时控制的是HTTP会话的有效期,而TGT超时控制的是SSO登录状态的有效期。在CAS 5.3的默认配置中,会话超时(300秒)远短于TGT超时(通常为2小时或更长)。
这种设计的意图是:HTTP会话主要用于保护CAS的Web交互流程(如登录页面的表单提交),一旦登录流程完成,TGT已经存储在Redis中,HTTP会话的历史使命就基本完成了。较短的会话超时可以减少Redis中的会话数据占用。
store-type: redis -- 这个配置将Spring Session的存储后端切换为Redis。启用后,Spring Session会自动将会话数据序列化后存储在Redis中,并通过自定义的Cookie(通常名为SESSION)来关联客户端与Redis中的会话数据。
tracking-modes: COOKIE -- 限制会话追踪仅使用Cookie方式,禁用URL重写(URL Rewriting)。这是一个安全最佳实践——URL重写方式会将会话ID暴露在URL中,容易被日志记录或通过Referer头泄露。
2.5 版本兼容性踩坑实录
在CAS 5.3的Redis集成实践中,版本兼容性是最常见的踩坑领域。以下是我们在实际项目中遇到的问题及解决方案。
问题一:Spring Data Redis版本不兼容
现象: CAS服务启动时报错NoSuchMethodError: org.springframework.data.redis.connection.RedisConnectionFactory.getConnection()
根因: CAS 5.3的cas-server-support-redis-ticket-registry模块内部依赖的是Spring Data Redis 1.8.x的API。如果项目中引入了Spring Data Redis 2.x(Spring Boot 2.x默认),会导致方法签名不匹配。
解决方案: 显式锁定Spring Data Redis的版本为1.8.23.RELEASE,同时锁定Jedis版本为2.9.3。这两个版本是经过CAS社区验证的稳定组合。
groovy
// 显式版本锁定(关键!)
implementation 'org.springframework.data:spring-data-redis:1.8.23.RELEASE'
implementation 'redis.clients:jedis:2.9.3'1
2
3
2
3
问题二:Jedis连接池类型冲突
现象: 启动时报错ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig
根因: Jedis 2.9.x的连接池功能依赖Apache Commons Pool2。如果项目中没有引入commons-pool2依赖,连接池将无法正常工作。
解决方案: 确保commons-pool2依赖存在于classpath中。在Spring Boot 2.x中,这个依赖通常通过spring-boot-starter-data-redis自动引入,但在CAS 5.3的Overlay项目中,由于依赖管理的特殊性,可能需要手动添加。
问题三:序列化方式不一致
现象: CAS节点A写入的票据,CAS节点B读取时报反序列化错误。
根因: 不同CAS节点的Spring Data Redis序列化配置不一致。CAS 5.3默认使用JdkSerializationRedisSerializer,这种序列化方式要求读写双方具有完全一致的类定义。如果不同节点的CAS版本或依赖版本不一致,可能导致序列化ID不匹配。
解决方案: 确保所有CAS节点使用完全相同的CAS版本和依赖版本。在CI/CD流程中,应使用统一的构建产物进行部署,避免在不同环境中分别构建。
问题四:Redis连接在CAS启动时未就绪
现象: CAS服务启动过程中,Redis尚未完全就绪,导致Bean初始化失败。
根因: CAS 5.3的Redis连接在应用启动时就会建立,如果Redis服务尚未就绪,会导致启动失败。
解决方案: 在部署脚本中添加健康检查,确保Redis完全就绪后再启动CAS服务。在Docker Compose或Kubernetes环境中,可以使用depends_on配合健康检查来实现启动顺序控制。
第三章 CAS 6.6 Redis配置演进
3.1 显式类型声明机制
CAS 6.6在票据注册表的配置上引入了一个重要的变化:显式类型声明。在CAS 5.3中,只要引入了cas-server-support-redis-ticket-registry依赖并配置了Redis参数,CAS就会自动使用Redis作为票据注册表。而在CAS 6.6中,需要通过type参数显式指定票据注册表的类型。
yaml
cas:
ticket:
registry:
# 显式声明使用REDIS类型的票据注册表
type: REDIS
redis:
host: redis.example.com
port: 6379
timeout: 5000
database: 01
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
type: REDIS这个配置项看起来简单,但其背后的设计思想值得关注。CAS 6.6支持多种票据注册表实现(DEFAULT、REDIS、HAZELCAST、EHCACHE、JPA等),引入显式类型声明后,CAS可以根据类型值有条件地加载对应的自动配置类,避免了不必要的Bean创建和依赖注入。
这种设计带来的好处包括:
- 启动性能优化: CAS在启动时只需要初始化指定类型的票据注册表相关的Bean,减少了启动时间和内存占用。
- 配置验证增强: CAS可以在启动阶段就验证配置的完整性。如果声明了
type: REDIS但Redis连接参数缺失,CAS可以在启动时快速失败(Fail-Fast),而不是在运行时才发现问题。 - 多注册表切换便利: 在测试环境中,可以通过修改
type值快速切换票据注册表实现,便于性能对比和功能验证。
3.2 增强的连接池配置
CAS 6.6在Redis连接池配置方面进行了显著增强,新增了connection-pool配置块,提供了更细粒度的连接池控制能力。
yaml
cas:
ticket:
registry:
redis:
# 基础连接池配置
pool:
max-active: 50
max-idle: 20
min-idle: 10
max-wait: 3000
# 增强的连接池配置(CAS 6.6新增)
connection-pool:
max-total: 50
max-idle: 20
min-idle: 10
max-wait-millis: 3000
# 借出连接时是否进行有效性检查
test-on-borrow: true
# 归还连接时是否进行有效性检查
test-on-return: true
# 空闲时是否进行有效性检查
test-while-idle: true
# 空闲连接检测间隔(毫秒)
time-between-eviction-runs-millis: 60000
# 连接最小空闲时间(毫秒),超过此时间的空闲连接将被回收
min-evictable-idle-time-millis: 3000001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这里出现了两套连接池配置:pool和connection-pool。这种设计反映了CAS 6.6在配置模型上的过渡状态——pool是CAS 5.3遗留的简化配置,connection-pool是新增的完整配置。在实际使用中,两套配置可以共存,connection-pool中的参数会覆盖pool中的同名参数。
test-on-borrow: true -- 这个参数控制是否在从连接池借出连接时进行有效性检查。启用后,每次获取连接时都会发送一个PING命令到Redis服务器,确认连接仍然有效。这可以有效避免使用已断开的连接,但每次借出连接都会增加一次网络往返的开销。
在生产环境中,是否启用test-on-borrow需要权衡:
- 如果Redis与CAS之间的网络非常稳定(如同机房部署),可以关闭test-on-borrow以减少延迟。
- 如果Redis与CAS之间存在网络不稳定因素(如跨可用区、跨机房),建议开启test-on-borrow以提升可靠性。
- 一种折中方案是关闭test-on-borrow,但开启test-while-idle,这样可以在空闲时检测失效连接,而不影响正常请求的延迟。
test-while-idle: true -- 这个参数控制是否在连接空闲时进行有效性检查。开启后,连接池的后台线程会定期检查空闲连接的有效性,自动剔除已断开的连接。这是一种"低开销、高回报"的健康检查策略。
time-between-eviction-runs-millis: 60000 -- 空闲连接检测的执行间隔,设置为60秒。这意味着连接池的后台线程每60秒会扫描一次空闲连接,检查它们是否仍然有效。这个值不宜设置过小,否则会增加CPU和网络开销;也不宜设置过大,否则失效连接不能被及时清理。
min-evictable-idle-time-millis: 300000 -- 连接在池中空闲的最小时间(5分钟),超过此时间的空闲连接在下次eviction检查时将被回收。这个参数与min-idle配合使用:即使空闲连接数低于min-idle,如果连接的空闲时间超过了min-evictable-idle-time-millis,也会被回收,然后由连接池创建新的连接来补充min-idle。
3.3 Redis Cleaner定时清理任务
CAS 6.6新增了Redis Cleaner(清理器)的配置,用于定期清理Redis中过期的票据数据。
yaml
cas:
ticket:
registry:
redis:
cleaner:
# 是否启用清理任务
enabled: true
# 清理任务的执行间隔(秒)
repeat-interval: 18001
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
enabled: true -- 启用Redis清理任务。虽然Redis本身会通过TTL机制自动清理过期的Key,但CAS的清理器提供了额外的保障:
- Redis的TTL清理是惰性的(Lazy)+ 定期抽样的组合策略,存在一定的延迟。在高写入负载下,过期Key可能不会被立即清理。
- CAS清理器可以执行更精细的清理逻辑,例如根据票据的业务状态(如已使用、已撤销)进行主动清理。
- 清理器可以收集清理统计信息,用于监控和告警。
repeat-interval: 1800 -- 清理任务每1800秒(30分钟)执行一次。这个间隔的选择需要考虑:
- 票据的过期策略。如果ST的TTL为10秒,那么30分钟的清理间隔已经足够——ST会在10秒后自动过期。
- Redis的内存使用情况。如果Redis内存充裕,可以适当增大清理间隔以减少CPU开销;如果Redis内存紧张,可以减小清理间隔。
- 监控需求。更频繁的清理可以提供更及时的统计信息。
需要特别注意的是,CAS 6.6的Cleaner与Redis自身的TTL机制是互补关系,而非替代关系。TTL提供了基础的数据过期保障,Cleaner提供了额外的主动清理能力。两者配合使用,可以确保Redis中的票据数据不会无限增长。
3.4 Bean定义覆盖策略
CAS 6.6引入了一个重要的Spring配置:spring.main.allow-bean-definition-overriding: true。
yaml
spring:
main:
allow-bean-definition-overriding: true
banner-mode: off1
2
3
4
2
3
4
这个配置的背景是:CAS 6.6的模块化程度更高,多个CAS模块可能定义了同名的Bean。在Spring Boot的默认行为中,如果出现同名的Bean定义,应用启动时会抛出BeanDefinitionOverrideException异常。设置allow-bean-definition-overriding: true后,后加载的Bean定义会覆盖先加载的定义。
在实际项目中,这个配置通常是必需的,原因包括:
- CAS的Redis模块可能与Spring Boot的Redis自动配置模块定义了同名的ConnectionFactory Bean。
- CAS的多个支持模块(如OAuth模块、Rest模块)可能存在Bean定义冲突。
- 在自定义扩展中,开发者可能需要覆盖CAS默认的Bean定义。
虽然这个配置解决了启动问题,但架构师需要意识到其潜在风险:Bean覆盖可能导致非预期的行为,特别是在升级CAS版本时,Bean的定义可能发生变化。建议在升级后进行完整的回归测试,确保Bean覆盖不会引入功能异常。
3.5 会话超时策略调整
CAS 6.6将会话超时时间从5.3的300秒大幅调整为86400秒(24小时):
yaml
server:
session:
timeout: 86400
cookie:
http-only: true
secure: false
tracking-modes: COOKIE1
2
3
4
5
6
7
2
3
4
5
6
7
这个变化反映了CAS架构团队对会话管理策略的重新思考。在CAS 5.3中,300秒的会话超时在大多数场景下是足够的,因为HTTP会话主要用于保护登录流程。但在CAS 6.6中,随着更多功能(如OAuth授权流程、REST API认证等)依赖HTTP会话,过短的会话超时可能导致用户体验问题。
86400秒(24小时)的会话超时意味着:
- 用户的HTTP会话在一天内保持有效,避免了频繁的会话过期。
- 会话数据在Redis中会占用更多的内存,需要通过合理的TTL配置来控制。
- 与TGT的超时时间(通常为2-8小时)解耦,会话超时不再成为SSO体验的瓶颈。
在实际项目中,会话超时的设置需要根据业务需求进行调整。如果安全策略要求更短的会话有效期,可以适当减小此值。但建议不要低于TGT的超时时间,否则会出现"HTTP会话已过期但TGT仍有效"的不一致状态。
第四章 CAS 7.3 Redis配置革新
4.1 配置路径迁移到Spring全局命名空间
CAS 7.3在Redis配置方面进行了最根本性的变革:将Redis连接配置从CAS专属的cas.ticket.registry.redis命名空间迁移到了Spring标准的spring.redis全局命名空间。
CAS 5.3 / 6.6的配置路径:
yaml
cas:
ticket:
registry:
redis:
host: redis.example.com
port: 63791
2
3
4
5
6
2
3
4
5
6
CAS 7.3的配置路径:
yaml
spring:
redis:
host: redis.example.com
port: 6379
database: 0
timeout: 100001
2
3
4
5
6
2
3
4
5
6
这个变化的背后有几个重要的设计考量:
第一,配置标准化。 将Redis配置统一到Spring标准的命名空间下,使得CAS的Redis配置与Spring Boot生态中的其他项目保持一致。开发者不需要记忆CAS专属的Redis配置路径,降低了学习成本。
第二,连接复用。 在CAS 7.3中,Redis连接不再仅用于票据注册表,还可能用于Spring Session、分布式缓存、分布式锁等多种用途。使用全局的spring.redis配置,所有这些用途可以共享同一个Redis连接池,提高了资源利用率。
第三,Sentinel/Cluster配置简化。 在CAS 5.3/6.6中,配置Redis Sentinel需要使用CAS专属的配置路径,格式与Spring Boot标准不同。迁移到spring.redis后,可以直接使用Spring Boot标准的Sentinel和Cluster配置方式,配置更加直观。
第四,与Spring Boot 3.x的深度集成。 CAS 7.3基于Spring Boot 3.5.x构建,Spring Boot 3.x对Redis的自动配置进行了大量增强(如Lettuce成为默认客户端、响应式Redis支持等)。使用标准配置路径可以充分利用这些增强能力。
需要注意的是,CAS 7.3中仍然保留了cas.ticket.registry.redis的部分配置项(如enabled、enableRedisSearch),用于控制票据注册表特有的行为。但Redis的连接参数(host、port、password、timeout等)已经完全迁移到spring.redis下。
以下是CAS 7.3中完整的Redis相关配置示例(教学简化版):
yaml
spring:
# 全局Redis连接配置
redis:
host: redis.example.com
port: 6379
database: 0
timeout: 10000
cas:
ticket:
registry:
# 票据注册表特有配置
redis:
enabled: true
host: redis.example.com
port: 6379
database: 0
password: ""
timeout: PT10S
useSsl: false
enableRedisSearch: false
pool:
enabled: true
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 01
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
从上面的配置可以看出,CAS 7.3存在一定程度的"配置冗余"——Redis连接参数在spring.redis和cas.ticket.registry.redis中都可能出现。在实际项目中,建议将连接参数统一配置在spring.redis下,cas.ticket.registry.redis仅用于票据注册表特有的功能开关和参数。
4.2 Duration格式超时配置
CAS 7.3在超时配置上采用了ISO-8601 Duration格式,这是一个值得关注的细节变化。
CAS 5.3 / 6.6的超时配置(毫秒数值):
yaml
cas:
ticket:
registry:
redis:
timeout: 2000 # 毫秒1
2
3
4
5
2
3
4
5
CAS 7.3的超时配置(Duration格式):
yaml
cas:
ticket:
registry:
redis:
timeout: PT10S # 10秒1
2
3
4
5
2
3
4
5
PT10S是ISO-8601 Duration格式的表示,含义为"Period of Time: 10 Seconds"。常用的Duration格式示例:
PT5S-- 5秒PT30S-- 30秒PT1M-- 1分钟PT10S-- 10秒PT1H30M-- 1小时30分钟
这种变化的好处在于:
- 可读性更强:
PT10S比10000更直观,一眼就能看出是10秒。 - 单位明确: 数值格式容易产生歧义(是毫秒还是秒?),Duration格式则没有歧义。
- 与Spring Boot 3.x保持一致: Spring Boot 3.x广泛使用Duration格式进行超时配置,CAS 7.3遵循了这一约定。
在CAS 7.3中,spring.redis.timeout仍然使用数值格式(毫秒),而cas.ticket.registry.redis.timeout使用Duration格式。这种不一致是CAS版本过渡期的产物,在后续版本中可能会统一。
4.3 enableRedisSearch新特性
CAS 7.3新增了enableRedisSearch配置项:
yaml
cas:
ticket:
registry:
redis:
enableRedisSearch: false1
2
3
4
5
2
3
4
5
这个配置项控制是否启用基于Redis的票据搜索功能。当设置为true时,CAS会在写入票据时额外构建搜索索引,支持按用户名、服务ID等条件查询票据。
在实际项目中,这个功能的使用场景包括:
- 管理控制台: 管理员需要查询某个用户的所有活跃票据,用于会话管理或安全审计。
- 故障排查: 运维人员需要查找特定服务的票据状态,用于定位认证问题。
- 安全审计: 安全团队需要统计特定时间范围内的票据发放情况。
但启用Redis Search也会带来额外的开销:
- 写入性能下降: 每次写入票据时需要额外更新搜索索引,增加了Redis的写入负载。
- 内存占用增加: 搜索索引本身需要占用Redis内存。
- 依赖Redis模块: Redis Search功能可能需要安装额外的Redis模块(如RediSearch),增加了部署复杂度。
在生产环境中,如果不需要票据搜索功能,建议保持enableRedisSearch: false,以获得最佳的性能表现。
4.4 cas-server-support-redis-core独立模块
CAS 7.3在模块划分上进行了重要调整,新增了cas-server-support-redis-core独立模块。在CAS 5.3和6.6中,Redis相关的所有功能都包含在cas-server-support-redis-ticket-registry一个模块中。而在CAS 7.3中,Redis的核心功能被拆分到了独立的core模块中。
在Gradle构建配置中:
groovy
dependencies {
// Redis票据注册表模块
implementation 'org.apereo.cas:cas-server-support-redis-ticket-registry'
// Redis核心模块(CAS 7.3新增)
implementation 'org.apereo.cas:cas-server-support-redis-core'
}1
2
3
4
5
6
2
3
4
5
6
在Maven的pom.xml中:
xml
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-redis-ticket-registry</artifactId>
</dependency>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-support-redis-core</artifactId>
</dependency>1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
这个模块拆分的设计意图包括:
- 关注点分离: Redis的连接管理、序列化、健康检查等核心功能与票据注册表的具体业务逻辑分离,提高了模块的内聚性。
- 复用性增强:
redis-core模块可以被CAS的其他功能模块复用(如分布式缓存、分布式锁等),而不需要依赖完整的票据注册表模块。 - 依赖管理优化: 项目可以根据实际需求选择性引入Redis模块,减少不必要的依赖。
在实际项目中,如果使用CAS 7.3的Redis票据注册表,建议同时引入这两个模块,以确保所有必要的类和自动配置都能正确加载。
4.5 连接池参数调优变化
CAS 7.3在连接池配置方面也进行了调整。与CAS 6.6相比,CAS 7.3的连接池配置更加简洁,移除了connection-pool配置块,将所有参数统一到pool下。
yaml
cas:
ticket:
registry:
redis:
pool:
enabled: true
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 01
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
与CAS 6.6的配置相比,CAS 7.3的默认值更加保守:
- max-active从50降到8: 这反映了CAS 7.3架构优化的结果。在Spring Boot 3.x中,Lettuce成为了默认的Redis客户端,Lettuce基于Netty的异步非阻塞IO模型,单个连接可以处理多个并发请求,因此不需要像Jedis那样维护大量连接。
- 移除了test-on-borrow、test-on-return等参数: Lettuce的连接管理机制与Jedis不同,它使用连接状态检测而非PING命令来验证连接有效性,因此不需要这些参数。
- min-idle默认为0: 在Lettuce中,连接的创建和销毁开销很小,不需要预先维护空闲连接。
CAS 7.3基于Spring Boot 3.5.x构建,要求Java 21运行环境。这一代技术栈的升级带来了以下变化:
- Jedis vs Lettuce: Spring Boot 3.x默认使用Lettuce作为Redis客户端。Lettuce基于Netty实现,支持同步、异步和响应式编程模型,在高并发场景下性能优于Jedis。
- Jakarta EE迁移: CAS 7.3完成了从javax到jakarta的包名迁移,这是Spring Boot 3.x的要求。
- 虚拟线程支持: Java 21引入的虚拟线程(Virtual Threads)可以进一步提升CAS的并发处理能力。
第五章 Redis连接池深度调优
5.1 Jedis vs Lettuce对比分析
在CAS + Redis的技术栈中,Redis客户端的选择直接影响连接池的配置策略和性能表现。CAS 5.3和6.6主要使用Jedis作为Redis客户端,而CAS 7.3则默认使用Lettuce。理解两者的差异对于连接池调优至关重要。
Jedis:同步阻塞模型
Jedis是最早被广泛使用的Java Redis客户端,其设计简洁直观:
- 同步阻塞IO: 每次Redis操作都会占用一个线程,直到操作完成才能释放。这意味着在高并发场景下,需要大量的连接来并行处理请求。
- 连接池必需: 由于每个连接同时只能处理一个请求,Jedis必须依赖连接池来提供并发能力。连接池的大小直接决定了系统的最大并发Redis操作数。
- 线程安全: Jedis的连接对象不是线程安全的,必须通过连接池来管理连接的借出和归还。
- 社区成熟: Jedis的社区非常成熟,文档丰富,问题解决方案完善。
Jedis的连接模型可以类比为"一连接一线程"的传统HTTP客户端模型。在CAS 5.3/6.6中,每个登录请求可能涉及2-3次Redis操作,如果使用Jedis,这些操作会串行或并行地占用连接池中的连接。
Lettuce:异步非阻塞模型
Lettuce是Spring Boot 2.x/3.x默认的Redis客户端,基于Netty构建:
- 异步非阻塞IO: Lettuce使用Netty的EventLoop模型,单个连接可以同时处理多个请求。底层通过Redis的Pipeline和Multiplexing协议实现。
- 连接池可选: 由于单个连接就能处理多个并发请求,Lettuce在不使用连接池的情况下也能提供良好的并发性能。当然,Lettuce也支持连接池模式。
- 线程安全: Lettuce的连接对象是线程安全的,多个线程可以共享同一个连接。
- 响应式支持: Lettuce原生支持响应式编程(Reactive Programming),可以与Spring WebFlux等响应式框架无缝集成。
- 自动重连: Lettuce内置了连接断开后的自动重连机制,不需要额外的心跳检测。
Lettuce的连接模型可以类比为"多路复用"的HTTP/2客户端模型。在CAS 7.3中,即使只使用一个Redis连接,也能处理大量的并发请求。
对比总结
| 特性维度 | Jedis | Lettuce |
|---|---|---|
| IO模型 | 同步阻塞 | 异步非阻塞(Netty) |
| 并发模型 | 一连接一请求 | 多路复用 |
| 连接池 | 必需 | 可选 |
| 线程安全 | 连接非线程安全 | 连接线程安全 |
| 响应式支持 | 不支持 | 原生支持 |
| 自动重连 | 不支持(需连接池管理) | 内置支持 |
| Spring Boot默认 | 1.x | 2.x/3.x |
| CAS版本 | 5.3, 6.6 | 7.3 |
| 依赖体积 | 较小 | 较大(含Netty) |
选型建议
- CAS 5.3 / 6.6: 推荐使用Jedis,因为CAS的Redis模块与Jedis的API深度耦合。虽然理论上可以切换到Lettuce,但需要大量的适配工作。
- CAS 7.3: 推荐使用Lettuce(默认),可以充分利用Spring Boot 3.x的自动配置和连接管理能力。如果团队有特殊的性能需求或运维习惯,也可以显式切换到Jedis。
5.2 连接池核心参数详解
无论使用Jedis还是Lettuce,连接池的核心参数概念是通用的。本节将深入解析每个参数的含义、影响和调优策略。
max-active(最大活动连接数)
max-active定义了连接池中同时处于"被借出"状态的最大连接数。当活动连接数达到此上限时,新的获取连接请求将进入等待队列(或直接抛出异常,取决于max-wait的配置)。
调优原则:
- 对于Jedis(CAS 5.3/6.6),max-active应设置为预期最大并发Redis操作数的1.2-1.5倍,留出一定的缓冲空间。
- 对于Lettuce(CAS 7.3),由于多路复用的特性,max-active可以设置得较小(如8-16),甚至可以不使用连接池。
- 需要考虑Redis服务器的maxclients配置(默认10000),确保所有CAS节点的连接总数不超过此限制。
计算公式(Jedis场景):
max-active = (单节点峰值QPS / 单连接平均QPS) * 1.51
例如,如果单节点峰值QPS为500,单个Jedis连接平均可以处理100 QPS,则:
max-active = (500 / 100) * 1.5 = 7.5 ≈ 81
max-idle(最大空闲连接数)
max-idle定义了连接池中允许保持的最大空闲连接数。当连接使用完毕归还到池中时,如果当前空闲连接数已经达到max-idle,多余的连接将被关闭并销毁。
调优原则:
- max-idle通常设置为max-active的50%-80%,避免在流量下降时连接池快速收缩。
- 在流量波动较大的场景下,适当增大max-idle可以减少连接创建/销毁的开销。
- max-idle不应超过max-active,否则没有实际意义。
min-idle(最小空闲连接数)
min-idle定义了连接池中保持的最小空闲连接数。当空闲连接数低于此值时,连接池的后台线程会主动创建新的连接。
调优原则:
- min-idle的作用是"预热"连接池,避免流量突增时的"冷启动"延迟。
- 对于Jedis,建议min-idle设置为max-active的25%-50%。
- 对于Lettuce,min-idle可以设置为0(默认值),因为Lettuce的连接创建开销很小。
max-wait(最大等待时间)
max-wait定义了当连接池中所有连接都在使用中时,新的获取连接请求的最大等待时间。
调优原则:
- 设置为-1表示无限等待(不推荐在生产环境使用)。
- 建议设置为3000-5000毫秒,在"容忍短暂等待"和"快速失败"之间取得平衡。
- 如果max-wait超时后抛出异常,调用方需要妥善处理异常(如重试或返回错误)。
test-on-borrow(借出时验证)
控制是否在从连接池借出连接时发送PING命令验证连接有效性。
调优原则:
- 开启可以避免使用已断开的连接,但每次借出增加一次网络往返(约0.1-1毫秒)。
- 如果网络非常稳定,可以关闭以减少延迟。
- 如果网络不稳定(跨可用区部署),建议开启。
- 对于Lettuce,通常不需要开启,因为Lettuce内置了连接状态检测。
test-on-return(归还时验证)
控制是否在将连接归还到池中时发送PING命令验证连接有效性。
调优原则:
- 通常不推荐开启,因为归还验证会增加请求的处理延迟。
- 如果连接泄漏是一个严重问题,可以临时开启以辅助排查。
- 对于Lettuce,不需要此参数。
test-while-idle(空闲时验证)
控制是否在连接空闲时由后台线程定期验证连接有效性。
调优原则:
- 推荐开启,这是一种低开销的健康检查策略。
- 配合time-between-eviction-runs-millis使用,控制检查频率。
time-between-eviction-runs-millis(空闲检查间隔)
定义后台线程执行空闲连接检查的间隔时间。
调优原则:
- 建议设置为30000-120000毫秒(30秒到2分钟)。
- 不宜设置过小(增加CPU和网络开销),也不宜设置过大(失效连接不能及时清理)。
min-evictable-idle-time-millis(最小空闲时间)
定义连接在池中空闲的最小时间。超过此时间的空闲连接在下次eviction检查时可能被回收。
调优原则:
- 建议设置为180000-600000毫秒(3分钟到10分钟)。
- 需要大于time-between-eviction-runs-millis,否则新创建的空闲连接可能立即被回收。
- 配合min-idle使用:即使空闲连接数低于min-idle,超过min-evictable-idle-time-millis的连接也会被回收。
5.3 生产环境推荐配置
基于上述参数分析,以下是针对不同CAS版本的生产环境推荐配置。
CAS 5.3 / 6.6(Jedis连接池)-- 中等规模部署
yaml
cas:
ticket:
registry:
redis:
host: redis-cluster.example.com
port: 6379
timeout: 2000
database: 0
useSsl: false
usePool: true
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 30001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
适用场景: 2-4个CAS节点,日活用户10万以内,峰值QPS 500以内。
CAS 5.3 / 6.6(Jedis连接池)-- 大规模部署
yaml
cas:
ticket:
registry:
redis:
host: redis-cluster.example.com
port: 6379
timeout: 3000
database: 0
useSsl: true
usePool: true
pool:
max-active: 50
max-idle: 25
min-idle: 10
max-wait: 5000
connection-pool:
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 3000001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
适用场景: 4-8个CAS节点,日活用户50万以上,峰值QPS 2000以上。
CAS 7.3(Lettuce连接池)-- 通用部署
yaml
spring:
redis:
host: redis-cluster.example.com
port: 6379
database: 0
timeout: 10000
cas:
ticket:
registry:
redis:
enabled: true
timeout: PT10S
pool:
enabled: true
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 01
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
适用场景: 2-8个CAS节点,Lettuce多路复用特性使得较小的连接池即可满足需求。
5.4 连接泄漏检测与预防
连接泄漏是Redis连接池使用中最常见的问题之一。当从连接池借出的连接没有被正确归还时,就会发生连接泄漏。随着时间推移,可用的连接数越来越少,最终导致系统无法获取新的连接。
连接泄漏的常见原因
第一,异常处理不当。 这是最常见的连接泄漏原因。在Java中,如果从连接池获取连接后发生了未捕获的异常,且异常处理逻辑中没有归还连接的代码,连接就会泄漏。
java
// 错误示例(教学用途)
Jedis jedis = pool.getResource();
jedis.set("key", "value");
// 如果这里抛出异常,jedis永远不会被归还
jedis.get("key");
jedis.close(); // 这行可能不会执行1
2
3
4
5
6
2
3
4
5
6
虽然CAS框架内部已经妥善处理了连接的获取和归还,但在自定义扩展代码中(如自定义认证处理器、自定义票据组件等)直接操作Redis时,仍需要注意连接管理。
第二,长时间持有的连接。 某些业务逻辑可能长时间持有Redis连接(如执行复杂的计算后再归还),在持有期间如果发生了线程中断或超时,连接可能不会被正确归还。
第三,连接池配置不当。 如果max-active设置过小,而并发请求过多,可能导致大量请求在等待队列中堆积,形成"伪泄漏"现象。
连接泄漏的检测方法
方法一:连接池监控。 Jedis连接池提供了getNumActive()和getNumIdle()方法,可以实时查看活动连接数和空闲连接数。通过定期采集这些指标,可以发现连接数的异常增长趋势。
方法二:Redis客户端列表。 通过Redis的CLIENT LIST命令,可以查看当前所有连接的详细信息,包括连接的空闲时间、最后操作时间等。如果发现大量长时间空闲的连接,可能存在连接泄漏。
方法三:日志分析。 在CAS的日志中搜索"Could not get a resource from the pool"或"Connection is closed"等异常信息,这些通常是连接泄漏的直接表现。
连接泄漏的预防策略
第一,使用try-with-resources模式。 在Java 7及以上版本中,推荐使用try-with-resources语法来自动管理连接的生命周期:
java
// 正确示例(教学用途)
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
String result = jedis.get("key");
} // 连接会自动归还,即使发生了异常1
2
3
4
5
2
3
4
5
第二,配置连接泄漏检测。 Apache Commons Pool2(Jedis连接池的底层实现)提供了removeAbandonedOnBorrow和removeAbandonedTimeout参数,可以在借出连接时检查是否存在超时未归还的连接,并自动回收。
第三,设置合理的max-wait。 将max-wait设置为一个有限的值(如3000-5000毫秒),而不是-1(无限等待)。当连接池耗尽时,快速失败比无限等待更有利于系统的稳定性。
第四,定期重启CAS服务。 虽然这不是一个优雅的解决方案,但在无法快速定位和修复连接泄漏的情况下,定期重启CAS服务可以重置连接池状态,避免问题积累。建议在低峰期执行滚动重启。
第六章 分布式部署下的会话一致性
6.1 Spring Session + Redis会话共享
在CAS的多节点集群部署中,会话共享是保证用户体验一致性的基础。当用户通过负载均衡访问CAS集群时,不同的请求可能被分发到不同的CAS节点。如果会话数据仅存储在单个节点的内存中,用户在节点切换后就会丢失会话状态。
Spring Session是Spring生态中解决分布式会话问题的标准方案。它通过将会话数据外置到Redis等存储后端,实现了会话在多个应用节点之间的透明共享。
工作原理
Spring Session的核心机制是"过滤器替换"。在Web应用启动时,Spring Session会注册一个名为springSessionRepositoryFilter的Servlet过滤器,这个过滤器在所有请求处理之前拦截HTTP请求,执行以下操作:
- 从请求中提取会话标识(通常从名为SESSION的Cookie中获取)。
- 根据会话标识从Redis中加载会话数据。
- 将加载的会话数据包装为标准的HttpSession对象,注入到请求处理链中。
- 请求处理完成后,将会话数据的变更同步回Redis。
对于CAS应用来说,这个机制是完全透明的——CAS的代码中使用的仍然是标准的HttpServletRequest.getSession()API,Spring Session在底层自动完成了会话的分布式存储和同步。
Redis中的会话存储结构
Spring Session在Redis中的数据结构设计如下:
# 会话数据(Hash结构)
Key: spring:session:sessions:<sessionId>
Type: Hash
Fields:
creationTime -- 会话创建时间
maxInactiveInterval -- 最大不活动间隔
lastAccessedTime -- 最后访问时间
sessionAttr:KEY -- 会话属性
# 会话过期索引(String结构,用于TTL管理)
Key: spring:session:sessions:expires:<sessionId>
Type: String
Value: (空)
TTL: 与会话超时时间一致
# 索引:按会话ID查找
Key: spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:<principalName>
Type: Set
Value: <sessionId集合>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
配置要点
在CAS 5.3中,通过server.session.store-type: redis启用Spring Session的Redis存储。在CAS 6.6和7.3中,Spring Session的自动配置由cas-server-support-redis-ticket-registry模块触发,通常不需要额外配置。
但需要注意以下几点:
第一,会话Cookie的域名和路径。 在多节点部署中,所有CAS节点必须共享同一个域名(通过反向代理或DNS实现)。Spring Session生成的SESSION Cookie需要设置正确的域名和路径,确保所有节点的请求都能携带这个Cookie。
第二,会话序列化方式。 Spring Session默认使用Java序列化,这与CAS的票据序列化方式一致。如果需要切换到JSON序列化(便于跨语言访问会话数据),需要确保CAS的所有会话属性都是可JSON序列化的。
第三,Redis连接的独立性。 Spring Session使用的Redis连接与票据注册表使用的Redis连接可以共享同一个Redis实例,但建议使用不同的database编号进行隔离。这样在需要清理会话数据时,不会影响票据数据。
6.2 TGT Cookie策略
TGT(Ticket Granting Ticket)通过浏览器Cookie传递给客户端,这个Cookie通常被称为TGC(Ticket Granting Cookie)。TGC的安全策略直接关系到SSO系统的安全性。
TGC的基本属性
在CAS的配置中,TGC的安全属性通过以下参数控制:
yaml
cas:
tgc:
# 是否仅通过HTTPS传输Cookie
secure: false
# Cookie的名称(默认为TGT)
name: TGT
# Cookie的最大存活时间(秒),-1表示会话Cookie
max-age: -1
# Cookie的路径
path: /cas/
# Cookie的域名
domain: ""
# 是否启用HttpOnly标志
http-only: true
# SameSite属性
same-site: ""1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
secure: false -- 在开发环境中通常设置为false,因为开发环境可能使用HTTP。在生产环境中,应设置为true,确保TGC仅通过HTTPS传输,防止中间人攻击。
http-only: true -- 启用HttpOnly标志后,JavaScript代码无法通过document.cookie访问TGC,可以有效防止XSS(跨站脚本攻击)窃取TGC。
same-site -- SameSite属性控制Cookie在跨站请求中的发送行为。设置为"Lax"可以在防止CSRF攻击的同时,允许从外部链接跳转到CAS时的正常认证流程。
分布式环境下的TGC策略
在多节点CAS部署中,TGC策略需要特别注意以下几点:
第一,Cookie域名设置。 如果CAS通过多个域名访问(如cas1.example.com和cas2.example.com),TGC无法在域名间共享。解决方案是使用统一的入口域名(如cas.example.com),通过反向代理将请求分发到后端的多个CAS节点。
第二,Cookie路径设置。 TGC的路径应设置为CAS的上下文路径(如/cas/),确保只有CAS相关的请求才会携带TGC,减少不必要的数据传输。
第三,Cookie加密。 在CAS 7.3中,TGC支持加密存储。启用加密后,即使攻击者获取了TGC的值,也无法伪造有效的票据。
yaml
cas:
tgc:
crypto:
enabled: true
encryption:
key: "your-encryption-key"
signing:
key: "your-signing-key"1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
6.3 会话超时配置演进
CAS不同版本中,会话超时的默认值和配置方式存在显著差异。理解这些差异对于跨版本升级和运维管理至关重要。
CAS 5.3:300秒(5分钟)
yaml
server:
session:
timeout: 300
store-type: redis1
2
3
4
2
3
4
CAS 5.3的300秒会话超时是一个相对保守的设置。这个设计的初衷是:HTTP会话主要用于保护CAS的登录流程(登录页面、认证提交等),一旦用户完成登录,TGT已经存储在Redis中,HTTP会话的作用就基本结束了。
然而,300秒的超时在某些场景下会导致问题:
- 用户在登录页面停留时间过长(如填写复杂的表单),会话过期后提交会失败。
- OAuth授权流程中,用户在授权确认页面停留时间过长,会话过期导致授权失败。
- 管理员在CAS管理控制台操作时间过长,会话过期导致操作中断。
CAS 6.6:86400秒(24小时)
yaml
server:
session:
timeout: 864001
2
3
2
3
CAS 6.6将会话超时大幅延长到24小时,解决了5.3版本中的上述问题。这个变化反映了CAS使用场景的扩展——CAS不再仅仅是一个"登录中转站",而是承担了更多的交互功能(如OAuth授权流程、用户自助服务、管理控制台等),这些功能需要更长的会话有效期。
CAS 7.3:灵活配置
yaml
server:
session:
timeout: 3001
2
3
2
3
CAS 7.3的默认会话超时回到了300秒,但提供了更灵活的配置方式。架构师可以根据实际需求调整此值。
会话超时与TGT超时的关系
会话超时和TGT超时是两个独立的概念,它们之间的关系需要仔细理解:
- 会话超时(server.session.timeout): 控制HTTP会话的有效期。过期后,用户需要重新创建HTTP会话(但不一定需要重新登录,因为TGT可能仍然有效)。
- TGT超时(cas.ticket.tgt.max-time-to-live-in-seconds): 控制SSO登录状态的有效期。过期后,用户需要重新输入用户名和密码进行登录。
在实际项目中,建议的配置策略是:
- 会话超时应大于等于TGT超时,避免出现"会话有效但TGT已过期"的状态。
- 如果安全策略要求较短的会话超时,可以通过前端心跳机制保持会话活跃。
- 在OAuth场景中,会话超时应大于授权码的有效期(默认300秒),否则用户可能在授权流程中遇到会话过期问题。
6.4 会话固定保护
会话固定攻击(Session Fixation Attack)是一种常见的安全威胁。攻击者先获取一个有效的会话ID,然后诱导受害者使用这个会话ID进行认证。认证成功后,攻击者和受害者共享同一个经过认证的会话,攻击者可以利用这个会话访问受害者的资源。
CAS框架内置了会话固定保护机制。在用户成功认证后,CAS会创建一个新的会话(生成新的会话ID),并将旧会话中的属性迁移到新会话中,同时使旧会话失效。这个过程对用户是完全透明的。
在CAS + Redis的部署中,会话固定保护需要特别关注以下几点:
第一,会话迁移的原子性。 当CAS创建新会话并使旧会话失效时,这两个操作需要在Redis中原子性地完成。如果旧会话失效但新会话创建失败(如Redis网络抖动),用户将丢失会话状态。Spring Session通过Redis的事务机制(MULTI/EXEC)来保证原子性。
第二,并发请求处理。 在用户认证的瞬间,浏览器可能同时发送多个请求(如页面刷新、异步请求等)。这些请求可能同时触发会话迁移,导致竞态条件。Spring Session通过会话锁机制来避免这个问题。
第三,负载均衡的会话亲和性。 在某些部署架构中,负载均衡器可能配置了会话亲和性(Session Affinity,也称为Sticky Session)。在会话迁移后,旧的会话亲和性规则可能导致请求被路由到错误的节点。建议在CAS集群部署中使用无状态的负载均衡策略(如轮询),完全依赖Spring Session + Redis实现会话共享。
第七章 Redis高可用架构
7.1 Redis Sentinel哨兵模式
Redis Sentinel是Redis官方提供的高可用解决方案,它通过监控、通知和自动故障转移三大功能,为Redis主从架构提供了高可用保障。
架构概述
Redis Sentinel架构包含以下组件:
- Master(主节点): 处理所有的写操作和部分读操作。
- Slave(从节点): 复制Master的数据,处理部分读操作。可以部署多个Slave。
- Sentinel(哨兵节点): 监控Master和Slave的健康状态,在Master故障时自动执行故障转移。
Sentinel节点通常部署3个或以上(奇数个),通过Raft协议达成一致。当多数Sentinel认为Master不可用时,会选举一个新的Master(从Slave中选出),并通知所有客户端新的Master地址。
CAS连接Sentinel的配置
在CAS 5.3/6.6中,连接Redis Sentinel需要使用CAS专属的配置路径:
yaml
cas:
ticket:
registry:
redis:
# Sentinel模式配置
sentinel:
master: mymaster
nodes: sentinel1.example.com:26379,sentinel2.example.com:26379,sentinel3.example.com:26379
password: ""
# 基础连接配置
timeout: 2000
database: 0
useSsl: false
usePool: true
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 30001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在CAS 7.3中,Sentinel配置迁移到了Spring标准命名空间:
yaml
spring:
redis:
sentinel:
master: mymaster
nodes: sentinel1.example.com:26379,sentinel2.example.com:26379,sentinel3.example.com:26379
password: ""
timeout: 10000
database: 01
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Sentinel模式的配置要点
master名称: Sentinel配置中的master名称必须与Redis Sentinel配置中的sentinel monitor指令指定的名称一致。默认通常为"mymaster"。
nodes列表: 建议配置所有的Sentinel节点地址,而不是只配置一个。这样即使某个Sentinel节点不可用,CAS也能通过其他Sentinel节点获取当前Master的地址。
密码配置: 如果Redis实例和Sentinel节点都设置了密码,需要分别配置。在CAS 5.3/6.6中,Sentinel的密码通过cas.ticket.registry.redis.sentinel.password配置。
连接池调整: 在Sentinel模式下,连接池的max-active需要考虑故障转移期间的连接重建开销。在故障转移过程中,CAS需要与新的Master建立新的连接,此时如果连接池已满,可能导致请求失败。建议适当增大max-active的值。
7.2 Redis Cluster集群模式
Redis Cluster是Redis 3.0引入的分布式解决方案,它通过数据分片(Sharding)实现了水平扩展和高可用。
架构概述
Redis Cluster架构的核心概念包括:
- 数据分片: Redis Cluster将数据分为16384个槽(Slot),每个Master节点负责一部分槽。当客户端发送命令时,Cluster根据Key的哈希值确定命令应该发送到哪个节点。
- 高可用: 每个Master节点可以有多个Slave节点。当Master故障时,Cluster自动将从节点提升为新的Master。
- 去中心化: Redis Cluster没有单独的代理层,客户端直接与Cluster中的所有节点通信。客户端只需要知道至少一个节点的地址,就可以自动发现整个Cluster的拓扑结构。
CAS连接Cluster的配置
在CAS 7.3中,连接Redis Cluster的配置如下:
yaml
spring:
redis:
cluster:
nodes:
- redis-node1.example.com:6379
- redis-node2.example.com:6379
- redis-node3.example.com:6379
- redis-node4.example.com:6379
- redis-node5.example.com:6379
- redis-node6.example.com:6379
max-redirects: 3
timeout: 100001
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
nodes列表: 配置Cluster中所有节点的地址。客户端会通过这些节点发现整个Cluster的拓扑。实际上只需要配置部分节点即可,但建议配置所有节点以提高可用性。
max-redirects: 3 -- 当客户端向错误的节点发送命令时(如Key属于另一个节点),Cluster会返回MOVED或ASK重定向响应。max-redirects定义了客户端跟随重定向的最大次数。超过此次数后,客户端会抛出异常。
Cluster模式的注意事项
第一,CAS票据的Key设计。 Redis Cluster根据Key的哈希值进行分片。CAS的票据Key通常以"CAS_TICKET:"为前缀,后跟票据ID。由于票据ID是随机生成的,Key的哈希值分布是均匀的,可以自然地利用Cluster的分片能力。
但需要注意,如果CAS使用了Redis的批量操作(如MGET、MSET)或事务操作(MULTI/EXEC),这些操作要求所有涉及的Key位于同一个分片上(即具有相同的哈希标签)。CAS的票据操作通常是单Key操作,不存在这个问题。
第二,database参数无效。 Redis Cluster只支持database 0,spring.redis.database配置在Cluster模式下会被忽略。如果需要数据隔离,只能通过Key前缀来实现。
第三,CAS 5.3/6.6的Cluster支持。 CAS 5.3和6.6的Redis模块基于Spring Data Redis 1.8.x/2.x,对Redis Cluster的支持有限。在实际项目中,如果需要使用Redis Cluster,建议升级到CAS 7.3。
7.3 CAS连接Sentinel/Cluster的配置方式
综合前面的分析,以下是不同CAS版本连接不同Redis部署模式的配置方式总结。
CAS 5.3 配置方式
yaml
# 直连模式
cas:
ticket:
registry:
redis:
host: redis.example.com
port: 6379
timeout: 2000
database: 0
usePool: true
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: -1
# Sentinel模式
cas:
ticket:
registry:
redis:
sentinel:
master: mymaster
nodes: sentinel1:26379,sentinel2:26379,sentinel3:26379
timeout: 2000
database: 0
usePool: true1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CAS 6.6 配置方式
yaml
# 直连模式
cas:
ticket:
registry:
type: REDIS
redis:
host: redis.example.com
port: 6379
timeout: 5000
database: 0
pool:
max-active: 50
max-idle: 20
min-idle: 10
max-wait: 3000
cleaner:
enabled: true
repeat-interval: 1800
connection-pool:
test-on-borrow: true
test-while-idle: true
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 3000001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CAS 7.3 配置方式
yaml
# 直连模式
spring:
redis:
host: redis.example.com
port: 6379
database: 0
timeout: 10000
cas:
ticket:
registry:
redis:
enabled: true
timeout: PT10S
pool:
enabled: true
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
# Cluster模式
spring:
redis:
cluster:
nodes:
- node1:6379
- node2:6379
- node3:6379
max-redirects: 3
timeout: 100001
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
7.4 故障转移与恢复策略
在Redis高可用架构中,故障转移和恢复是保障系统持续可用性的关键环节。本节将分析不同故障场景下的应对策略。
场景一:Redis Master故障(Sentinel模式)
故障检测: Sentinel节点通过定期向Master发送PING命令来检测其健康状态。如果Master在down-after-milliseconds时间内没有响应,Sentinel会将其标记为"主观下线"(SDOWN)。当超过半数的Sentinel都将Master标记为SDOWN时,Master被标记为"客观下线"(ODOWN),触发故障转移。
故障转移过程:
- Sentinel通过Raft协议选举出一个Leader Sentinel,由其负责执行故障转移。
- Leader Sentinel从Slave中选出一个新的Master(选择标准包括数据新鲜度、复制偏移量、优先级等)。
- Leader Sentinel对选中的Slave执行
SLAVEOF NO ONE命令,将其提升为新的Master。 - Leader Sentinel通知其他Slave复制新的Master。
- Sentinel更新客户端(CAS)关于新Master地址的映射。
对CAS的影响: 在故障转移期间(通常持续数秒到数十秒),CAS的Redis操作可能会失败。CAS需要能够容忍这种短暂的服务中断:
- 票据写入失败:用户登录请求会失败,前端应显示友好的错误信息并引导用户重试。
- 票据查询失败:业务应用的ST验证请求会失败,业务应用应将用户重定向到CAS登录页面。
- 会话查询失败:Spring Session无法从Redis加载会话数据,会创建一个新的空会话。
恢复策略:
- CAS的Redis客户端(Jedis/Lettuce)内置了Sentinel地址发现机制,会自动感知Master地址的变化。
- 在故障转移完成后,CAS的后续请求会自动路由到新的Master,无需人工干预。
- 建议在负载均衡器上配置健康检查,在Redis故障转移期间暂时将CAS节点从负载均衡池中移除,避免用户遇到错误。
场景二:Redis节点网络分区
问题描述: 当CAS节点与Redis之间的网络出现分区时,CAS可能无法访问Redis,但Redis本身仍在正常运行。
影响分析:
- CAS无法创建新的TGT和ST,所有登录请求失败。
- CAS无法验证已有的ST,业务应用的回调验证失败。
- Spring Session无法加载会话数据,用户的交互状态丢失。
应对策略:
- 在网络层面,确保CAS与Redis之间有多条网络路径(如双网卡、多可用区部署)。
- 在应用层面,配置合理的连接超时和重试策略。
- 在运维层面,建立网络分区的监控和告警机制,确保问题能被快速发现和处理。
场景三:Redis内存满
问题描述: 当Redis的内存使用达到maxmemory限制时,Redis会根据配置的淘汰策略(eviction policy)开始删除Key。
对CAS的影响: 如果票据Key被淘汰,用户会被强制登出。如果会话Key被淘汰,用户的交互状态会丢失。
预防措施:
- 配置合理的
maxmemory值,确保Redis有足够的内存存储所有活跃票据和会话。 - 设置合适的淘汰策略。推荐使用
allkeys-lru(在所有Key中使用LRU算法淘汰),而不是volatile-lru(仅在设置了TTL的Key中使用LRU算法)。 - 监控Redis的内存使用情况,设置告警阈值(如80%时告警,90%时严重告警)。
- 确保CAS的Cleaner任务正常运行,及时清理过期票据。
第八章 票据清理策略
8.1 票据过期机制
CAS票据的过期管理涉及两个层面:应用层面的过期检测和存储层面的自动清理。
应用层面的过期检测
CAS框架在每次票据操作(查询、更新、删除)时,都会检查票据是否已经过期。如果票据已经过期,CAS会将其从注册表中删除,并返回相应的过期状态。
这种"惰性删除"(Lazy Deletion)策略的优点是:
- 无需额外的清理线程: 过期检测嵌入在正常的业务操作中,不需要额外的系统开销。
- 精确的过期控制: 票据的过期判断基于其创建时间和配置的TTL,精度可以达到毫秒级。
缺点是:
- 过期票据不能被及时清理: 如果某个票据过期后长时间没有被访问,它会一直占用存储空间。
- 存储空间浪费: 在高并发场景下,大量短生命周期的ST可能在Redis中积累。
存储层面的自动清理
Redis的Key过期机制为票据清理提供了存储层面的保障。当CAS将票据写入Redis时,会为每个Key设置TTL(Time To Live)。Redis会在TTL到期后自动删除对应的Key。
Redis的过期Key清理采用以下策略的组合:
- 惰性删除: 当客户端访问一个Key时,Redis先检查其是否过期,如果过期则删除并返回空。
- 定期删除: Redis每秒执行10次(默认)的定期扫描,每次随机检查一部分设置了TTL的Key,删除已过期的Key。
这种组合策略在性能和内存使用之间取得了良好的平衡。但在极端情况下(大量Key同时过期),定期删除可能跟不上过期速度,导致Redis内存使用短暂上升。
CAS 5.3/6.6的TTL设置
在CAS 5.3和6.6中,票据的TTL与其过期时间一致:
- TGT的TTL: 默认为7200秒(2小时),可通过
cas.ticket.tgt.max-time-to-live-in-seconds配置。 - ST的TTL: 默认为10秒,可通过
cas.ticket.st.time-to-kill-in-seconds配置。 - PGT的TTL: 默认与TGT一致。
- PT的TTL: 默认与ST一致。
CAS 7.3的TTL设置
CAS 7.3的TTL配置方式与5.3/6.6基本一致,但配置属性的命名风格从camelCase变更为kebab-case:
yaml
cas:
ticket:
tgt:
max-time-to-live-in-seconds: 7200
st:
time-to-kill-in-seconds: 101
2
3
4
5
6
2
3
4
5
6
8.2 Redis清理任务(Cleaner)
CAS 6.6引入了专门的Redis清理任务(Cleaner),用于主动清理Redis中的过期票据。虽然Redis的TTL机制已经提供了基础的过期清理能力,但CAS Cleaner提供了额外的保障。
Cleaner的工作流程
CAS Cleaner是一个基于Spring的定时任务(Scheduled Task),其工作流程如下:
- 扫描票据Key: Cleaner通过Redis的SCAN命令遍历所有以"CAS_TICKET:"为前缀的Key。
- 检查过期状态: 对于每个Key,Cleaner读取其对应的票据对象,检查票据是否已经过期。
- 删除过期票据: 如果票据已过期,Cleaner从Redis中删除对应的Key。
- 更新统计信息: Cleaner记录清理的票据数量和类型,用于监控和告警。
Cleaner的配置参数
yaml
cas:
ticket:
registry:
redis:
cleaner:
enabled: true
repeat-interval: 18001
2
3
4
5
6
7
2
3
4
5
6
7
enabled: true -- 启用清理任务。在CAS 6.6及以上版本中,建议始终启用此功能。
repeat-interval: 1800 -- 清理任务的执行间隔为1800秒(30分钟)。这个值的选择需要考虑:
- ST的TTL通常为10秒,30分钟的清理间隔意味着ST主要依赖Redis的TTL机制进行清理,Cleaner主要处理TGT等长生命周期票据。
- 如果Redis内存紧张,可以减小清理间隔(如600秒),增加主动清理的频率。
- 清理任务本身会消耗一定的CPU和网络资源,不宜设置过小的间隔。
Cleaner与TTL的协作关系
CAS Cleaner和Redis TTL是互补关系,而非替代关系:
- Redis TTL: 提供基础的、自动的过期清理,是第一道防线。TTL的清理是异步的、概率性的,存在一定的延迟。
- CAS Cleaner: 提供主动的、确定性的清理,是第二道防线。Cleaner可以确保所有过期票据都被及时清理,同时收集清理统计信息。
在实际项目中,两者的协作关系如下:
- 票据过期后,Redis TTL会在一定时间内(通常数秒到数分钟)自动删除对应的Key。
- CAS Cleaner定期扫描并删除可能遗漏的过期票据(如TTL清理延迟的Key)。
- CAS Cleaner的统计信息用于监控Redis中的票据存储状态,及时发现异常情况。
8.3 内存优化与Key设计
在Redis中存储CAS票据时,合理的Key设计和内存优化策略可以显著降低Redis的内存占用,提高系统的整体性能。
Key命名规范
CAS默认的Key命名规则为CAS_TICKET:<ticketId>,其中<ticketId>是票据的唯一标识(如"TGT-1-abc123..."或"ST-1-def456...")。
在实际项目中,可以考虑以下Key设计优化:
第一,添加环境前缀。 在多环境(开发、测试、生产)共享同一个Redis实例时,添加环境前缀可以避免Key冲突:
PROD_CAS_TICKET:TGT-1-abc123...
DEV_CAS_TICKET:TGT-1-abc123...1
2
2
第二,添加票据类型前缀。 将票据类型编码到Key中,便于按类型进行批量操作和监控:
CAS_TICKET:TGT:TGT-1-abc123...
CAS_TICKET:ST:ST-1-def456...1
2
2
第三,Key长度控制。 Redis的Key越长,占用的内存越大。CAS的票据ID通常较长(包含随机字符串),但Key的总长度应控制在合理范围内。建议Key的总长度不超过100字节。
序列化优化
CAS默认使用JDK序列化将票据对象存储到Redis中。JDK序列化的优点是简单直接,不需要额外的配置,但其缺点是序列化后的字节体积较大。
JDK序列化后的票据对象通常包含:
- 类的全限定名(如
org.apereo.cas.ticket.TicketGrantingTicketImpl) - 序列化版本UID
- 对象的完整字段数据
- 引用类型的嵌套序列化
一个典型的TGT对象经过JDK序列化后,可能占用1-5KB的存储空间。在高并发场景下,大量的TGT和ST对象可能占用数百MB甚至数GB的Redis内存。
优化序列化体积的方法包括:
- 使用JSON序列化: JSON序列化后的数据体积通常比JDK序列化小30%-50%。但需要注意,JSON序列化需要确保所有字段都是可JSON序列化的。
- 使用Protobuf或MessagePack: 这些二进制序列化格式的体积更小,解析速度更快。但需要额外的序列化Schema定义和代码生成。
- 字段裁剪: 分析票据对象中哪些字段是存储必需的,哪些字段可以在查询时从其他数据源获取。通过自定义序列化逻辑,只存储必需的字段。
在实际项目中,序列化优化需要权衡开发成本和收益。对于大多数企业级部署,JDK序列化的内存开销是可以接受的。只有在Redis内存成为瓶颈时,才需要考虑更高效的序列化方案。
内存使用估算
以下是在不同用户规模下的Redis内存使用估算(基于JDK序列化):
| 用户规模 | 活跃TGT数量 | 每TGT关联ST数 | TGT内存占用 | ST内存占用 | 总内存占用 |
|---|---|---|---|---|---|
| 1万 | 5000 | 3 | 15MB | 0.15MB | ~15MB |
| 10万 | 30000 | 3 | 90MB | 0.9MB | ~91MB |
| 100万 | 100000 | 3 | 300MB | 3MB | ~303MB |
| 1000万 | 300000 | 3 | 900MB | 9MB | ~909MB |
注:以上估算基于以下假设——TGT平均3KB,ST平均0.5KB,ST的TTL为10秒(大部分ST在扫描时已过期)。
从估算结果可以看出,CAS票据的Redis内存占用主要取决于活跃TGT的数量。ST由于其短生命周期(10秒),对内存的占用可以忽略不计。
8.4 监控与告警
完善的监控和告警体系是保障CAS + Redis系统稳定运行的关键。本节将介绍需要监控的核心指标和告警策略。
Redis核心监控指标
内存相关指标:
used_memory:Redis已使用的内存量。需要关注其增长趋势,设置告警阈值(如达到maxmemory的80%)。used_memory_peak:Redis的历史最大内存使用量。用于评估是否需要调整maxmemory配置。evicted_keys:被淘汰的Key数量。如果此值持续增长,说明Redis内存不足,需要扩容或优化Key设计。
连接相关指标:
connected_clients:当前连接的客户端数量。CAS节点的连接数应与连接池配置一致。blocked_clients:被阻塞的客户端数量(通常由BLPOP等阻塞命令引起)。CAS正常操作不应产生阻塞客户端。rejected_connections:被拒绝的连接数。如果此值增长,说明Redis的maxclients配置需要调整。
性能相关指标:
instantaneous_ops_per_sec:每秒执行的命令数。用于评估Redis的负载水平。keyspace_hits/keyspace_misses:Key的命中和未命中次数。命中率应保持在95%以上。latest_fork_usec:最近一次fork操作(RDB快照或AOF重写)的耗时。如果此值过大,说明Redis在执行持久化时可能出现延迟。
票据相关指标(通过CAS Cleaner收集):
- 活跃TGT数量:反映当前在线用户数。
- 活跃ST数量:反映当前的认证请求频率。
- 清理的过期票据数量:反映票据的过期速率。
告警策略
| 告警级别 | 指标 | 阈值 | 处理建议 |
|---|---|---|---|
| P0(紧急) | Redis可用性 | 连接失败 | 立即检查Redis服务状态,启动故障转移 |
| P0(紧急) | evicted_keys | 持续增长 | 紧急扩容Redis内存,检查是否有异常的大量票据创建 |
| P1(严重) | used_memory | > 80% maxmemory | 评估扩容需求,检查Cleaner是否正常运行 |
| P1(严重) | connected_clients | > 80% maxclients | 检查CAS连接池配置,评估是否需要增加maxclients |
| P2(警告) | keyspace_misses | 命中率 < 95% | 检查是否有大量无效的票据查询 |
| P2(警告) | latest_fork_usec | > 1000ms | 评估RDB/AOF配置,考虑调整save策略 |
| P3(通知) | 活跃TGT数量 | 异常增长 | 检查是否有异常的登录行为(如暴力破解) |
| P3(通知) | 清理票据数量 | 异常增长 | 检查票据TTL配置是否合理 |
监控工具推荐
- Redis命令行工具:
redis-cli info命令可以获取Redis的完整运行状态信息。 - RedisInsight: Redis官方的可视化监控工具,提供丰富的仪表盘和告警功能。
- Prometheus + Grafana: 通过Redis Exporter采集Redis指标,在Grafana中创建可视化仪表盘。
- CAS Actuator: CAS的
/status/health端点可以查看票据注册表的健康状态。
总结与展望
本文从CAS票据注册表的架构概述出发,深入解析了CAS 5.3、6.6、7.3三个大版本中Redis票据注册表的配置演进、连接池调优、会话一致性保障、高可用部署以及票据清理策略。
回顾CAS Redis配置的演进历程,我们可以看到以下趋势:
配置标准化: 从CAS 5.3的专属命名空间(cas.ticket.registry.redis)到CAS 7.3的Spring标准命名空间(spring.redis),配置方式越来越标准化,与Spring Boot生态的融合越来越深入。
功能精细化: 从CAS 5.3的基础连接池配置,到CAS 6.6的增强连接池参数和Cleaner任务,再到CAS 7.3的enableRedisSearch和Duration格式超时,功能越来越精细和灵活。
技术栈现代化: 从CAS 5.3的Java 8 + Spring Boot 2.x + Jedis,到CAS 6.6的Java 11 + Spring Boot 2.7.x,再到CAS 7.3的Java 21 + Spring Boot 3.5.x + Lettuce,技术栈持续向现代化演进。
架构简化: 从CAS 5.3需要显式锁定Spring Data Redis和Jedis版本,到CAS 7.3由BOM统一管理所有依赖版本,架构的复杂度持续降低。
对于正在进行CAS分布式部署的企业级项目,我们给出以下实践建议:
新项目优先选择CAS 7.3。 CAS 7.3提供了最现代化的技术栈和最简洁的配置方式,长期来看维护成本最低。
Redis Sentinel适合中小规模部署。 对于大多数企业级SSO场景,Redis Sentinel提供的自动故障转移能力已经足够。
Redis Cluster适合大规模部署。 当单节点Redis无法满足性能需求时,再考虑引入Redis Cluster。
始终启用Cleaner任务。 虽然Redis TTL提供了基础的过期清理,但Cleaner任务提供了额外的保障和监控能力。
建立完善的监控体系。 Redis的内存使用、连接数、命中率等指标需要纳入统一的监控平台,并配置合理的告警阈值。
重视版本兼容性。 CAS的版本升级需要充分测试,特别是Redis相关模块的版本兼容性。建议在升级前在测试环境中进行完整的回归测试。
配置与代码分离。 将Redis的连接参数(地址、密码等)通过环境变量或配置中心注入,避免将敏感信息硬编码在配置文件中。
定期评估连接池参数。 随着业务规模的变化,连接池参数可能需要调整。建议每季度评估一次连接池的使用情况,根据实际负载进行优化。
CAS + Redis的组合为企业级SSO系统提供了成熟、可靠、高性能的分布式票据存储方案。通过深入理解其架构原理和配置细节,架构师和开发者可以构建出满足业务需求的高可用SSO平台。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc。