Apache Ignite 的分布式锁Distributed Locks的介绍
以下这段内容是关于 Apache Ignite 的分布式锁(Distributed Locks) 的介绍。这是一个非常重要的功能,用于在分布式系统中协调多个节点对共享资源的并发访问。
下面我们来一步步深入理解它。
🎯 一、一句话理解:什么是 Ignite 分布式锁?
Ignite 分布式锁是一个跨多个服务器节点的“互斥锁”,确保同一时间只有一个节点可以操作某个共享数据(比如缓存中的某个 key)。
✅ 类比:
- 就像一把“全球唯一的钥匙”:只有拿到这把钥匙的线程才能修改某个数据。
- 单机环境下用
synchronized
或ReentrantLock
; - 分布式环境下就需要
IgniteCache.lock()
这种跨 JVM 的锁。
🧩 二、核心概念解析
1️⃣ IgniteCache.lock(key)
—— 获取一个分布式锁
Lock lock = cache.lock("keyLock");
- 这个
lock
是一个实现了java.util.concurrent.locks.Lock
接口的对象。 - 它不是本地锁!它是集群范围内的分布式锁。
- 当你在 Node A 上调用
lock.lock()
,Node B 和 Node C 上试图对同一个 key 加锁的线程都会阻塞等待,直到 Node A 释放锁。
2️⃣ 使用方式:try-finally 确保释放
lock.lock(); // 阻塞直到获取锁
try {// 安全地操作共享资源cache.put("Hello", 11);cache.put("World", 22);
} finally {lock.unlock(); // 必须释放,否则死锁!
}
⚠️ 注意:必须放在
finally
块中释放,防止异常导致锁未释放,造成死锁或资源饥饿。
3️⃣ lockAll(keys)
—— 批量加锁
Collection<String> keys = Arrays.asList("key1", "key2", "key3");
Lock lock = cache.lockAll(keys);
lock.lock();
try {// 同时锁定多个 keycache.put("key1", 1);cache.put("key2", 2);cache.put("key3", 3);
} finally {lock.unlock();
}
- 适用于需要原子性地操作多个 key 的场景。
- 所有 key 的锁会一起获取、一起释放。
- 避免因部分加锁成功而导致的数据不一致问题。
🔐 三、为什么需要分布式锁?
在分布式系统中,多个节点可能同时访问同一份数据。例如:
场景 | 问题 | 解决方案 |
---|---|---|
多个节点同时更新用户余额 | 超卖、余额错乱 | 对 userId 加分布式锁 |
多个节点争抢执行定时任务 | 重复执行 | 对 "task-refresh" 加锁 |
缓存双写一致性 | 缓存和数据库不一致 | 更新时对 key 加锁 |
👉 没有锁 → 数据竞争(Race Condition) → 数据错误!
⚙️ 四、Atomicity Mode:必须是 TRANSACTIONAL
CacheConfiguration cfg = new CacheConfiguration("myCache");
cfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); // 必须设置
- Ignite 支持两种原子性模式:
ATOMIC
:高性能,无事务支持,不能使用显式锁。TRANSACTIONAL
:支持事务和显式分布式锁。
❌ 如果你在
ATOMIC
模式下调用cache.lock()
,会抛出异常!
✅ 所以:要用分布式锁,缓存必须配置为 TRANSACTIONAL
模式。
🔄 五、Locks vs Transactions:锁与事务的关系
这是最容易混淆的部分,原文说得很清楚:
“Explicit locks are not transactional and cannot be used from within transactions.”
我们来拆解这句话:
✅ 情况 1:显式锁 ≠ 事务锁
类型 | 显式锁 (cache.lock() ) | 事务中的锁 |
---|---|---|
是否可嵌套在事务中 | ❌ 不可以 | ✅ 可以 |
是否自动提交/回滚 | ❌ 不支持回滚 | ✅ 支持 |
如何获取 | 手动 lock.lock() | 自动由事务管理器获取 |
使用场景 | 非事务性临界区 | 事务性数据操作 |
🔴 错误写法(会抛异常):
IgniteTransactions txs = ignite.transactions();
try (Transaction tx = txs.txStart()) {Lock lock = cache.lock("key");lock.lock(); // ❌ 抛异常!不能在事务中使用显式锁cache.put("key", 1);tx.commit();
}
✅ 情况 2:想要“事务中的显式锁”?用 PESSIMISTIC 事务
如果你希望在事务中也能“显式控制锁”的行为(比如立即失败而不是等待),应该使用:
try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.PESSIMISTIC, // 悲观并发控制TransactionIsolation.REPEATABLE_READ)) {// 第一次读/写就会自动加锁Integer val = cache.get("key");cache.put("key", val + 1);tx.commit(); // 提交时释放锁
}
悲观事务(PESSIMISTIC)的特点:
- 在
get()
或put()
时立即尝试获取分布式锁。 - 如果锁被占用,可以选择超时失败(避免无限等待)。
- 行为类似于“显式锁 + 事务”的组合效果。
🧪 六、完整示例:银行转账(防止并发超支)
IgniteCache<String, Integer> cache = ignite.cache("accounts");// 模拟两个账户
String from = "account-A";
String to = "account-B";// 对两个账户加锁(避免死锁:按字母顺序加锁)
List<String> sortedKeys = Arrays.asList(from, to).stream().sorted().collect(Collectors.toList());
Lock lock = cache.lockAll(sortedKeys);lock.lock();
try {Integer balanceA = cache.get(from);Integer balanceB = cache.get(to);if (balanceA >= 100) {cache.put(from, balanceA - 100);cache.put(to, balanceB + 100);System.out.println("转账成功");} else {System.out.println("余额不足");}
} finally {lock.unlock(); // 释放所有锁
}
✅ 保证了即使多个节点同时发起转账,也不会出现“超卖”。
⚠️ 七、注意事项 & 最佳实践
项目 | 建议 |
---|---|
🔒 锁粒度 | 尽量小(比如按用户 ID 锁),避免锁整个缓存 |
⏱️ 锁持有时间 | 越短越好,不要在锁内做耗时操作(如网络请求) |
💥 异常处理 | 一定要 finally unlock() ,建议用 try-with-resources(如果自定义封装) |
🪢 死锁风险 | 多 key 加锁时,按固定顺序加锁(如排序) |
📈 性能影响 | 分布式锁涉及网络通信,频繁使用会影响性能 |
🔄 替代方案 | 考虑使用 EntryProcessor (invoke() )进行原子更新,避免手动加锁 |
✅ 总结:一句话掌握精髓
Ignite 的
cache.lock(key)
提供了一种简单、直观的跨节点互斥机制,让你像使用本地ReentrantLock
一样保护分布式共享资源,但前提是缓存必须是TRANSACTIONAL
模式,并且不能与事务混用。
🔄 对比总结表
功能 | cache.lock() 显式锁 | 悲观事务(PESSIMISTIC) | EntryProcessor (invoke) |
---|---|---|---|
是否跨节点 | ✅ 是 | ✅ 是 | ✅ 是 |
是否支持事务 | ❌ 否 | ✅ 是 | ✅ 是(单 key) |
是否自动加锁 | ✅ 手动 | ✅ 自动 | ✅ 自动 |
适用场景 | 非事务临界区 | 多 key 事务操作 | 单 key 原子更新 |
性能 | 中等 | 中等 | 高(推荐) |
如果你想实现高并发下的安全更新,优先考虑 EntryProcessor
;如果逻辑复杂必须加锁,再用 lock()
或 悲观事务。
如有具体业务场景(如库存扣减、计数器、任务调度),欢迎继续提问,我可以给出更具体的代码建议!