分布式插件之ClusterID、DistributedLock、SnowFlakeID、TickID等

本文介绍一个分布式系统插件,它基于SpringBoot框架,为分布式集群系统提供一些有用的功能,包括ClusterIDDistributedLockSnowFlakeIDTickID等。

GitHub链接:https://github.com/sisyphsu/common

Maven配置

直接将此插件添加入你的maven依赖中即可:

1
2
3
4
5
<dependency>
<groupId>com.github.sisyphsu</groupId>
<artifactId>common-cluster</artifactId>
<version>1.0.4</version>
</dependency>

Configuration

此插件依赖于zookeeperredis数据源:

1
2
3
4
5
6
spring:
zookeeper:
addr: localhost:2181
redis:
host: localhost
port: 6379

功能介绍

  • ClusterID: 提供集群内竞争分配NodeID的插件,它基于ZooKeeper自动扫描未被使用的ID并锁定占用。
  • DistributedLock: 提供Multi-KEY的分布式锁解决方案,它基于Redis批量setnx多个KEY,并封装了自动Expire以及Unlock广播等功能。
  • SnowFlakeID: 提供了一个简单高效的SnowFlakeID实现方案,支持自定义bitNumClusterID
  • TickID: 基于ZooKeeperRedis实现的另外一种分布式递增趋势ID生成器,特点为短小。

ClusterID

基于ZooKeeper的临时节点等自动竞争并锁定一个全局ID,并维护锁的状态监听和失败重试等。

全局ID的取值范围可以根据参数bitNum调整,默认值是8,其取值范围是[0,256)。分配ID的规则是,优先取未被使用过的,然后再尝试竞争最久未被使用的,一般情况下,最好给集群留下充足的取值余地,尽量避免竞争。

ClusterID有三种状态:

  • NONE: 未锁定任何ID,可能是正在启动或重连ZooKeeper后ID锁被其他节点抢走而需要重新申请。
  • LOCK: 成功竞争到集群ID,直接调用 ClusterID#get()即可获取其值。
  • UNLOCK: 之前竞争到了集群ID,但是当前ZooKeeper链接出现问题而导致锁失效。

使用实例:

1
2
3
4
5
6
7
8
9
10
11
public class ClusterIDTest extends SpringBaseTest {

@Autowired
private ClusterID clusterID;

@Test
public void testID() {
log.info("clusterID: {}", clusterID.get());
}

}

提示:ClusterID#get()方法会阻塞直至状态脱离NONE

DistributedLock

这是一个特殊的分布式锁实现,技术上它利用Redisevalpubsubsetnx等指令实现,功能上它实现了multi-key批量锁定。

multi-key批量锁定在某些业务场景下可能会很有用,尤其是在批处理及复杂业务中,可能需要锁定一批数据而非单个数据。

比如在交易系统中,我们可能需要锁定 userId=1001orderId=100000001 以阻止其他节点并发修改相同的资金、订单等,具体使用实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DistributedLockTest extends SpringBaseTest {

@Autowired
private DistributedLock dlock;

@Test
public void runInTradeLock() throws Exception {
dlock.runInLock(Arrays.asList("user:1001", "order:100000001"), () -> {
System.out.println("do business");
});
}

@Test
public void runInTradeLock2() throws Exception {
List<String> keys = Arrays.asList("user:1001", "order:100000001");
if (!dlock.lock(keys, 3000)) {
throw new TimeoutException("lock failed");
}
try {
System.out.println("do business");
} finally {
dlock.unlock(keys);
}
}
}

DistributedLock会尝试锁定所有KEY,直至锁定成功或者操作超时。当锁释放时,它也会异步广播给所有关注这些KEY的节点,激活它们的等待。

SnowFlakeID

如果想了解SnowFlakeID的具体介绍,可以参考这篇文章

此插件包提供了一种灵活的SnowFlakeID实现,你可以根据自己的业务需求自定义timestamp, workId, sequenceIdbitNum。同时它直接依赖于ClusterID获取全局唯一的workId,因为ClusterID是一个interface,因此你也可以自己提供其他ClusterID实现,不一定必须使用前文提到的ZooKeeper技术方案。

需要额外说明的是,此实现的时间戳是相对于2017-01-01 00:00:00 GMT+0800的。

默认情况下,bitNum的配置如下:

  • timestamp: 39 bits, 支持时间范围为2017 ~ 2051。
  • workId: 8 bits, 最大支持256个节点的集群。
  • sequence: 6 bits, 每个节点每秒钟可以生成64k个ID。

你可以看到,这些配置项都相当保守,这么做的原因有以下两点考量:

  1. 并不需要太大的并发,64k的单节点QPS对于绝大多数应用而言已经足够大了。
  2. 不希望最终ID太大,53-bits相对而言比较小(1187276660375616),更关键的是它与JavaScript兼容。

当然如果你可以非常简单地修改以上默认bitNum配置,使用实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Slf4j
public class SnowFlakeIDTest {

@Test
public void testOne() {
int timestampBitNum = 40; // 修改默认的39
int nodeBitNum = 10; // 修改默认的8
int sequenceBitNum = 8; // 修改默认的6
SnowFlakeID flakeID = new SnowFlakeID(new ClusterID() {
@Override
public int getBitNum() {
return nodeBitNum;
}

@Override
public int get() {
return 1;
}

@Override
public ClusterIDStatus getStatus() {
return ClusterIDStatus.LOCK;
}
}, timestampBitNum, sequenceBitNum);

System.out.println(flakeID.generate());
}

}

TickID

这是另一种分布式趋势递增ID的解决方案,它的特点就是短小精悍。

对于某一些业务场景而言,普通的SnowFlakeID太长了,例如1187276660375616。你可能希望有一种更小更短的ID,就像11872766或者1187276660,此组件就提供了这样一种功能。

TickID从设计上就为了解决SnowFlakeID的问题,它不依赖于timestamp,而是将计数器统一委托给RedisZooKeeper,甚至你也可以自定义TickProvider将计数器存储在其他地方。

TickID的主要功能是异步的方式维护两个TickPool,一个用于分配ID,一个用作后备,当前一个TickPool枯竭后,后备TickPool前补然后继续异步创建新的后备TickPool

TickPool的本质是批量分配ID,根据指定的batchSize通过TickProvider累加全局计数器,成功后即可视为成功锁定一批连续的ID片段,你可以设置不同的batchSize以满足性能、连续性等要求。

具体使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class TickIDTest {

@Autowired
private TickTemplate template;

@Test
public void testNormal() {
TickID tickID = template.createTickID("user_id", 100);
System.out.println(tickID.generate());
tickID.close();
}

}

TickTemplate内部直接尝试使用ZooKeeperRedis作为全局计数器存储位置,你也可以直接提供自己的TickProvider实现类,然后手动new TickID()

Notice

  • 由于兼容性问题,Curator 的版本号必须匹配你的ZooKeeper服务版本,查看详情.