目录
1. 什么是分布式锁?
生活中的例子
想象一下这个场景:
🚻 公共厕所的锁
- 当你进入厕所时,你会把门锁上
- 其他人看到门锁着,就知道里面有人,会等待
- 你用完后开锁出来,下一个人才能进入
🏦 银行ATM机
- 同一时间只能有一个人使用ATM
- 其他人必须排队等待
- 前一个人操作完成后,下一个人才能开始
技术层面的理解
// 想象这是你的业务代码
public void transferMoney(String fromAccount, String toAccount, BigDecimal amount) {
// 步骤1: 检查余额
BigDecimal balance = getBalance(fromAccount);
// 步骤2: 判断余额是否足够
if (balance.compareTo(amount) >= 0) {
// 步骤3: 扣除金额
deductMoney(fromAccount, amount);
// 步骤4: 增加金额
addMoney(toAccount, amount);
}
}
问题:如果多个请求同时执行这段代码会怎样?
- 请求A和请求B同时检查余额,都发现余额充足
- 请求A扣除100元
- 请求B也扣除100元
- 结果:账户被扣了200元,但只转出了100元!
分布式锁的作用:确保同一时间只有一个请求能执行这段代码。
2. 为什么需要分布式锁?
单体应用 vs 分布式应用
单体应用(简单)
// 在单体应用中,我们可以用Java的synchronized
public synchronized void transferMoney(...) {
// 业务逻辑
}
特点:
- 所有代码在一个JVM中运行
- synchronized可以保证线程安全
- 简单有效
分布式应用(复杂)
用户请求 → 负载均衡器 → 服务器A (JVM1)
→ 服务器B (JVM2)
→ 服务器C (JVM3)
问题:
- synchronized只能在单个JVM内生效
- 不同服务器上的代码无法使用synchronized互斥
- 需要一个"全局"的锁机制
分布式环境的挑战
// 服务器A上的代码
public class ServerA {
public synchronized void processOrder(String orderId) {
// 处理订单
Order order = getOrder(orderId);
if (order.getStatus() == PENDING) {
order.setStatus(PROCESSING);
updateOrder(order);
// 实际处理逻辑...
}
}
}
// 服务器B上的代码
public class ServerB {
public synchronized void processOrder(String orderId) {
// 同样的处理逻辑
// 但是和服务器A的synchronized互不影响!
}
}
结果:同一个订单可能被两台服务器同时处理!
3. 分布式锁的核心要求
一个好的分布式锁必须满足以下特性:
3.1 互斥性(Mutual Exclusion)
// 同一时间只有一个客户端能获得锁
if (客户端A已经获得锁) {
客户端B无法获得锁; // 必须等待
}
3.2 安全性(Safety)
// 只有获得锁的客户端才能释放锁
if (lockOwner != currentClient) {
throw new Exception("你不能释放别人的锁!");
}
3.3 活性(Liveness)
// 即使持有锁的客户端崩溃,锁也要能被释放
lockWithTimeout(30, TimeUnit.SECONDS); // 30秒后自动释放
3.4 高可用(High Availability)
// Redis集群模式下,即使部分节点宕机,锁服务仍然可用
4. Redisson分布式锁基础
4.1 基本概念
Redisson分布式锁的底层原理:
- 使用Redis的
SET
命令设置一个key - 设置过期时间防止死锁
- 使用Lua脚本保证操作原子性
- 通过Redis的发布订阅机制实现锁等待
4.2 快速开始
// 1. 创建Redisson客户端
RedissonClient redisson = Redisson.create();
// 2. 获取锁对象
RLock lock = redisson.getLock("myBusinessLock");
// 3. 使用锁
try {
// 尝试获取锁
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
// ↑ ↑
// 等待时间 自动释放时间
if (acquired) {
// 获取锁成功,执行业务逻辑
System.out.println("获得锁,开始处理业务");
doBusinessLogic();
} else {
// 获取锁失败
System.out.println("获取锁失败,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁(只有锁的持有者才能释放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("释放锁成功");
}
}
4.3 核心API解析
// 获取锁对象
RLock lock = redisson.getLock("lockName");
// 方法1: 阻塞等待获取锁(危险!可能永远等待)
lock.lock();
// 方法2: 带超时的阻塞等待
lock.lock(10, TimeUnit.SECONDS); // 10秒后自动释放
// 方法3: 尝试获取锁(推荐)
boolean success = lock.tryLock(); // 立即返回
// 方法4: 带超时的尝试获取锁(最推荐)
boolean success = lock.tryLock(
3, // 等待获取锁的时间
10, // 锁自动释放时间
TimeUnit.SECONDS
);
// 检查锁状态
boolean isLocked = lock.isLocked(); // 锁是否被任何客户端持有
boolean isHeldByMe = lock.isHeldByCurrentThread(); // 锁是否被当前线程持有
int holdCount = lock.getHoldCount(); // 当前线程持有锁的次数(可重入)
// 释放锁
lock.unlock();
// 强制释放锁(谨慎使用)
lock.forceUnlock();
5. 基本分布式锁详解
5.1 完整示例:库存扣减
这是电商系统中最常见的场景:
@Service
public class InventoryService {
@Autowired
private RedissonClient redisson;
@Autowired
private InventoryRepository inventoryRepository;
/**
* 安全的库存扣减
* @param productId 商品ID
* @param quantity 扣减数量
* @return 是否扣减成功
*/
public boolean deductInventory(Long productId, int quantity) {
// 为每个商品创建独立的锁
String lockKey = "inventory_lock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
// 尝试获取锁,等待5秒,锁30秒后自动释放
boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!acquired) {
log.warn("获取库存锁失败,商品ID: {}", productId);
return false;
}
log.info("获取库存锁成功,开始处理商品: {}", productId);
// 查询当前库存
Inventory inventory = inventoryRepository.findByProductId(productId);
if (inventory == null) {
log.warn("商品不存在: {}", productId);
return false;
}
// 检查库存是否充足
if (inventory.getQuantity() < quantity) {
log.warn("库存不足,当前库存: {}, 需要扣减: {}",
inventory.getQuantity(), quantity);
return false;
}
// 执行库存扣减
inventory.setQuantity(inventory.getQuantity() - quantity);
inventoryRepository.save(inventory);
log.info("库存扣减成功,商品: {}, 扣减数量: {}, 剩余库存: {}",
productId, quantity, inventory.getQuantity());
return true;
} catch (InterruptedException e) {
log.error("等待锁被中断", e);
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
log.error("库存扣减异常", e);
return false;
} finally {
// 确保锁被正确释放
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("释放库存锁: {}", productId);
}
}
}
}
5.2 并发测试验证
@Test
public void testConcurrentInventoryDeduction() {
// 初始化库存
Long productId = 12345L;
inventoryRepository.save(new Inventory(productId, 100)); // 初始库存100
// 创建100个并发请求,每个扣减1个库存
int threadCount = 100;
CountDownLatch latch = new CountDownLatch(threadCount);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
boolean success = inventoryService.deductInventory(productId, 1);
if (success) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
}).start();
}
// 等待所有线程完成
latch.await();
// 验证结果
Inventory finalInventory = inventoryRepository.findByProductId(productId);
System.out.println("成功扣减次数: " + successCount.get());
System.out.println("最终库存: " + finalInventory.getQuantity());
System.out.println("预期库存: " + (100 - successCount.get()));
// 断言:最终库存 = 初始库存 - 成功扣减次数
assertEquals(100 - successCount.get(), finalInventory.getQuantity().intValue());
}
5.3 可重入锁示例
Redisson的锁是可重入的,同一个线程可以多次获取同一把锁:
public class ReentrantLockExample {
private RedissonClient redisson = Redisson.create();
private RLock lock = redisson.getLock("reentrantLock");
public void outerMethod() {
try {
lock.lock();
System.out.println("外层方法获得锁,持有次数: " + lock.getHoldCount());
innerMethod(); // 调用内层方法
} finally {
lock.unlock();
System.out.println("外层方法释放锁");
}
}
public void innerMethod() {
try {
lock.lock(); // 同一线程再次获取锁
System.out.println("内层方法获得锁,持有次数: " + lock.getHoldCount());
// 执行业务逻辑
System.out.println("执行内层业务逻辑");
} finally {
lock.unlock();
System.out.println("内层方法释放锁,剩余持有次数: " + lock.getHoldCount());
}
}
}
// 输出结果:
// 外层方法获得锁,持有次数: 1
// 内层方法获得锁,持有次数: 2
// 执行内层业务逻辑
// 内层方法释放锁,剩余持有次数: 1
// 外层方法释放锁
6. 公平锁vs非公平锁
6.1 概念对比
非公平锁(默认):
- 新来的线程可能直接获得锁
- 性能更好
- 可能导致某些线程长时间等待
公平锁:
- 严格按照申请锁的顺序获得锁
- 避免饥饿现象
- 性能相对较差
6.2 非公平锁示例
public class UnfairLockExample {
public static void main(String[] args) {
RedissonClient redisson = Redisson.create();
RLock unfairLock = redisson.getLock("unfairLock"); // 默认非公平锁
// 创建5个线程竞争锁
for (int i = 0; i < 5; i++) {
int threadId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
try {
unfairLock.lock();
System.out.println("线程" + threadId + "获得锁,第" + (j+1) + "次");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unfairLock.unlock();
}
}
}).start();
}
}
}
// 可能的输出(顺序不固定):
// 线程0获得锁,第1次
// 线程0获得锁,第2次 ← 同一线程连续获得锁
// 线程2获得锁,第1次
// 线程1获得锁,第1次
// ...
6.3 公平锁示例
public class FairLockExample {
public static void main(String[] args) {
RedissonClient redisson = Redisson.create();
RLock fairLock = redisson.getFairLock("fairLock"); // 公平锁
// 创建5个线程竞争锁
for (int i = 0; i < 5; i++) {
int threadId = i;
new Thread(() -> {
for (int j = 0; j < 3; j++) {
try {
fairLock.lock();
System.out.println("线程" + threadId + "获得锁,第" + (j+1) + "次");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
}).start();
}
}
}
// 输出(严格按顺序)&#x