在讨论数据库热点行问题的时候我们先看下 PolarDB 是如何解决的?
PolarDB 使用流水线(Pipeline)方式解决热点行问题的核心思想,是通过将针对热点行的并发操作拆分成多个阶段的顺序执行,以减少锁争用并提高吞吐量。这种方式借鉴了生产流水线的理念,将一批请求分解并交替处理,而不是让每个请求独立竞争锁资源。
1. 热点行问题的本质
在高并发场景下,多个事务对同一行数据进行更新,会导致数据库层面锁争用(如行锁或表锁),这些锁的排队和竞争成为性能瓶颈。如果并发度很高,事务等待时间会急剧增加,从而降低数据库的整体吞吐量。
2. 流水线机制的原理
PolarDB 通过流水线机制,将针对同一热点行的更新请求组织成一组批处理操作,并在后台按序逐步执行,从而避免并发锁争用。具体原理如下:
(1)将请求打包成批次(Batching)
- 请求收集:PolarDB 在接收到多个针对同一行(或相同分片)的更新请求时,先将这些请求缓存在内存中,而不是立即触发数据库更新操作。
- 批处理:在合适的时间点(如批次大小达到阈值,或延迟时间到达),将这些请求打包成一个批次。
(2)流水线分阶段处理
-
拆分阶段任务:将一组更新操作按阶段依次处理,例如:
- 合并更新:对于类似计数器的更新(如
+1操作),PolarDB 可以将多个请求合并为一个操作(如+N),从而减少需要实际执行的更新次数。 - 顺序执行:对无法合并的更新请求按序处理,避免多个请求同时访问同一行记录。
- 批量提交:在事务层面将多个操作一并提交,减少事务管理和日志写入的开销。
- 合并更新:对于类似计数器的更新(如
-
流水线并行化:尽管同一批次内的更新是顺序处理,但不同批次间可以并行执行,这种交替执行的方式提高了整体吞吐量。
(3)结果快速返回
- 延迟最终一致性:在流水线机制中,客户端的更新请求可以快速返回成功状态,而实际的数据更新会在后台按顺序完成。这种方式适合对强一致性要求不高的场景(如计数器增长、日志写入等)。
3. 流水线机制的优势
- 减少锁争用: 流水线模式通过将并发请求变为顺序批处理,避免了事务锁的竞争,提升了数据库的并发能力。
- 合并优化: 针对热点行的累积操作(如计数器递增),可以将多个更新操作合并为一个事务,从而减少写入次数,显著提高吞吐量。
- 延迟分摊: 请求的延迟被分摊到流水线的各个阶段,单个请求的等待时间虽然略有增加,但整体吞吐量得到了大幅提升。
- 高并发场景友好: 适合大量并发写请求集中到同一行的场景,如点击量统计、排名计数等。
polarDB 内核做了批次优化,第一个事务作为 leader,其余事务为 follower,leader 会收集所有的 follower 更新,leader 开一次事务,拿一把锁。然后提交释放锁,其余 follower 都不需要开事务和拿锁
PolarDB 官方文档中的“Leader-Follower”设计模式在处理热点行问题时,展示了以下几个核心思想,旨在高效解决并发写入的冲突问题。这个模式通过将并发请求分为一个 Leader 和多个 Follower,合理组织请求处理流程,从而提高吞吐量并降低锁争用
一下是实现 Leader-Follower 模式的代码来模拟多个事务来的时候如何将多个事务更新合并为一个事务更新的
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
// Shared Counter class
class Counter {
private int value = 0;
public synchronized int increment(int amount) {
value += amount;
return value;
}
public synchronized int getValue() {
return value;
}
}
// Follower class
class Follower {
private final int incrementAmount;
public Follower(int incrementAmount) {
this.incrementAmount = incrementAmount;
}
public int getIncrementAmount() {
return incrementAmount;
}
}
// Leader class
class Leader {
private final Counter counter;
private final Queue<Follower> followerQueue;
private final int batchSize;
private final ExecutorService executor;
private final Object processingLock = new Object();
private volatile boolean isProcessing;
public Leader(Counter counter, Queue<Follower> followerQueue, int batchSize) {
this.counter = counter;
this.followerQueue = followerQueue;
this.batchSize = batchSize;
this.executor = Executors.newCachedThreadPool();
this.isProcessing = false;
}
public void addFollower(Follower follower) {
followerQueue.offer(follower);
synchronized (processingLock) {
if (!isProcessing) {
isProcessing = true;
executor.submit(this::processFollowers);
}
}
}
private void processFollowers() {
try {
while (true) {
List<Follower> batch;
synchronized (processingLock) {
batch = new ArrayList<>();
while (!followerQueue.isEmpty() && batch.size() < batchSize) {
Follower follower = followerQueue.poll();
if (follower != null) {
batch.add(follower);
}
}
// If no more followers and no batch to process, exit
if (batch.isEmpty() && followerQueue.isEmpty()) {
isProcessing = false;
break;
}
}
// Process the batch outside the lock
if (!batch.isEmpty()) {
int totalIncrement = batch.stream().mapToInt(Follower::getIncrementAmount).sum();
int newValue = counter.increment(totalIncrement);
System.out.println("Leader processes batch: " + batch.size() + " followers, Total Increment: " + totalIncrement + ", New Counter Value: " + newValue);
}
// Simulate processing delay
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
synchronized (processingLock) {
if (!followerQueue.isEmpty()) {
executor.submit(this::processFollowers);
} else {
isProcessing = false;
}
}
}
}
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
public class LeaderFollowerPattern {
public static void main(String[] args) {
Counter counter = new Counter();
Queue<Follower> followerQueue = new ConcurrentLinkedQueue<>();
Leader leader = new Leader(counter, followerQueue, 5);
// Simulate multiple concurrent requests
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int threadId = i;
Thread t = new Thread(() -> {
for (int j = 0; j < 5; j++) {
int incrementAmount = ThreadLocalRandom.current().nextInt(1, 11);
System.out.println("Thread-" + threadId + " adds Follower with increment: " + incrementAmount);
leader.addFollower(new Follower(incrementAmount));
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
threads.add(t);
t.start();
}
// Wait for threads to finish
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
leader.shutdown();
System.out.println("Final Counter Value: " + counter.getValue());
}
}