1. 从单机到集群:为什么我们需要分布式锁?
记得我刚入行那会儿,做的还是单体应用。那时候处理多线程并发问题,脑子里蹦出来的第一个词就是 synchronized,或者用 ReentrantLock。这招在单机环境下确实好使,锁住一个共享变量,大家排队访问,数据安全得很。
但后来项目越做越大,服务从一台机器变成了十几台、几十台机器组成的集群。问题就来了:我在机器A上用 synchronized 锁住了一个对象,机器B上的线程压根就不知道这回事,它依然可以大摇大摆地访问共享资源(比如数据库里的库存)。这下好了,数据一致性彻底乱套,超卖、重复扣款这些生产事故就跟着来了。我踩过的坑告诉我,单机锁在分布式系统面前,就像用一把锁去锁一排并排的门,根本锁不住。
这时候,分布式锁 就成了我们的“救命稻草”。它的核心思想很简单:我们需要一个所有服务节点都能访问到的、全局唯一的“裁判”,来裁决同一时刻谁能拿到锁。这个“裁判”通常是一个独立的中间件,比如 Redis 或者 ZooKeeper。今天咱们的主角 Redisson,就是基于 Redis 实现分布式锁的一个非常优秀的 Java 客户端库,它把复杂的分布式锁逻辑封装成了像使用本地锁一样简单的 API。
在 Redisson 的武器库里,最常用、也最让人纠结的两个方法就是 lock() 和 tryLock()。一个会“傻等”,一个会“试探”。选错了,轻则性能下降,重则系统死锁。接下来,我就结合电商秒杀、库存扣减这些咱们程序员天天打交道的场景,把这哥俩的脾气秉性给你掰扯清楚。
2. 死等到底:深入 Redisson 的 lock() 方法
lock() 这个方法,我给它起的外号叫“老实人”。它的行为非常直接:一旦决定要锁,就死等到底,直到拿到锁为止。这种模式在学术上被称为 阻塞式加锁。
2.1 lock() 是怎么工作的?
我们先来看一段最简单的使用代码。说实话,在实际项目里我直接用 lock() 的场景不多,但为了弄明白它,我特意写了测试代码来折腾它。
@Autowired
private RedissonClient redissonClient;
public void doSomething() {
RLock lock = redissonClient.getLock("myLock");
// 获取锁,如果锁被占用,当前线程会一直等待
lock.lock();
try {
// 你的核心业务逻辑,比如扣减库存
System.out.println("执行业务逻辑...");
Thread.sleep(5000); // 模拟耗时操作
} finally {
// 必须在finally块中释放锁
lock.unlock();
}
}
你也可以指定一个租约时间,意思是“我最多只持有锁这么长时间,时间一到,即使我没干完活,锁也自动释放,防止我死锁”。
// 获取锁,并设置锁的自动释放时间为10秒
lock.lock(10, TimeUnit.SECONDS);
这里有个大坑我得提醒你:如果你用了带超时参数的 lock(leaseTime, unit),Redisson 会关闭看门狗自动续期机制。这意味着,如果你的业务逻辑执行时间超过了 10 秒,锁会在第10秒自动释放,而此时你的业务可能还没执行完!其他线程就能拿到锁进来操作,导致数据错乱。所以,除非你非常确定业务的最大执行时间,否则慎用这个参数,或者使用我们后面会讲的 tryLock。
2.2 源码窥探:lock() 的“死循环”哲学
光会用不行,咱们还得看看它肚子里装的什么货。扒开 lock() 的源码(这里我做了大量简化,只留核心逻辑),你会发现它的核心就是一个“尝试-等待-再尝试”的循环。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 第一次尝试获取锁
Long ttl = tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl == null) {
// 获取成功,直接返回
return;
}
// 获取失败,订阅这个锁的释放消息
CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
try {
// 进入“死循环”,直到获取锁成功
while (true) {
// 再次尝试获取锁
ttl = tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl == null) {
// 成功,跳出循环
return;
}
// 如果锁被其他线程持有(ttl是剩余的存活时间)
if (ttl >= 0) {
// 这里很关键!它不是无脑循环,而是利用信号量(Semaphore)等待
// 最多等待 ttl 毫秒,等锁持有者释放的信号
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// 如果ttl为负(一种特殊情况),则进行不可中断的等待
future.getNow().getLatch().acquire();
}
}
} finally {
unsubscribe(future, th


1万+

被折叠的 条评论
为什么被折叠?



