0%

Session 共享配置:Tomcat 集群与 Redis 存储方案

在分布式系统中,Session 共享是核心问题之一。当用户请求被负载均衡分发到不同服务器时,若 Session 未共享,会导致用户反复登录、状态丢失。本文详细讲解两种主流的 Session 共享方案:Tomcat 原生集群复制Redis 集中存储,分析其原理、配置方法及适用场景。

Tomcat 原生集群:基于 Session 复制实现共享

Tomcat 通过内置的集群组件(SimpleTcpCluster)实现 Session 自动复制,多个 Tomcat 节点组成集群,当某节点的 Session 发生变化时,会同步到其他节点。

核心配置步骤

(1)修改server.xml,启用集群功能

EngineHost标签内添加集群配置(所有 Tomcat 节点配置一致):

1
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<!-- tomcat集群节点
channelSendOptions 可以设置为2、4、8、10
- 2 确认发送
- 4 同步发送
- 8 异步发送
- 10 异步发送,且通过加上Acknowledge来提供可用性
-->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">

<!-- Manager管理集群的session信息
提供了两种Manager:BackupManager和DeltaManager
- BackupManager:集群下的所有session放在一个备份节点,集群下所有节点都访问此备份节点
- DeltaManager:集群下某一节点生成、改动的session,将复制到其他节点,默认使用该种
每个节点部署的应用一样则使用DeltaManager;每个节点部署的应用不一样则使用BackupManager

expireSessionsOnShutdown 设置为true时,一个节点关闭,将导致集群下所有session失效
notifyListenersOnReplication 集群下节点间session复制、删除操作,是否通知session listeners
maxInactiveInterval 集群下Session的有效时间(单位:s),maxInactiveInterval内未活动的Session,将被Tomcat回收。默认值为1800(30min)
-->
<Manager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false" <!-- 节点关闭时不失效其他节点的Session -->
notifyListenersOnReplication="true"/> <!-- 复制时通知Session监听器 -->

<!-- 节点通信通道 -->
<Channel className="org.apache.catalina.tribes.group.GroupChannel">

<!-- Membership(组播方式) 维护集群的可用节点列表,可以检查到新增的节点,也可以检查到没有心跳的节点
address 组播地址
port 组播端口
frequency 心跳的间隔时间,单位是ms,默认500
dropTime 在dropTime时间内没有收到某个节点的心跳,则将该节点删除
-->
<Membership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4" <!-- 组播地址集群内统一-->
port="45564" <!-- 组播端口 -->
frequency="500" <!-- 心跳间隔(ms) -->
dropTime="3000"/> <!-- 超时未收到心跳则移除节点(ms) -->


<!-- Receiver 接收器,负责接收消息,分为阻塞式BioReceiver和非阻塞式NioReceiver
address 接收消息的地址
port 接收消息的端口
autoBind 端口的变化区间,如果port是4000,autoBind为100,则接收器将在4000-4099间取一个端口,进行监听
selectorTimeout NioReceiver轮询的超时时间
maxThreads 线程池的最大线程数
-->
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto" <!-- 自动获取本机IP -->
port="4000" <!-- 接收端口(不同节点需不同,如4000、4001) -->
autoBind="100" <!-- 端口冲突时自动偏移范围(4000-4099) -->
selectorTimeout="5000"
maxThreads="6"/>


<!-- Sender 发送器,负责发送消息
Sender内嵌了Transport组件,Transport真正负责发送消息

Transport分为两种:PooledParallelSender非阻塞式和PooledMultiSender阻塞式
-->
<Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
<Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
</Sender>


<!-- Interceptor 集群的拦截器(增强可靠性)
TcpFailureDetector 网络、系统比较繁忙时,Membership可能无法及时更新可用节点列表,此时TcpFailureDetector可以拦截到某个节点关闭的信息,并尝试通过TCP连接此节点,以确保此节点真正的关闭,从而更新可用节点列表
-->
<Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
<!-- MessageDispatch15Interceptor 查看Cluster组件发送消息的方式channelSendOptions是否设置为8,如果为8,MessageDispatch15Interceptor先将等待发送的消息进行排队,然后将排好队的消息转给Sender -->
<Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>
</Channel>

<!-- 集群部署器(同步应用部署) -->
<!-- Deployer 部署
tempDir 应用部署的临时目录,与其他实例同步来的应用文件会先存放在这里
deployDir 应用的部署目录,与Host配置的appBase一致
watchDir 监控目录,当watchEnabled为true时,该目录下应用的变化会同步到集群中的各个实例
-->
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="true"/>


<!-- ClusterListener 监听器,监听Cluster组件接收的消息,使用DeltaManager时,Cluster接收的信息通过ClusterSessionListener传递给DeltaManager -->
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>

(2)为每个 Tomcat 节点设置唯一标识(jvmRoute

server.xmlEngine标签中添加jvmRoute,区分不同节点:

阅读全文 »

Lucene 中 Field 域的核心属性解析

在 Lucene 中,Field(域)是文档(Document)的基本组成单元,每个 Field 的行为由其核心属性决定。这些属性包括是否分词(tokenized)是否索引(indexed)是否存储(stored),它们直接影响索引构建、查询效率和结果展示。以下是对这些属性的详细说明:

是否分词(tokenized)

分词(tokenized)指将 Field 的原始值拆分为多个词项(Term)的过程,目的是为后续的全文检索提供可匹配的单元。

是(tokenized = true)

  • 行为:Field 的值会被分词器(Analyzer)处理,拆分为多个词项(如 “Lucene 搜索引擎”→“Lucene”“搜索引擎”)。
  • 适用场景:需要进行全文检索的字段,其内容通常是长文本,且用户可能通过其中的任意关键词查询。
    • 示例:商品名称、文章正文、描述信息等。
    • 原因:这些字段需要支持 “包含某个关键词” 的模糊查询,分词后才能建立细粒度的倒排索引。

否(tokenized = false)

  • 行为:Field 的值不会被拆分,整个值作为一个完整的词项(Term)。
  • 适用场景:需要精确匹配的字段,其值通常是唯一标识或固定格式的字符串。
    • 示例:商品 ID(如 “P10086”)、订单号、身份证号、URL 等。
    • 原因:这些字段的查询通常是 “完全匹配”(如 “查询 ID 为 P10086 的商品”),无需分词。

是否索引(indexed)

索引(indexed)指将 Field 的值(分词后的词项或完整值)存入倒排索引,使其可被查询。只有索引的字段才能作为查询条件。

阅读全文 »

Luke:Lucene 索引可视化工具详解

Luke 是一款专为 Lucene 设计的可视化 GUI 工具,由 Apache 社区维护(目前由第三方开发者持续更新),旨在帮助开发者便捷地查看、诊断和管理 Lucene 索引。无论是调试分词效果、分析索引结构,还是执行查询测试,Luke 都能提供直观的操作界面,是 Lucene 开发和运维的得力助手。

Luke 的核心功能

Luke 围绕 Lucene 索引的 “查看、检索、维护” 三大场景设计,核心功能如下:

索引结构可视化

  • 查看索引库信息:显示索引库的路径、段(Segment)数量、文档总数、索引版本等元数据。
  • 浏览文档与字段:以列表形式展示索引中的所有文档,可查看每个文档的字段(Field)名称、值及属性(是否分词、索引、存储)。
  • 分析段结构:查看每个段的详细信息(如文档数、删除文档数、创建时间),支持段合并操作。

字段与分词分析

  • 字段内容预览:针对任意字段,可查看其在不同文档中的值,快速定位异常数据(如分词错误、格式问题)。
  • 分词器测试:输入文本后,选择指定分词器(如 StandardAnalyzer、中文结巴分词),实时查看分词结果(词项、偏移量、类型),验证分词逻辑是否符合预期。
  • 词项统计:查看指定字段的所有词项(Term)及其文档频率(DF)、词频(TF),帮助优化索引质量(如识别高频无意义词)。

索引检索与查询测试

  • 执行各类查询:支持输入 Lucene 查询语法(如 title:luceneprice:[100 TO 200]),执行后可查看匹配的文档、得分(Score)及匹配详情。
  • 查询解析可视化:将查询语句解析为 Lucene 的查询树(如 BooleanQuery 包含的子查询),帮助理解复杂查询的执行逻辑。
  • 排序与过滤:支持按字段排序查询结果,或添加过滤条件(如 publishTime > 2023-01-01),模拟实际检索场景。

索引维护操作

阅读全文 »

InnoDB 索引页文件结构:深入理解数据存储的基本单元

InnoDB 存储引擎中,页(Page)是数据存储的基本单位(默认大小 16KB)。所有索引和数据都以页为单位组织,查询时需先定位到记录所在的页,再从页内查找具体记录。索引页的结构设计直接影响数据存取效率,其内部由 7 个部分组成,共同实现数据的有序存储、快速查找和完整性校验。

索引页的整体结构

InnoDB 索引页(B + 树节点)的结构可分为 7 个部分,按顺序依次为:

存储结构

各部分的大小和核心作用如下表:

结构部分 固定大小 核心作用
File Header 38 字节 记录页的基本信息(如页编号、前后页指针、页类型),用于页的定位和关联。
Page Header 56 字节 记录页的状态信息(如记录数、索引层级、插入方向),用于页内空间和状态管理。
Infimum & Supremum Record 固定大小 虚拟记录,限定页内记录的边界(最小值和最大值)。
User Records 动态变化 实际存储用户数据的记录,随插入 / 删除操作动态变化。
Free Space 动态变化 页内未使用的空闲空间,以链表形式管理,供新记录插入复用。
Page Directory 动态变化 页目录(类似 “索引的索引”),存储记录的相对位置,加速页内记录查找。
File Trailer 8 字节 校验页的完整性,确保数据从内存刷写到磁盘时未损坏。

各部分详细解析

File Header(文件头):页的 “身份证”

File Header 是页的元数据区,固定 38 字节,包含 8 个关键字段,用于标识页的身份、关联关系和类型:

阅读全文 »

JMM:Java 内存模型的底层逻辑与实践

CPU 的运算速度远超主内存的读写能力,因此现代计算机引入了高速缓存(Cache)作为缓冲:运算时先将数据从主内存复制到缓存,运算结束后再同步回主内存,避免处理器等待。这在单线程下高效且无问题,但多线程环境中,多核 CPU 的缓存独立性会导致缓存一致性问题—— 不同线程对共享变量的修改可能无法及时感知。JMM(Java Memory Model,Java 内存模型) 正是为解决这一问题而设计的规范。

JMM 的核心目标与内存模型

JMM 的核心目标是定义线程对共享变量的访问规则,即虚拟机如何将变量从主内存加载到工作内存、如何从工作内存同步回主内存的底层细节。其核心价值在于:

  • 解决多线程环境下的内存可见性(一个线程的修改对其他线程可见)、原子性(操作不可分割)和有序性(指令执行顺序)问题;
  • 屏蔽不同硬件和操作系统的内存访问差异,保证 Java 程序在多平台下的一致性。

内存模型的结构

JMM 定义了以下内存交互角色:

  • 主内存:所有线程共享的内存区域,存储共享变量(实例变量、静态变量、数组元素等);
  • 工作内存:每个线程独有的内存区域,存储共享变量的副本(主内存的拷贝)。

线程对共享变量的操作必须遵循以下规则:

  • 线程读写共享变量时,需先将变量从主内存复制到工作内存,操作完成后再同步回主内存;
  • 线程间无法直接访问对方的工作内存,变量传递必须通过主内存。

注意:局部变量、方法参数是线程私有变量,不存在多线程竞争,因此不在 JMM 的管理范围内。

JMM

缓存一致性与指令重排序

多线程问题的根源在于:

  • 缓存一致性:线程修改工作内存后,若未及时同步到主内存,其他线程可能读取到旧值;
  • 指令重排序:编译器、处理器为优化性能,会在不影响单线程语义的前提下调整指令顺序,可能破坏多线程的执行逻辑。
阅读全文 »