4. 锁的基础分类:乐观锁与悲观锁、公平锁与非公平锁、可重入锁与不可重入锁、共享锁与排他锁
在 SSO 单点登录系统中,多个异步任务(如令牌刷新、会话同步、票据校验)需要并发访问共享资源(如用户会话缓存、令牌黑名单、分布式计数器)。锁是控制并发访问的核心机制。理解锁的基础分类,是设计高效、正确并发控制方案的前提。
4.1 乐观锁与悲观锁
这是从并发冲突的应对策略角度进行的分类,决定了锁的加锁时机与粒度。
| 特性 | 乐观锁 (Optimistic Lock) | 悲观锁 (Pessimistic Lock) |
|---|---|---|
| 核心思想 | 假设并发冲突很少发生,先进行操作,提交时再检查冲突。 | 假设并发冲突一定会发生,在操作前就加锁阻塞其他线程。 |
| 实现方式 | 版本号机制、CAS(Compare And Swap)算法。 | 数据库行锁、synchronized、ReentrantLock。 |
| 适用场景 | 读多写少、冲突概率低的场景(如用户信息读取)。 | 写多读少、冲突概率高的场景(如令牌发放计数器)。 |
| 优缺点 | 无锁开销,吞吐量高;但需要处理重试或回滚。 | 保证强一致性;但锁竞争激烈时性能下降,易死锁。 |
SSO 实战提示: 在 SSO 的会话缓存更新中,推荐使用乐观锁(如 Redis 的 WATCH 命令或版本号)来更新用户属性,避免长时间持有锁导致令牌发放延迟。仅在操作令牌黑名单等写冲突极高的资源时,才使用悲观锁。
4.2 公平锁与非公平锁
这是从线程获取锁的顺序角度进行的分类,直接影响线程的调度公平性和系统吞吐量。
- 公平锁 (Fair Lock): 按照线程请求锁的先后顺序(FIFO)分配锁。所有线程都会排队,不会发生“插队”现象。
- 非公平锁 (Non-fair Lock): 允许线程在锁释放时“插队”。如果锁刚好被释放,新来的线程可能直接获取锁,而唤醒队列中的线程。
| 对比维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 吞吐量 | 较低(线程切换频繁) | 较高(减少线程挂起/唤醒开销) |
| 公平性 | 严格保证,无饥饿 | 不保证,可能导致线程饥饿 |
| 实现代价 | 需要维护精确的等待队列 | 实现简单,CAS 直接竞争 |
| SSO 推荐 | 令牌发放等关键路径,避免某个请求长时间等待 | 会话缓存批量更新,追求高吞吐 |
避坑指南: 在 SSO 的异步任务编排中,如果使用非公平锁处理令牌刷新任务,可能导致低优先级的刷新任务长时间“插队”,使得高优先级的登录请求因获取不到锁而超时。建议对关键路径使用公平锁,或使用优先级队列配合锁机制。
4.3 可重入锁与不可重入锁
这是从同一线程能否重复获取同一把锁的角度进行的分类。
- 可重入锁 (Reentrant Lock): 允许同一个线程多次获取同一把锁。锁内部维护一个持有计数器和所有者线程。每次重入计数器 +1,每次释放 -1,直到计数器归零才真正释放锁。
- 不可重入锁 (Non-reentrant Lock): 同一线程在持有锁的情况下,再次尝试获取该锁会导致死锁(自己锁住自己)。
代码示例:可重入锁 vs 不可重入锁行为
// 可重入锁(Java ReentrantLock)
ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
inner(); // 再次获取同一把锁,成功
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock(); // 可重入:计数器 +1
try {
// 执行内部逻辑
} finally {
lock.unlock(); // 计数器 -1
}
}
// 不可重入锁(自定义简单实现)
public class NonReentrantLock {
private boolean locked = false;
public synchronized void lock() {
while (locked) {
wait(); // 如果已锁,阻塞
}
locked = true; // 假设线程A持有锁
// 如果线程A再次调用 lock(),locked 为 true,自己阻塞自己 → 死锁
}
}
在 SSO 的异步任务中,一个任务可能调用多个子方法(如 validateToken() 内部调用 refreshSession()),如果这些方法都使用同一把锁,必须使用可重入锁,否则会导致死锁。
4.4 共享锁与排他锁
这是从锁的访问权限角度进行的分类,决定了多个线程能否同时读取或写入。
| 锁类型 | 读-读 | 读-写 | 写-写 | 典型实现 |
|---|---|---|---|---|
| 共享锁 (S锁 / Read Lock) | 允许 | 阻塞 | 阻塞 | ReentrantReadWriteLock.ReadLock |
| 排他锁 (X锁 / Write Lock) | 阻塞 | 阻塞 | 阻塞 | ReentrantLock、synchronized |
- 共享锁: 多个线程可以同时持有共享锁,只要没有线程持有排他锁。适用于读多写少的场景,极大提升并发读性能。
- 排他锁: 同一时刻只能有一个线程持有该锁,其他线程无论是读还是写都必须等待。保证数据写入的强一致性。
4.5 锁分类在 SSO 异步任务中的综合选型
在 SSO 单点登录的异步任务编排中,不同锁类型需要组合使用。以下是一个典型的选型对照表:
| SSO 资源 | 推荐锁类型 | 理由 |
|---|---|---|
| 用户会话缓存(读多写少) | 乐观锁 + 共享锁 | 减少锁竞争,提升并发读取性能 |
| 令牌黑名单(写多) | 悲观锁 + 排他锁 | 保证黑名单的强一致性,防止重复校验 |
| 分布式计数器(如登录失败次数) | 乐观锁(CAS) | 短操作,冲突概率低,避免锁开销 |
| 异步任务队列调度 | 公平锁 + 可重入锁 | 防止任务饥饿,支持任务内部嵌套调用 |
| 全局配置更新 | 排他锁 + 悲观锁 | 配置变更频率低,但必须原子生效 |
设计原则: 在 SSO 异步任务编排中,优先使用乐观锁和共享锁提升吞吐量;仅在数据一致性要求极高或写冲突严重时,才退化为悲观锁和排他锁。同时,所有涉及递归或嵌套调用的锁,必须使用可重入锁。