ES深度分页
解决方案
1. 滚动搜索(Scroll API)
- 原理:滚动搜索通过创建一个 “快照” 来避免深度分页问题。在第一次查询时,ES 会创建一个滚动上下文,并返回一个滚动 ID,后续的查询使用这个滚动 ID 来获取下一页的结果,直到滚动上下文过期或没有更多结果为止。
- 示例(Python):
收起
python
from elasticsearch import Elasticsearch
# 连接到 Elasticsearch
es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
# 定义索引名称
index_name = 'your_index'
# 每页文档数量
page_size = 10
# 第一次查询,创建滚动上下文
result = es.search(
index=index_name,
body={"query": {"match_all": {}}},
scroll='1m', # 滚动上下文有效期为 1 分钟
size=page_size
)
# 获取滚动 ID
scroll_id = result['_scroll_id']
# 处理第一页结果
for hit in result['hits']['hits']:
print(hit['_source'])
# 循环获取后续页结果
while len(result['hits']['hits']):
result = es.scroll(scroll_id=scroll_id, scroll='1m')
scroll_id = result['_scroll_id']
for hit in result['hits']['hits']:
print(hit['_source'])
# 清除滚动上下文
es.clear_scroll(scroll_id=scroll_id)
- 适用场景:适用于需要一次性获取大量数据的场景,如数据导出、批量处理等。但滚动搜索不适合实时查询,因为滚动上下文是一个快照,在滚动过程中对索引的更新不会反映在结果中。
2. Search After
- 原理:search_after 参数通过记录上一页最后一条文档的排序值,在后续查询中从该排序值之后继续获取结果,避免了跳过大量记录的操作,从而提高了性能。
- 示例(Python):
收起
python
from elasticsearch import Elasticsearch
# 连接到 Elasticsearch
es = Elasticsearch([{'host': 'localhost', 'port': 9200}])
# 定义索引名称
index_name = 'your_index'
# 每页文档数量
page_size = 10
# 第一次查询
query = {
"query": {
"match_all": {}
},
"size": page_size,
"sort": [{"_id": "asc"}] # 按 _id 升序排序
}
result = es.search(index=index_name, body=query)
# 处理第一页结果
for hit in result['hits']['hits']:
print(hit['_source'])
# 获取最后一条文档的排序值
last_sort_value = result['hits']['hits'][-1]['sort']
# 循环获取后续页结果
while len(result['hits']['hits']) == page_size:
query = {
"query": {
"match_all": {}
},
"size": page_size,
"sort": [{"_id": "asc"}],
"search_after": last_sort_value
}
result = es.search(index=index_name, body=query)
for hit in result['hits']['hits']:
print(hit['_source'])
if len(result['hits']['hits']):
last_sort_value = result['hits']['hits'][-1]['sort']
- 适用场景:适用于实时分页查询,支持按任意字段排序。但 search_after 只能向后翻页,不支持向前翻页。
总结
在处理 ES 深度分页时,需要根据具体的业务场景选择合适的解决方案。如果需要一次性获取大量数据,可以使用滚动搜索;如果是实时分页查询,建议使用 search_after。
Synchronized实现原理
一、Synchronized 的基本作用
synchronized 是 Java 中实现线程同步的关键字,用于确保多个线程对共享资源的原子性、可见性和有序性访问。它通过以下两种方式使用:
- 修饰代码块:锁定指定对象。
- 修饰方法:锁定当前实例对象(实例方法)或类对象(静态方法)。
二、底层实现机制
1. 字节码层面
代码块同步:通过 monitorenter 和 monitorexit 指令实现。
- java复制
public void syncBlock() {
synchronized (this) {
// 临界区代码
}
}
编译后的字节码:
复制
monitorenter // 获取锁
... // 临界区代码
monitorexit // 释放锁
方法同步:通过方法访问标志 ACC_SYNCHRONIZED 标记。
- java复制
public synchronized void syncMethod() {
// 临界区代码
}
方法调用时:JVM 会隐式获取锁(无需显式字节码指令)。
三、锁的存储结构(对象头)
每个 Java 对象在内存中分为三部分:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
对象头结构:
- Mark Word(64位系统占8字节):
- 存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等)。
- 锁状态不同时,Mark Word 的内容会动态变化。
- Klass Pointer:指向类元数据的指针(开启压缩指针后占4字节)。
四、锁升级过程(优化机制)
Java 6 后引入锁升级机制,根据竞争情况动态调整锁状态,减少性能开销:
- 偏向锁(Biased Locking)
- 适用场景:单线程访问,无实际竞争。
- 实现原理:
- 对象头 Mark Word 中存储线程ID。
- 后续同一线程进入同步代码时,无需 CAS 操作,直接检查线程ID是否匹配。
-
- 优势:消除无竞争时的同步开销。
- 触发条件:默认开启(JVM 参数 -XX:+UseBiasedLocking)。
- 轻量级锁(Lightweight Locking)
- 适用场景:多线程交替执行,竞争不激烈。
- 实现原理:
- 线程在栈帧中创建锁记录(Lock Record),存储对象 Mark Word 的拷贝。
- 通过 CAS 操作尝试将对象头替换为指向锁记录的指针。
- 成功:线程获得锁。
- 失败:锁已占用,升级为重量级锁。
-
- 优势:避免线程阻塞,通过自旋(CAS)减少上下文切换。
- 重量级锁(Heavyweight Locking)
- 适用场景:多线程竞争激烈。
- 实现原理:
- 依赖操作系统提供的**互斥量(Mutex)**实现线程阻塞。
- 未获取锁的线程进入阻塞队列,由操作系统调度唤醒。
-
- 劣势:涉及用户态到内核态的切换,性能开销较大。
锁升级流程:
复制
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
五、锁的优化技术
- 自旋锁(Spin Lock)
- 原理:线程在竞争锁失败后,不立即阻塞,而是循环(自旋)尝试获取锁。
- 适用场景:锁占用时间短,减少线程切换的开销。
- JVM 参数:-XX:+UseSpinning(Java 6 后默认开启)。
- 适应性自旋锁(Adaptive Spinning)
- 原理:根据历史自旋成功率动态调整自旋次数。
- 若上次自旋成功获取锁,则允许更长的自旋时间。
- 锁消除(Lock Elimination)
- 原理:JIT 编译器通过逃逸分析,移除不可能存在共享资源竞争的锁。
示例:
-
- java复制
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1).append(s2);
return sb.toString();
}
-
-
- StringBuffer 的 append() 方法使用 synchronized,但此处 sb 未逃逸出方法,JVM 会自动消除锁。
-
- 锁粗化(Lock Coarsening)
- 原理:将多个连续的加锁-解锁操作合并为一个更大范围的锁。
示例:
-
- java复制
for (int i = 0; i < 100; i++) {
synchronized (this) {
// 操作共享资源
}
}
-
-
- JVM 可能将锁范围扩大到整个循环,减少锁的获取/释放次数。
-
六、重量级锁的实现细节(Mutex)
- 操作系统依赖:在 Linux 中,重量级锁通过 pthread_mutex_t 实现。
- 阻塞与唤醒:
- 未获取锁的线程进入等待队列(Contention List),由操作系统挂起。
- 锁释放时,唤醒队列中的线程,触发上下文切换。
七、实战示例与验证
1. 查看对象头信息(JOL 工具)
使用 Java Object Layout(JOL)工具分析对象头变化:
java
复制
// 添加依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
// 示例代码
public class LockExample {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
synchronized (obj) {
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
输出:
复制
无锁状态:01 00 00 00 (偏向锁标志位 0)
加锁后: 05 90 e1 1a (偏向锁标志位 1,线程ID 存储)
2. 性能对比测试
java
复制
// 测试不同锁状态下的性能差异
public class LockBenchmark {
private static final int THREADS = 4;
private static final int CYCLES = 1000000;
public static void main(String[] args) {
long start = System.currentTimeMillis();
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
for (int i = 0; i < THREADS; i++) {
executor.submit(() -> {
for (int j = 0; j < CYCLES; j++) {
synchronized (LockBenchmark.class) {
// 模拟临界区操作
}
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.HOURS);
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
结果分析:
- 无竞争时(单线程),偏向锁耗时最低。
- 高并发时,重量级锁更稳定(但耗时较长)。