0%

ZooKeeper API 实战:从原生客户端到主流框架

ZooKeeper 提供了原生 Java API 用于分布式协调,但原生 API 存在会话重连、监听器一次性触发等痛点。实际开发中,更推荐使用封装完善的 ZkClient 或 Curator 框架。以下详细介绍三类客户端的核心用法与最佳实践。

原生 ZooKeeper API

原生 API 是 ZooKeeper 官方提供的基础接口,适合理解底层原理,但需手动处理诸多细节(如异常、会话维护)。

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>

核心操作示例

(1)创建连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private ZooKeeper zkClient;
private final String connectString = "localhost:2181"; // 集群地址用逗号分隔
private final int sessionTimeout = 3000; // 会话超时时间(毫秒)

@Before
public void init() throws IOException {
// 回调监听器:处理连接状态变化(如连接建立、断开)
Watcher watcher = event -> {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("连接建立成功");
}
};
// 初始化连接(异步操作,需等待回调确认连接成功)
zkClient = new ZooKeeper(connectString, sessionTimeout, watcher);
}
(2)创建节点
阅读全文 »

ZooKeeper 核心应用场景:分布式系统的协同利器

ZooKeeper 凭借其强一致性、高可用的特性,成为分布式系统中解决协同问题的 “瑞士军刀”。从服务注册发现到分布式锁,从配置管理到集群选举,其应用场景覆盖了分布式架构的诸多核心需求。以下详细解析各场景的实现原理与实践方式。

服务注册与发现(注册中心)

在微服务架构中,服务提供者与消费者需动态感知彼此的存在,ZooKeeper 可通过临时节点和监听机制实现这一功能。

实现原理

  1. 服务注册:服务提供者启动时,在 ZooKeeper 的指定路径(如 /services/user-service)下创建临时节点,节点数据为服务地址(如 192.168.1.100:8080)。
    • 临时节点特性保证:服务下线(会话失效)时,节点自动删除,无需手动注销。
  2. 服务发现:服务消费者启动时,通过 getChildren 获取 /services/user-service 下的所有子节点(即所有服务提供者地址),并监听子节点变化
    • 当服务提供者上下线时,子节点增删触发监听事件,消费者实时更新本地缓存的服务列表。
  3. 负载均衡:消费者从服务列表中通过轮询、随机等算法选择一个地址调用,无需依赖第三方组件。

与 Eureka 的对比

特性 ZooKeeper(CP) Eureka(AP)
一致性 强一致性(Leader 选举期间不可用) 最终一致性(优先保证可用性)
可用性 集群半数以上节点故障时不可用 允许部分节点故障,仍提供服务
适用场景 对一致性要求高的服务(如金融交易) 对可用性要求高的服务(如电商)

分布式锁

分布式系统中,多节点竞争同一资源(如库存扣减、订单创建)时,需通过分布式锁保证操作的原子性。ZooKeeper 基于临时有序节点和监听机制实现高效锁控制。

实现原理(独占锁)

  1. 抢锁:所有节点在 /lock 路径下创建临时有序节点(如 /lock/req-0000000001)。
  2. 判断锁权:节点创建后,获取 /lock 下的所有子节点,若自身是序号最小的节点,则获取锁
  3. 等待锁:若不是最小节点,监听前一个节点的删除事件(如 /lock/req-0000000000),前节点释放锁后触发事件,重复步骤 2。
  4. 释放锁:完成操作后删除自身节点,或会话失效时临时节点自动删除,释放锁资源。

优势

  • 避免死锁:临时节点特性保证节点故障时锁自动释放;
  • 公平锁:通过有序节点实现 “先到先得”,避免饥饿问题。

工具推荐

Curator 框架已封装分布式锁实现(InterProcessMutex),无需重复开发:

阅读全文 »

Spring 中获取当前 HttpServletRequest 的方法详解

在 Spring(尤其是 Spring MVC)应用中,经常需要在非 Controller 层(如 Service、工具类)获取当前请求的 HttpServletRequest 对象(如获取请求头、客户端 IP、参数等)。核心解决方案是利用 RequestContextHolder 工具类,它通过 ThreadLocal 存储当前请求上下文,确保线程安全。从 “原理→使用方式→注意事项→扩展场景” 四个维度,彻底讲透如何安全、高效地获取 HttpServletRequest

核心原理:RequestContextHolder 与 ThreadLocal

RequestContextHolder 是 Spring 提供的请求上下文持有者,其底层通过 ThreadLocal 存储当前线程的请求上下文(RequestAttributes),确保每个线程只能访问自己的请求对象,避免多线程环境下的资源竞争。

1. 关键类与接口关系

graph LR
    A[RequestContextHolder] --> B[ThreadLocal]
    B --> C[ServletRequestAttributes]
    C --> D[HttpServletRequest]
    C --> E[HttpServletResponse]
  • RequestContextHolder:静态工具类,提供 currentRequestAttributes() 方法获取当前线程的 RequestAttributes
  • RequestAttributes:请求属性接口,定义了获取请求 / 会话属性的方法,其实现类 ServletRequestAttributes 封装了 HttpServletRequestHttpServletResponse
  • ThreadLocal:线程局部变量,确保每个线程的 RequestAttributes 独立存储,互不干扰。

2. 上下文存储时机

Spring MVC 在请求进入时(DispatcherServlet 处理请求的初期),会自动将当前 HttpServletRequest 封装为 ServletRequestAttributes,并通过 RequestContextHolder.setRequestAttributes() 存入 ThreadLocal;请求结束后,会清除 ThreadLocal 中的数据,避免内存泄漏。

核心流程简化:

阅读全文 »

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 的值(分词后的词项或完整值)存入倒排索引,使其可被查询。只有索引的字段才能作为查询条件。

阅读全文 »