三种同步工具countdownlatch,cylicBarrier,Semaphore
countdownlatch:类似于一个起跑线,所有来的线程到这先等待,到齐后倒计时一起跑
cylicBarrier:类似与一个大巴,里面有许多的座位,等到所有的人都上车以后才开始跑
Semaphore:信号量,类似于给线程加权,第一个需要5个信号量,第二个需要1个,第三需要3个,现在我有7个信号量的话,第一个拿走5个第二个拿走1个剩下一个第三就走不了类似于这样
1.同时进行使用countdownlatch
import java.util.concurrent.CountDownLatch;
public class ConcurrentStart {
public static void main(String[] args) throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
new Thread(() -> {
try {
startLatch.await();
System.out.println("A开始执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "A").start();
new Thread(() -> {
try {
startLatch.await();
System.out.println("B开始执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "B").start();
new Thread(() -> {
try {
startLatch.await();
System.out.println("C开始执行");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "C").start();
// 同时释放所有线程
Thread.sleep(100);
startLatch.countDown();
}
}
public class SequentialExecution {
// 使用volatile保证可见性
private volatile static int current = 1;
public static void main(String[] args) {
new Thread(() -> {
while (current != 1) {} // 忙等待
System.out.println("A");
current = 2;
}, "A").start();
new Thread(() -> {
while (current != 2) {} // 忙等待
System.out.println("B");
current = 3;
}, "B").start();
new Thread(() -> {
while (current != 3) {} // 忙等待
System.out.println("C");
}, "C").start();
}
}
3.交替执行(使用 Semaphore)
import java.util.concurrent.Semaphore;
public class AlternateExecution {
// 初始化信号量:A有1个许可,B/C无许可
private static Semaphore semA = new Semaphore(1);
private static Semaphore semB = new Semaphore(0);
private static Semaphore semC = new Semaphore(0);
public static void main(String[] args) {
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
semA.acquire(); // 获取A许可
System.out.print("A ");
semB.release(); // 释放B许可
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "A").start();
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
semB.acquire(); // 获取B许可
System.out.print("B ");
semC.release(); // 释放C许可
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "B").start();
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
semC.acquire(); // 获取C许可
System.out.print("C ");
semA.release(); // 释放A许可
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "C").start();
}
}
一致性:不管用户访问哪一个节点,得到的数据必须是一致的,意思就是所有的节点必须同步
可用性:用户访问分布式系统的时候总是能够成功的
分区容错性:Partition
,就是分区,就是当分布式系统节点之间出现网络故障导致节点之间无法通信的情况,Tolerance
,就是容错,即便是系统出现网络分区,整个系统也要持续对外提供服务。
矛盾:在分布式架构中网络不可能一直保持稳定,网络分区是必定的此时就导致一致性和可用性不可能同时满足,要做取舍
允许用户任意读写,保证可用性。但由于node03无法完成同步,就会出现数据不一致的情况。满足AP
不允许用户写,可以读,直到网络恢复,分区消失。这样就确保了一致性,但牺牲了可用性。满足CP
既然分布式系统要遵循CAP定理,那么问题来了,我到底是该牺牲一致性还是可用性呢?如果牺牲了一致性,出现数据不一致该怎么处理?
人们在总结系统设计经验时,最终得到了一些心得:
Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
以上就是BASE理论。
简单来说,BASE理论就是一种取舍的方案,不再追求完美,而是最终达成目标。因此解决分布式事务的思想也是这样,有两个方向:
AP思想:各个子事务分别执行和提交,无需锁定数据。允许出现结果不一致,然后采用弥补措施恢复,实现最终一致即可。例如AT
模式就是如此
CP思想:各个子事务执行后不要提交,而是等待彼此结果,然后同时提交或回滚。在这个过程中锁定资源,不允许其它人访问,数据处于不可用状态,但能保证一致性。例如XA
模式
好的,我们来用通俗的比喻和场景解释一下分布式事务中的“悬挂”和“空回滚”这两个概念。
想象一下,你在网上同时买了两件东西,一个来自仓库A(比如书本),一个来自仓库B(比如玩具)。为了保证交易公平(要么都发货,要么都不发),有一个总指挥(事务协调者) 在协调这两个仓库的操作。
核心概念:TCC 模式
我们以最常用的事务模式之一 TCC (Try-Confirm-Cancel) 来举例说明,它分三个阶段:
问题来了:网络是不可靠的!消息可能会延迟、重复或丢失。 这就导致了悬挂和空回滚。
通俗比喻:
核心问题: Try 操作执行成功了,但其结果(成功回复)在对应的 Cancel 操作执行完成之后才到达协调者。 导致这个 Try 预留的资源失去了归属,成了“孤儿”。
危害: 被悬挂的资源(如库存、优惠券、余额)被锁定无法释放,影响后续业务。
总结就是新老try命令没有区分,按照我的记忆这个和分布式集群的脑裂问题很像,所有可以有相同的解法,记录版本,是新版本发的try我就接受,老的我就丢弃
通俗比喻:
核心问题: 在一个分支事务(仓库B)根本没有执行过 Try 操作的情况下,该分支事务收到了协调者发来的 Cancel 指令并要求执行回滚。
为什么允许空回滚? 分布式系统要求最终一致性。即使某个参与者没收到Try,协调者发出Cancel了,所有参与者都必须能响应Cancel,确保最终状态一致(都回滚了)。系统设计上要求,即使没执行Try,收到Cancel也必须能处理并返回成功。
潜在冲突: 如果那个迷路的 Try 指令在 Cancel 之后才到达仓库B,仓库B就会执行Try(预留一个玩具)。这时就出现了冲突:仓库B刚刚为一个事务预留了资源(Try),但这个事务在协调者那边已经被标记为回滚(Cancel)了!这就是为什么需要防悬挂措施(见下文)。
系统设计者想出了办法,主要靠记录状态:
防悬挂 (防止悬挂):
允许空回滚:
总结一下这个防御机制:
阶段 | 正常流程 | 事务悬挂 (幽灵包裹) | 空回滚 (取消不存在的订单) | 防御措施 |
---|---|---|---|---|
Try (预留) | 总指挥: “仓库A/B,预留货!” 仓库: “好的,预留了!” (记录预留) |
总指挥: “仓库A,预留货!” 仓库A: 预留成功,但回复丢失。 总指挥没收到回复,以为失败。 |
总指挥根本没发出 Try 给仓库B (或消息丢失)。 | 仓库执行 Try 前: 查本地记录: 已有该订单的 Cancel 记录? → 拒绝执行真预留 (防悬挂) 无记录 → 正常预留并记录 Try。 |
Cancel (取消) | 总指挥: “仓库A/B,取消预留!” 仓库: 释放预留。 (正常取消) |
总指挥: (因超时) “仓库A,取消预留!” 仓库A: 收到 Cancel,释放预留,记录 Cancel。 然后,迟到的 Try 成功回复才到总指挥 → 悬挂产生! |
总指挥: (因超时或其他失败) “仓库B,取消预留!” 仓库B: 从未收到过该订单的 Try! → 执行空回滚:记录 “XID 已空取消”,并回复成功。 |
仓库收到 Cancel: 查本地记录: 有该订单的 Try 记录? → 正常释放预留,记录 Cancel。 无 Try 记录? → 记录 “XID 已空取消” (允许空回滚),回复成功。 |
后续问题 | 无 | 仓库A 的预留已被取消,但总指挥后来收到它的 Try 成功回复,误以为是新事务 → 该预留成幽灵状态,资源被锁死。 | 若迟到的 Try 指令之后到达仓库B: 仓库B 检查记录 → 发现已有该 XID 的 Cancel 记录 (空回滚记录) → 拒绝执行真预留 (防悬挂生效!) |
核心思想: 1. Cancel 记录优先: 见 Cancel 记录,Try 必须怂。 2. Cancel 要大度: 没活干也要应一声并记下来。 3. Try 要谨慎: 干活前先看有没有 Cancel 记录。 |
通过这种在参与者(仓库)本地记录事务状态(Try执行过吗?Cancel执行过吗?是空回滚吗?)的机制,就能有效地防止悬挂事务的产生,并正确处理空回滚请求,保证分布式事务最终的一致性。
好的,我们来通俗易懂地讲解一下这四种关键的限流算法:固定窗口计数、滑动窗口计数、令牌桶算法和漏桶算法。它们都用于控制系统的流量(如 API 请求、网络数据包),防止系统被突发流量冲垮。
想象一个水龙头和水桶的场景,请求就是水流。
原理:
通俗比喻:
优点: 实现简单,内存消耗少(只需存储当前窗口计数和窗口结束时间)。
缺点: 临界点问题(Boundary Problem) 非常严重!如上例所示,在两个窗口的交界处(如 0:59 - 1:00),系统可能会在极短时间内承受 2倍阈值 的请求(前一个窗口末尾的请求 + 后一个窗口开头的请求),这对系统是很大的冲击。不够平滑。
原理:
通俗比喻:
优点: 大大缓解了固定窗口的临界点问题,流量控制更加平滑和精确。能更好地反映最近的流量状况。
缺点: 实现比固定窗口复杂。精确计数需要存储更多数据(每个请求的时间戳)或消耗更多 CPU(频繁清理过期数据)。子窗口法是一种内存和精度之间的权衡,子窗口越多越精确但内存开销越大。
原理:
通俗比喻:
优点:
缺点: 实现相对复杂(需要定时放令牌或惰性计算),需要维护桶的当前令牌数量和时间戳。突发流量的大小受限于桶容量。
典型应用: 网络流量整形(如 QoS),API 网关限流(允许合理突发),云计算服务限流。
原理:
通俗比喻:
优点:
缺点:
典型应用: 需要严格平滑输出速率的场景,如老式电信网络、某些需要保护下游脆弱系统的场景。
特性 | 固定窗口计数 | 滑动窗口计数 | 令牌桶算法 | 漏桶算法 |
---|---|---|---|---|
核心思想 | 固定时间段内计数 | 最近时间段内计数 | 按固定速率产生令牌,请求消耗令牌 | 请求流入队列,按固定速率流出处理 |
关键数据结构 | 计数器 + 窗口结束时间 | 时间戳队列 / 子窗口计数器数组 | 当前令牌数 + 最后填充时间 | FIFO 队列 |
流量控制依据 | 当前窗口计数 < 阈值? | 最近 N 秒计数 < 阈值? | 桶中有令牌? | 队列未满? |
处理速率 | 窗口内平均 <= 阈值 | 任意时刻最近 N 秒平均 <= 阈值 | 长期平均 <= 令牌产生速率 | 绝对恒定 = 流出速率 |
突发流量处理 | ❌ 差 (临界点 2 倍冲击) | ⚠️ 较好 (取决于窗口精度) | ✅ 好 (可用令牌数内允许突发) | ⚠️ 有限 (靠队列缓存,但处理速度不变) |
输出流量平滑度 | ❌ 不平滑 (窗口边界突变) | ✅ 较平滑 | ✅ 较平滑 (允许突发) | ✅✅ 绝对平滑 |
主要缺点 | 临界点问题 | 实现较复杂,资源消耗稍高 | 实现较复杂 | 无法利用空闲资源加速处理突发 |
典型用途 | 简单限流场景 | 需要相对精确平滑限流的场景 | 允许合理突发、限制长期平均速率的场景 | 需要强制平滑输出速率的场景 |
简单选择指南:
理解这四种算法是设计和实现高效、稳定限流系统的基础。它们各有千秋,需要根据实际应用场景的需求(是否允许突发?是否需要绝对平滑?实现复杂度要求?)来选择合适的算法或组合。