前言
分布式锁是分布式系统中一个非常关键的组件。目前业界有多种分布式锁的实现方案,比如借助 Redis、MQ、数据库、ZooKeeper 等第三方服务系统来构建。tldb 给出的分布式锁方案,核心目标在于简化设计过程,让你像使用程序中的对象锁一样,简洁可靠地获取分布式锁。
tldb 分布式锁使用方式
tldb 提供了三类核心方法:
- Lock:阻塞式请求锁
- TryLock:尝试加锁,若锁已被占用则失败返回;反之则获取该锁
- UnLock:释放已经获取的锁
tldb 的分布式锁功能主要在 MQ 模块中实现,调用的方法位于 MQ 客户端。客户端的实现其实非常轻量,除了目前已经支持的 Java、Golang、Python、JavaScript 等语言的 simpleClient 之外,有兴趣的开发者完全可以自行实现其他语言的 MQ 客户端,几乎没有技术门槛。
由于分布式锁由 tldb 服务器统一控制,它对客户端来说是跨语言的。例如,用 Java 客户端上锁的对象,其他语言同样无法获取该对象锁。
Lock(string, int) 方法的使用
tldb 提供的是以字符串为锁对象的独占锁。例如调用 lock("abc", 3),需要提供两个参数:
-
第一个参数为锁对象,即服务器对 "abc" 对象分配一把锁。所有对 "abc" 对象请求加锁的线程争用同一把独占锁。该方法为阻塞方法,请求到锁就返回;如果锁被其他线程占用,则一直阻塞直至获取到锁。
-
第二个参数表示持有该分布式锁的最长时间,单位为秒。比如 lock("abc", 3) 的意思是:如果超过 3 秒还没有调用 unlock 释放该锁,服务器会在服务端强制释放,并将锁继续分配给其他等待的线程。
UnLock(string) 方法的使用
UnLock 是释放分布式锁时调用的方法。客户端成功获取分布式锁后,服务器会返回一把锁的 key。客户端在执行完业务逻辑代码之后,必须显式调用 UnLock(key) 来释放该锁。如果没有调用 unlock 释放锁,tldb 会等待锁释放的超时时间,直至超时后强制释放该锁。
TryLock(string, int) 方法的使用
TryLock 与 Lock 相似,但行为有明显区别:
Go 语言调用示例
因为 tldb 分布式锁的实现位于 MQ 模块,所以 Go 程序必须使用 tlmq-go 这个 MQ 客户端来调用锁方法。
导入依赖包:
import "github.com/donnie4w/tlmq-go/cli"
调用 Lock 的示例程序,Lock 方法是阻塞的:
sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")
sc.Connect()
// 以上为客户端连接 MQ 服务器
key, err := sc.Lock("testlock", 3)
// Lock 中两个参数:第一个参数为字符串,即 tldb 服务器为 "testlock" 分配一个全局的分布式锁
// 第二个参数 3 为客户端持有该锁的最长时间,表示超过 3 秒没有释放锁时,
// tldb 服务器将在服务端强制释放该锁,并分配给其他请求锁的线程
if err != nil {
// 获取锁失败,需检查 tldb 是否能正常访问
} else {
defer sc.UnLock(key) // 获取锁成功后,必须在程序最后调用 UnLock
// 执行业务逻辑程序
}
调用 TryLock 的示例程序,TryLock 是非阻塞的:
sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")
sc.Connect()
if key, ok := sc.TryLock("testlock2", 3); ok {
// ok 为 true,表示已经成功获取到分布式锁
defer sc.UnLock(key) // 在程序最后释放锁对象
// ...
}
Go 语言中,还可以用自旋方式搭配 TryLock 获取分布式锁,实现类似阻塞等待的效果:
sc := cli.NewMqClient("ws://127.0.0.1:5001", "mymq=123")
sc.Connect()
var key string
for {
if v, ok := sc.TryLock("testlock", 3); ok {
key = v
break
} else {
<-time.After(100 * time.Millisecond)
}
}
defer sc.UnLock(key)
// ...
// 业务逻辑代码
这段程序比较好理解:每隔 100 毫秒循环获取一次字符串 "testlock" 的分布式锁,直到成功为止。
Java 语言调用示例
Java 客户端为 tlmq-j,项目地址:https://github.com/donnie4w/tlmq-j
Maven 配置如下:
<dependency>
<groupId>io.github.donnie4w</groupId>
<artifactId>tlmq-j</artifactId>
<version>0.0.2</version>
</dependency>
调用 Lock 方法:
MqClient mc = new SimpleClient("ws://127.0.0.1:5001", "mymq=123");
mc.connect();
// Java 连接服务器
String key = null;
try {
key = mc.lock("testlock", 3); // 获取分布式锁
// ... 执行业务逻辑程序
} finally {
if (key != null) {
mc.unLock(key); // 释放分布式锁
}
}
调用 TryLock 方法:
MqClient mc = new SimpleClient("ws://127.0.0.1:5001", "mymq=123");
mc.connect();
String key = null;
try {
key = mc.tryLock("testlock", 3); // 尝试获取分布式锁
// ... 执行业务逻辑程序
} finally {
if (key != null) {
mc.unLock(key); // 释放分布式锁
}
}
多线程并发测试数据
下面是 tldb 分布式锁的功能测试数据。
第一组测试:多线程并发调用 Lock 获取同一个对象锁

第二组测试:多线程并发使用自旋方式调用 TryLock 与 Lock 争用同一个对象锁

从测试数据可以看出,tldb 集群对分布式锁的正常加锁、排队等待、释放后再分配等行为都符合预期。TryLock 由于自旋间隔设置为 100ms,在获取锁时会有一定的时间延迟,这也是非阻塞锁的典型特征。