Java 基于 ZooKeeper 实现分布式锁需要注意什么
在前一篇有关 Redis 分布式锁的文章中,我们讨论了几点有关分布式锁的要求: 1. 操作原子性 2. 可重入性 3. 效率
为了满足上述条件,采用 本地锁 + Redis 锁
的方式解决了问题。不过在文章末尾提到,Redis
不保证强一致性,因此对一致性要求很高的场景会存在安全隐患。
本文将讨论使用满足 CP 要求的 ZooKeeper 来实现强一致性的分布式锁。
在前一篇有关 Redis 分布式锁的文章中,我们讨论了几点有关分布式锁的要求: 1. 操作原子性 2. 可重入性 3. 效率
为了满足上述条件,采用 本地锁 + Redis 锁
的方式解决了问题。不过在文章末尾提到,Redis
不保证强一致性,因此对一致性要求很高的场景会存在安全隐患。
本文将讨论使用满足 CP 要求的 ZooKeeper 来实现强一致性的分布式锁。
rust 的 module system,类似 java 的 package,可以用于将代码分别放置在适合他们的单元内。同时 rust 还允许用户控制 module 之间的可见性(public/private)。
在 module 内,rust 允许用户放置 function,struct,trait,struct implement 以及 child module。
rust 对 module 的要求是任何 module 都应处在一颗以 root module 为根的 module tree 中。期望访问某个 module 时,能够找到一条通路从root一路向下直到该 module。
不说什么具体的知识,我们先一步步的,来写个最简单的测试,并且让他啊跑起来,看看
rust 下的测试是什么样子的: 1. 创建个 lib
工程:cargo new simplest-test --lib 2. 在
src/lib.rs 里面,rust 已经自动帮我们写下了如下代码:
1
2
3
4
5
6
7#[cfg(test)]
mod test {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}cargo test 1
2
3
4
5
6
7
8
9
10
11
12
13
14 Compiling simplest-test v0.1.0 (/User/xxx/simplest-test)
Finished dev [unoptimized + debuginfo] target(s) in 5.91s
Running target/debug/deps/simplest_test-c430fbaec5f55b85
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests simplest-test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
经过上述三步,我们已经创建了一个最简单的测试工程,并且运行了自带的测试。
在代码层面,测试本身无需多说,结构上我们看到,与业务代码不同,测试在
module 上增加了 attribute: #[cfg(test)],在测试方法上增加了
attribute: #[test]。 -
#[cfg(test)]:配置编译条件,只有在 test
模式下,被标记的代码块才会被编译(换句话说,它确保 release
中不包含测试代码) -
#[test]:被标记的方法将被视为测试来执行
在 output 中,还包含了两部分“running x test”,第一部分是我们已有的测试,第二部分为文档测试,本文暂不涉及。
在如今这样一个张口分布式,闭口微服务的软件开发趋势下,多实例似乎已经不是某种选择而是一个无需多说的基本技术要求了。
多实例为我们带来稳定性提升的同时,也伴随着更复杂的技术要求,原先在本地即可处理的问题,全部扩展为分布式问题,其中就包含我们今天会聊到的多实例同步即分布式锁问题。JDK 提供的锁实现已经能够非常好的解决本地同步问题,而扩展到多实例环境下,Redis、ZooKeeper 等优秀的实现也使得我们使用分布式锁变得更加简单。
其实对于分布式锁的原理、分布式锁的 Redis 实现、ZK 实现等等各类文章不计其数,然而只要简单一搜就会发现,大多数文章都在教大家 Redis 分布式锁的原理和实现方法,但却没有几篇会写什么实现是好的实现,是适合用于生产环境,高效而考虑全面的实现。这将是本文讨论的内容。
本文的标题实际上来自于一次与项目上同事中午吃饭时的讨论:
A: 我觉得我们现在的抽象有点多,infra 层里面每一个类都抽取了接口,这些被调用的类多半只有一个实现, 我们是不是做的太细了?
B: 从依赖倒置的角度讲,domain 层和 service 层并不应该直接调用 infra 层的实现,因此我们确实是需要每一个实现都抽一个接口出来。
A: 那依赖倒置就是每一个实现都要抽一个接口出来吗?
B: 这个...
看来小伙伴 A 不经意间触碰到了 S.O.L.I.D. 的深水区...
相比于单一职责、开闭、接口隔离等原则,依赖倒置与里氏替换类似,属于更偏向操作指导的一类原则,比如从依赖倒置的定义来看:
依赖倒置:高层模块不应直接依赖低层模块,他们都应该依赖于彼此间的抽象。
以开发的角度理解:高层不要直接调用低层,而是调用抽取出来的接口。
那这么说,依赖倒置就是每一个实现都要抽一个接口出来吗?
为了解释这个问题,我们尝试来提出一个新的问题:为啥要依赖倒置?
1 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
任何体验过 JDK 集合框架的人都了解且喜爱java.util.Collections中的工具。
Guava 在这方面提供了更多工具:应用于所有集合的静态方法。 而且这些是
Guava 最流行也最成熟的部分。
与特定 interface 相关联的方法以相对直观的规则来分组:
| Interface | JDK or Guava? | Corresponding Guava utility class |
|---|---|---|
Collection |
JDK | Collections2 |
List |
JDK | Lists |
Set |
JDK | Sets |
SortedSet |
JDK | Sets |
Map |
JDK | Maps |
SortedMap |
JDK | Maps |
Queue |
JDK | Queues |
Multiset |
Guava | Multisets |
Multimap |
Guava | Multimaps |
BiMap |
Guava | Maps |
Table |
Guava | Tables |
在寻找变换、过滤等工具吗? 它们在我们的功能性语法下的 functional 文章里.
Guava 引入了许多 JDK 未包含但我们却广泛使用的的新集合类型。他们全都设计为可以和 JDK 的集合框架友好的共存, 而不是硬塞入 JDK 的集合类抽象中。
一般来说,Guava 的集合会精确地按照 JDK interface 所定义的契约来实现。
在传统单体应用中,我们用锁来保证非幂等操作在并发调用时不会产生意外的结果。最常见的应用场景应该就是 MySQL 用锁保证写表时不会重复写入或读表时读到脏数据。
进入微服务时代,整个业务系统可能是由数十个不同的服务节点组成,每个服务节点还包括多个实例确保高可用。在这样的环境下,一个写请求可能会由于负载均衡通过不同的服务实例操作数据,大多 NoSQL 实现为了并发性而牺牲了事务,则可能导致数据的正确性被破坏。这时如果有一个全局锁来对不同服务的操作进行限制,那么会一定程度解决上述问题。(对于复杂场景还需要采用分布式事务来处理回滚等等。)
与本地锁类似,分布式锁也是独立的对象,只不过存储在独立的节点上。最朴素的方法是在数据库中存储一段数据,以此为锁对象,存在则表示锁已被其他服务获取,不存在则表示可获取。当然此方案完全没考虑过死锁、可重入性等问题,而且如果是用关系型数据库来实现,则无法支撑高并发的场景。因此通常我们会采用 Redis、ZooKeeper 等方案来实现,并对锁代码进行一定设计,增加超时、重试等等功能。
1 | public static final ImmutableSet<String> COLOR_NAMES = ImmutableSet.of( |