10分钟上手Disque分布式队列:Python/Java/Go客户端实战指南

【免费下载链接】disque Disque is a distributed message broker 【免费下载链接】disque 项目地址: 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客户端实现中使用了以下核心命令:

  1. ADDJOB:添加任务到队列,对应代码中的self.client.execute_command(*args)
  2. GETJOB:获取任务,支持阻塞等待
  3. ACKJOB:确认任务完成,Disque将从集群中删除该任务
  4. 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

性能优化

为获得最佳性能,建议采用以下优化措施:

  1. 批量操作:在可能的情况下,批量处理任务
  2. 合理设置重试参数:避免过于频繁的重试
  3. 适当的线程/协程数:根据CPU核心数调整
  4. 网络优化:客户端与服务端尽量部署在同一局域网

总结与展望

Disque作为一个轻量级的分布式消息队列,提供了简单而强大的功能,适合许多分布式任务调度场景。本文介绍了如何使用Python、Java和Go三种语言开发Disque客户端,涵盖了任务的生产、消费和确认等核心操作。

学习资源

下一步

Disque虽然是一个实验性项目,但其设计思想和实现细节对理解分布式系统有很大帮助。在实际生产环境中使用时,建议先进行充分的测试和评估。

希望本文能帮助你快速掌握Disque客户端开发,如果你有任何问题或建议,欢迎在项目仓库提交issue或PR。

本文代码示例已同步至项目的examples目录,欢迎参考和改进。

参考资料

  1. Disque官方文档:README.md
  2. Disque命令参考:help.h
  3. Disque源代码:src/
  4. Disque测试用例:tests/

【免费下载链接】disque Disque is a distributed message broker 【免费下载链接】disque 项目地址: https://gitcode.com/gh_mirrors/di/disque

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐