10分钟上手Disque分布式队列:Python/Java/Go客户端实战指南
10分钟上手Disque分布式队列:Python/Java/Go客户端实战指南
【免费下载链接】disque Disque is a distributed message broker 项目地址: https://gitcode.com/gh_mirrors/di/disque
你是否还在为分布式系统中的任务调度头疼?消息丢失、重复消费、节点故障处理等问题是否让你彻夜难眠?本文将带你一步到位掌握Disque(分布式消息代理)的客户端开发,通过Python、Java、Go三种主流语言的实战示例,让你轻松应对分布式任务队列的各种挑战。读完本文后,你将能够:
- 理解Disque的核心概念与优势
- 掌握三种语言的Disque客户端实现
- 学会处理任务的生产、消费与确认
- 解决分布式环境下的常见问题
Disque简介:分布式消息队列的新选择
Disque是一个分布式内存消息代理(Message Broker),源自Redis作者Salvatore Sanfilippo的实验性项目。它专为解决分布式系统中的任务队列问题而设计,提供了高可用、高吞吐的消息传递能力。
核心特性
Disque的主要优势包括:
- 多主架构:所有节点地位平等,支持任意节点的读写操作
- 同步复制:消息默认复制到多个节点,确保数据安全
- 灵活的投递语义:支持"至少一次"和"最多一次"两种投递模式
- 自动重试机制:消费者未确认的消息会自动重新入队
- 持久化支持:可选的AOF(Append Only File)持久化机制
详细架构说明可参考项目官方文档:README.md
与传统队列的区别
传统消息队列如RabbitMQ、Kafka各有优势,但Disque在某些场景下表现更出色:
| 特性 | Disque | RabbitMQ | Kafka |
|---|---|---|---|
| 部署复杂度 | 简单 | 中等 | 复杂 |
| 消息延迟 | 低(内存存储) | 中 | 中 |
| 持久化 | 可选 | 支持 | 支持 |
| 分布式 | 原生支持 | 需插件 | 原生支持 |
| 消息重试 | 自动 | 需配置 | 需开发 |
环境准备:快速搭建Disque服务
在开始客户端开发前,我们需要先搭建Disque服务环境。
编译安装
Disque的编译非常简单,与Redis类似,无需外部依赖:
# 克隆仓库
git clone https://gitcode.com/gh_mirrors/di/disque.git
cd disque
# 编译
make
# 查看编译结果
ls src/disque*
编译完成后,可执行文件将位于src目录下,包括disque-server(服务端)和disque-cli(命令行客户端)。
启动集群
Disque推荐使用集群模式以确保高可用。以下是启动3节点集群的步骤:
# 启动第一个节点(端口7711)
src/disque-server --port 7711 &
# 启动第二个节点(端口7712)
src/disque-server --port 7712 &
# 启动第三个节点(端口7713)
src/disque-server --port 7713 &
# 使用disque-cli将节点加入集群
src/disque-cli -p 7711 cluster meet 127.0.0.1 7712
src/disque-cli -p 7711 cluster meet 127.0.0.1 7713
配置文件示例:disque.conf
验证集群状态
使用命令行客户端验证集群是否正常工作:
# 连接到集群
src/disque-cli -p 7711
# 添加测试任务
127.0.0.1:7711> ADDJOB test_queue "hello disque" 0
D-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# 获取任务
127.0.0.1:7711> GETJOB FROM test_queue
1) 1) "test_queue"
2) "D-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
3) "hello disque"
Python客户端:简洁高效的实现
Python是Disque客户端开发的理想选择,得益于丰富的第三方库支持。我们将使用hiredis库的Python绑定来实现客户端功能。
安装依赖
pip install hiredis redis-py
生产者实现
生产者负责将任务发送到Disque队列。以下是一个基本的生产者实现:
import redis
import time
class DisqueProducer:
def __init__(self, host='localhost', port=7711):
"""初始化Disque连接"""
self.client = redis.Redis(host=host, port=port, decode_responses=True)
def add_job(self, queue_name, job_content, timeout=0, replicate=2, delay=0, retry=300, ttl=86400):
"""
添加任务到Disque队列
参数:
queue_name: 队列名称
job_content: 任务内容(字符串)
timeout: 命令超时时间(ms)
replicate: 复制节点数
delay: 延迟投递时间(s)
retry: 重试间隔(s)
ttl: 任务生存时间(s)
返回:
任务ID
"""
# 构建ADDJOB命令
args = [
'ADDJOB', queue_name, job_content, timeout,
'REPLICATE', replicate,
'DELAY', delay,
'RETRY', retry,
'TTL', ttl
]
# 执行命令
job_id = self.client.execute_command(*args)
print(f"任务已添加,ID: {job_id}")
return job_id
# 使用示例
if __name__ == "__main__":
producer = DisqueProducer()
# 连续发送10个任务
for i in range(10):
job_id = producer.add_job(
queue_name="email_queue",
job_content=f"发送邮件给用户{i}@example.com",
replicate=2, # 复制到2个节点
ttl=3600 # 1小时过期
)
time.sleep(0.1) # 控制发送速率
消费者实现
消费者负责从队列中获取任务并处理,完成后需要确认任务已处理:
import redis
import time
import threading
class DisqueConsumer:
def __init__(self, host='localhost', port=7711, queues=None):
"""初始化Disque消费者"""
self.client = redis.Redis(host=host, port=port, decode_responses=True)
self.queues = queues or []
self.running = False
self.threads = []
def process_job(self, job):
"""处理任务的业务逻辑"""
queue_name, job_id, job_content = job
print(f"处理任务 {job_id} 来自队列 {queue_name}: {job_content}")
# 这里添加实际的业务处理逻辑
# 例如:发送邮件、处理订单等
# 模拟处理耗时
time.sleep(0.5)
return True
def consume_loop(self):
"""消费循环,持续获取并处理任务"""
while self.running:
try:
# 获取任务,最多等待5秒
jobs = self.client.execute_command('GETJOB', 'TIMEOUT', 5000, 'FROM', *self.queues)
if jobs:
# 处理每个任务
for job in jobs:
success = self.process_job(job)
if success:
# 确认任务完成
self.client.execute_command('ACKJOB', job[1])
print(f"任务 {job[1]} 已确认")
else:
# 任务处理失败,使用NACK使其立即重新入队
self.client.execute_command('NACK', job[1])
print(f"任务 {job[1]} 处理失败,已重新入队")
except Exception as e:
print(f"消费过程出错: {e}")
time.sleep(1) # 出错后稍等再重试
def start(self, thread_count=1):
"""启动消费者"""
self.running = True
for _ in range(thread_count):
t = threading.Thread(target=self.consume_loop)
t.daemon = True
t.start()
self.threads.append(t)
print(f"消费者已启动,{thread_count}个线程,监听队列: {self.queues}")
def stop(self):
"""停止消费者"""
self.running = False
for t in self.threads:
t.join()
print("消费者已停止")
# 使用示例
if __name__ == "__main__":
consumer = DisqueConsumer(queues=["email_queue"])
# 启动2个消费线程
consumer.start(thread_count=2)
# 运行10秒后停止
try:
time.sleep(10)
finally:
consumer.stop()
关键代码解析
Python客户端实现中使用了以下核心命令:
- ADDJOB:添加任务到队列,对应代码中的
self.client.execute_command(*args) - GETJOB:获取任务,支持阻塞等待
- ACKJOB:确认任务完成,Disque将从集群中删除该任务
- NACK:通知任务处理失败,Disque将立即重新入队
完整的命令参考可查看源代码中的帮助定义:help.h
Java客户端:企业级应用开发
对于Java开发者,我们可以使用Jedis客户端库(Redis的Java客户端)来与Disque交互,因为Disque的协议与Redis兼容。
添加依赖
在Maven项目中添加Jedis依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
生产者实现
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
public class DisqueProducer {
private final Jedis jedis;
/**
* 构造函数
* @param host Disque服务器主机
* @param port Disque服务器端口
*/
public DisqueProducer(String host, int port) {
this.jedis = new Jedis(host, port);
// 测试连接
try {
jedis.ping();
System.out.println("成功连接到Disque服务器");
} catch (JedisConnectionException e) {
System.err.println("无法连接到Disque服务器: " + e.getMessage());
throw e;
}
}
/**
* 添加任务到Disque队列
*
* @param queueName 队列名称
* @param jobContent 任务内容
* @param timeout 命令超时时间(ms)
* @param replicate 复制节点数
* @param delay 延迟投递时间(s)
* @param retry 重试间隔(s)
* @param ttl 任务生存时间(s)
* @return 任务ID
*/
public String addJob(String queueName, String jobContent, int timeout,
int replicate, int delay, int retry, int ttl) {
try {
// 执行ADDJOB命令
String jobId = jedis.sendCommand(
new Command("ADDJOB"),
queueName, jobContent, String.valueOf(timeout),
"REPLICATE", String.valueOf(replicate),
"DELAY", String.valueOf(delay),
"RETRY", String.valueOf(retry),
"TTL", String.valueOf(ttl)
).getStatusCodeReply();
System.out.println("任务已添加,ID: " + jobId);
return jobId;
} catch (Exception e) {
System.err.println("添加任务失败: " + e.getMessage());
throw new RuntimeException("添加任务失败", e);
}
}
/**
* 关闭连接
*/
public void close() {
jedis.close();
}
// 使用示例
public static void main(String[] args) {
DisqueProducer producer = new DisqueProducer("localhost", 7711);
try {
// 添加10个测试任务
for (int i = 0; i < 10; i++) {
producer.addJob(
"payment_queue",
"处理订单 #" + System.currentTimeMillis(),
0, 2, 0, 300, 3600
);
Thread.sleep(100); // 控制发送速率
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
producer.close();
}
}
}
消费者实现
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class DisqueConsumer {
private final Jedis jedis;
private final String[] queues;
private final ExecutorService executor;
private volatile boolean running;
/**
* 构造函数
* @param host Disque服务器主机
* @param port Disque服务器端口
* @param queues 要消费的队列列表
* @param threadCount 消费线程数
*/
public DisqueConsumer(String host, int port, String[] queues, int threadCount) {
this.jedis = new Jedis(host, port);
this.queues = queues;
this.executor = Executors.newFixedThreadPool(threadCount);
this.running = false;
// 测试连接
try {
jedis.ping();
System.out.println("成功连接到Disque服务器");
} catch (JedisConnectionException e) {
System.err.println("无法连接到Disque服务器: " + e.getMessage());
throw e;
}
}
/**
* 处理任务的业务逻辑
* @param job 任务信息,格式: [队列名, 任务ID, 任务内容]
* @return 是否处理成功
*/
private boolean processJob(List<String> job) {
if (job.size() < 3) {
System.err.println("无效的任务格式: " + job);
return false;
}
String queueName = job.get(0);
String jobId = job.get(1);
String jobContent = job.get(2);
System.out.println("处理任务 " + jobId + " 来自队列 " + queueName + ": " + jobContent);
// 这里添加实际的业务处理逻辑
try {
// 模拟处理耗时
Thread.sleep(500);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} catch (Exception e) {
System.err.println("处理任务 " + jobId + " 出错: " + e.getMessage());
return false;
}
}
/**
* 消费循环
*/
private void consumeLoop() {
Jedis localJedis = new Jedis(jedis.getClient().getHost(), jedis.getClient().getPort());
while (running) {
try {
// 获取任务,最多等待5秒
List<Object> jobs = localJedis.sendCommand(
new Command("GETJOB"),
"TIMEOUT", "5000", "FROM", queues
).getObjectMultiBulkReply();
if (jobs != null && !jobs.isEmpty()) {
for (Object jobObj : jobs) {
@SuppressWarnings("unchecked")
List<String> job = (List<String>) jobObj;
boolean success = processJob(job);
if (success) {
// 确认任务完成
localJedis.sendCommand(new Command("ACKJOB"), job.get(1));
System.out.println("任务 " + job.get(1) + " 已确认");
} else {
// 任务处理失败,使用NACK使其立即重新入队
localJedis.sendCommand(new Command("NACK"), job.get(1));
System.out.println("任务 " + job.get(1) + " 处理失败,已重新入队");
}
}
}
} catch (Exception e) {
System.err.println("消费过程出错: " + e.getMessage());
try {
Thread.sleep(1000); // 出错后稍等再重试
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
localJedis.close();
System.out.println("消费线程已退出");
}
/**
* 启动消费者
*/
public void start() {
if (running) {
System.out.println("消费者已经在运行中");
return;
}
running = true;
int threadCount = ((ExecutorService) executor).toString().contains("FixedThreadPool") ?
((java.util.concurrent.ThreadPoolExecutor) executor).getCorePoolSize() : 1;
for (int i = 0; i < threadCount; i++) {
executor.submit(this::consumeLoop);
}
System.out.println("消费者已启动," + threadCount + "个线程,监听队列: " + String.join(",", queues));
}
/**
* 停止消费者
*/
public void stop() {
running = false;
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
jedis.close();
System.out.println("消费者已停止");
}
// 使用示例
public static void main(String[] args) {
String[] queues = {"payment_queue"};
DisqueConsumer consumer = new DisqueConsumer("localhost", 7711, queues, 2);
// 启动消费者
consumer.start();
// 运行30秒后停止
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
consumer.stop();
}
}
}
Java客户端核心实现参考了Disque命令行工具的代码逻辑:disque-cli.c
Go客户端:高性能的选择
Go语言以其出色的并发性能和简洁的语法,成为开发高性能分布式系统的理想选择。下面我们将实现一个Go语言的Disque客户端。
依赖选择
Go语言没有官方的Disque客户端,但我们可以使用支持自定义命令的Redis客户端库,如github.com/go-redis/redis/v8。
go get github.com/go-redis/redis/v8
生产者实现
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
// DisqueProducer Disque生产者
type DisqueProducer struct {
client *redis.Client
ctx context.Context
}
// NewDisqueProducer 创建新的Disque生产者
func NewDisqueProducer(addr string) *DisqueProducer {
client := redis.NewClient(&redis.Options{
Addr: addr,
})
ctx := context.Background()
// 测试连接
_, err := client.Ping(ctx).Result()
if err != nil {
log.Fatalf("无法连接到Disque服务器: %v", err)
}
return &DisqueProducer{
client: client,
ctx: ctx,
}
}
// AddJob 添加任务到Disque队列
func (p *DisqueProducer) AddJob(queueName, jobContent string, timeout, replicate, delay, retry, ttl int) (string, error) {
// 构建ADDJOB命令参数
args := []interface{}{
"ADDJOB", queueName, jobContent, timeout,
"REPLICATE", replicate,
"DELAY", delay,
"RETRY", retry,
"TTL", ttl,
}
// 执行命令
jobID, err := p.client.Do(p.ctx, args...).String()
if err != nil {
return "", fmt.Errorf("添加任务失败: %v", err)
}
log.Printf("任务已添加,ID: %s", jobID)
return jobID, nil
}
// Close 关闭连接
func (p *DisqueProducer) Close() error {
return p.client.Close()
}
func main() {
// 创建生产者
producer := NewDisqueProducer("localhost:7711")
defer producer.Close()
// 连续添加10个任务
for i := 0; i < 10; i++ {
jobContent := fmt.Sprintf("生成报表 #%d", i)
_, err := producer.AddJob(
"report_queue",
jobContent,
0, // timeout
2, // replicate
0, // delay
300, // retry
3600, // ttl
)
if err != nil {
log.Printf("添加任务失败: %v", err)
}
time.Sleep(100 * time.Millisecond)
}
}
消费者实现
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/go-redis/redis/v8"
)
// DisqueConsumer Disque消费者
type DisqueConsumer struct {
client *redis.Client
ctx context.Context
queues []string
running bool
wg sync.WaitGroup
threadCount int
}
// NewDisqueConsumer 创建新的Disque消费者
func NewDisqueConsumer(addr string, queues []string, threadCount int) *DisqueConsumer {
client := redis.NewClient(&redis.Options{
Addr: addr,
})
ctx := context.Background()
// 测试连接
_, err := client.Ping(ctx).Result()
if err != nil {
log.Fatalf("无法连接到Disque服务器: %v", err)
}
return &DisqueConsumer{
client: client,
ctx: ctx,
queues: queues,
threadCount: threadCount,
}
}
// ProcessJob 处理任务的业务逻辑
func (c *DisqueConsumer) ProcessJob(job []interface{}) bool {
if len(job) < 3 {
log.Printf("无效的任务格式: %v", job)
return false
}
queueName, _ := job[0].(string)
jobID, _ := job[1].(string)
jobContent, _ := job[2].(string)
log.Printf("处理任务 %s 来自队列 %s: %s", jobID, queueName, jobContent)
// 这里添加实际的业务处理逻辑
// 模拟处理耗时
time.Sleep(500 * time.Millisecond)
return true
}
// consumeLoop 消费循环
func (c *DisqueConsumer) consumeLoop() {
defer c.wg.Done()
for c.running {
// 构建GETJOB命令
args := []interface{}{"GETJOB", "TIMEOUT", 5000, "FROM"}
args = append(args, c.queues...)
// 执行命令
result, err := c.client.Do(c.ctx, args...).Result()
if err != nil {
log.Printf("获取任务失败: %v", err)
time.Sleep(1 * time.Second)
continue
}
if result == nil {
// 超时,继续循环
continue
}
// 解析结果
jobs, ok := result.([]interface{})
if !ok {
log.Printf("无效的任务列表格式: %v", result)
continue
}
// 处理每个任务
for _, jobObj := range jobs {
job, ok := jobObj.([]interface{})
if !ok {
log.Printf("无效的任务格式: %v", jobObj)
continue
}
success := c.ProcessJob(job)
jobID, _ := job[1].(string)
if success {
// 确认任务完成
_, err := c.client.Do(c.ctx, "ACKJOB", jobID).Result()
if err != nil {
log.Printf("确认任务 %s 失败: %v", jobID, err)
} else {
log.Printf("任务 %s 已确认", jobID)
}
} else {
// 任务处理失败,使用NACK使其立即重新入队
_, err := c.client.Do(c.ctx, "NACK", jobID).Result()
if err != nil {
log.Printf("NACK任务 %s 失败: %v", jobID, err)
} else {
log.Printf("任务 %s 处理失败,已重新入队", jobID)
}
}
}
}
}
// Start 启动消费者
func (c *DisqueConsumer) Start() {
if c.running {
log.Println("消费者已经在运行中")
return
}
c.running = true
c.wg.Add(c.threadCount)
for i := 0; i < c.threadCount; i++ {
go c.consumeLoop()
}
log.Printf("消费者已启动,%d个线程,监听队列: %v", c.threadCount, c.queues)
}
// Stop 停止消费者
func (c *DisqueConsumer) Stop() {
c.running = false
c.wg.Wait()
c.client.Close()
log.Println("消费者已停止")
}
func main() {
consumer := NewDisqueConsumer("localhost:7711", []string{"report_queue"}, 2)
// 启动消费者
consumer.Start()
// 运行30秒后停止
time.Sleep(30 * time.Second)
consumer.Stop()
}
Go客户端实现参考了Disque服务器的网络处理模块:networking.c
高级特性与最佳实践
处理节点故障
Disque的分布式特性使其具备良好的容错能力,但客户端仍需处理可能的节点故障:
# Python客户端添加重试逻辑示例
def add_job_with_retry(self, max_retries=3, **kwargs):
retries = 0
while retries < max_retries:
try:
return self.add_job(**kwargs)
except Exception as e:
retries += 1
if retries >= max_retries:
raise
print(f"添加任务失败,正在重试 ({retries}/{max_retries}):{e}")
time.sleep(0.5 * (2 ** retries)) # 指数退避
监控与统计
Disque提供了一些命令来监控队列状态和统计信息:
# 查看队列信息
disque-cli -p 7711 QINFO my_queue
# 查看节点信息
disque-cli -p 7711 INFO
# 查看集群信息
disque-cli -p 7711 CLUSTER INFO
性能优化
为获得最佳性能,建议采用以下优化措施:
- 批量操作:在可能的情况下,批量处理任务
- 合理设置重试参数:避免过于频繁的重试
- 适当的线程/协程数:根据CPU核心数调整
- 网络优化:客户端与服务端尽量部署在同一局域网
总结与展望
Disque作为一个轻量级的分布式消息队列,提供了简单而强大的功能,适合许多分布式任务调度场景。本文介绍了如何使用Python、Java和Go三种语言开发Disque客户端,涵盖了任务的生产、消费和确认等核心操作。
学习资源
下一步
- 探索Disque的持久化配置:disque.conf
- 深入理解集群管理:cluster.c
- 研究任务复制机制:job.c
Disque虽然是一个实验性项目,但其设计思想和实现细节对理解分布式系统有很大帮助。在实际生产环境中使用时,建议先进行充分的测试和评估。
希望本文能帮助你快速掌握Disque客户端开发,如果你有任何问题或建议,欢迎在项目仓库提交issue或PR。
本文代码示例已同步至项目的examples目录,欢迎参考和改进。
参考资料
【免费下载链接】disque Disque is a distributed message broker 项目地址: https://gitcode.com/gh_mirrors/di/disque
更多推荐



所有评论(0)