java队列_实现一个消息队列——MyMQ

2e2dd0ec7e2a068254b14ca05e8553e6.png

想要源码可以访问我的github(记得帮我点个小星星 ):

Anonymoushhh/MyMQ​github.com
152560715b6686384d2ac742808beb4c.png

MyMQ简介

MyMQ是一个简单版的消息队列,它的架构主要分为三部分:Producer,Broker和Consumer。

df2d3135ca478933b083155f690d4049.png

生产者支持同步发送消息和发送单向消息,生产者发送消息时需先通过一个消息主题向Broker申请队列,Broker根据自己的负载情况返回给生产者可用队列号,生产者将队列号添加到topic中,并用该消息主题发送消息;

Broker中有许多队列,每个队列中消息顺序一定,队列对消息主题Topic可以是多对多,一对多,多对一的关系,具体如何使用由使用者决定。Broker支持负载均衡和消息过滤功能,对消费者提供Push和Pull两种模式。Broker还实现了主从同步(Slave节点)和队列持久化存储与恢复来保证消息的可靠性。若消息由于网络原因发送失败时会重试,默认为16次,发送成功(返回ACK)或返回失败消息后才会发送下一条消息,以此来保证消息的有序性;

消费者可以同步获取消息,延时获取消息,支持Push和Pull两种模式。

Producer,Broker和Consumer三者支持单机和分布式环境,通过NIO的Socket通信。

Producer,Broker和Consumer三者均支持横向扩展,增加新的机器对旧的服务没有任何影响,保证了高可用性。

MyMQ架构

Broker

  • Broker.java
  • BrokerResponseProcessor.java
  • Filter.java
  • LoadBalancer.java
  • MyQueue.java
  • Slave.java
  • SlaveResponseProcessor.java
  • Synchronizer.java

Broker包的作用主要是创建Broker实例对象,以及提供主从同步,负载均衡,消息过滤服务。

Common

  • IpNode.java
  • Message.java
  • MessageType.java
  • PullMessage.java
  • RegisterMessage.java
  • Topic.java

Common包定义了一些通用的类,如消息类,地址类等。

Consumer

  • ConsumerFactory.java
  • ConsumerResponseProcessor.java

消费者包定义了消费者工厂,可通过工厂方法添加消费者。

Producer

  • DelaySyscProducerFactory.java
  • SyscProducerFactory.java
  • UnidirectionalProducerFactory.java

生产者包定义了生产者工厂,支持同步生产者工厂,延时生产者工厂和单向生产者工厂。

Test

  • ConsumerTest.java
  • DaoTest.java
  • BrokerTest.java
  • ProducerTest.java
  • PressTest.java

测试包,里面包含了MyMQ的基本使用方法。

Utils

  • Client.java
  • DefaultRequestProcessor.java
  • DefaultResponseProcessor.java
  • JsonFormatUtil.java
  • PersistenceUtil.java
  • MessageUtil.java
  • RequestProcessor.java
  • ResponseProcessor.java
  • SequenceUtil.java
  • SerializeUtil.java
  • Server.java

工具包,定义了一些通用的工具类。

MyMQ使用指南

Broker.Broker

Broker为消息队列服务器节点,提供的服务有:消息存储,消息分发(Push模式与Pull模式),失败重试机制,消息过滤,负载均衡,死信队列,主从备份,持久化存储(同步或异步刷盘)与冗机恢复,横向扩展等。

eb17dee02e24ade7b22c7639cbd90906.png

09b07ee047959f7a7fe7992d9ea7d4f5.png

Broker.BrokerResponseProcessor

该类实现了ResponseProcessor接口,为Broker制定了特殊的消息响应机制。

815e1587c4587ee521736538edf56a50.png

Broker.Filter

消息过滤器,将消息按照消费者地址分类。

0ff28d9d029315652188cae61b7268a4.png

Broker.LoadBalancer

负载均衡器,用于为生产者选择一个合适的消息队列。

b7793803b9be54705b8eab56a09a79c2.png

Broker.MyQueue

消息队列类,保证了消息的顺序性。

6f0063c4aa9d8c4c7179e54b170742d0.png

Broker.Slave

备份节点类,用于Slave的同步或异步备份。

cc2e92f7b85631d2512591531ce66fc6.png

Broker.SlaveResponseProcessor

用于指定备份节点的特殊消息响应机制。

dc1f0eff98a5f6142a7a85136558af55.png

Broker.Synchronizer

同步器,用于Broker主从节点的同步。

537c8a5f93c4314408b0130f1265296e.png

Common.IpNode

定义一个网络地址。

95bd578589eec1084f6cff22794a8886.png

Common.Message

定义了传输的消息结构。

35a2337b89c053f819e7462c02b12366.png

Common.MessageType

定义了消息类型。

a28e1b9457901aa31fe755c05d78413f.png

Common.PullMessage

一种特殊的消息,用于消费者向Broker拉取消息。

b526b631621418ab37c75e0cfb3a2af9.png

Common.RegisterMessage

一种特殊的消息,用与消费者向Broker注册。

36087de0a6c72512e9938c67b5f849e6.png

Common.Topic

消息主题。

c657e3eca80f5bc94900bbaad8a56d89.png

Consumer.ConsumerFactory

消费者工厂类,用于创建消费者。

926bcf2d3efb3bd55e1ff48049ae364e.png

Consumer.ConsumerResponeProcessor

为消费者指定特殊的消息响应机制。

eda66be67ad57620e7632e43875f01f6.png

Producer.SyscProducerFactory

同步生产者工厂。

5ee968a11a0ace1b9d3f1c0c8d049c1b.png

Producer.DelaySyscProducerFactory

延时生产者工厂。

15e43203776b67dcb7cd4711dd205220.png

Producer.UndirectionalProducerFactory

单向消息生产者工厂。 API同SyscProducerFactory。

Utils.Client

NIO通信模型客户端类,用于发送消息和接受回复。

182fdc6114330606e3e3a207a371eb8e.png

Utils.DefaultRequestProcessor

默认的请求接收响应类。

4fe51d4cea0579b98a11ab61e7a2887a.png

Utils.DefaultResponeProcessor

默认的请求回复响应类。

a77fcd0d6ea7d08ad89fd128349d0e25.png

Utils.RequestProcessor接口

请求接收响应接口。

f301b8406de138aed47e7f7d9a2d4a37.png

Utils.ResponseProcessor接口

请求回复响应接口。

60acf14b47a1d28e2b97a4541bd7e435.png

Utils.SequenceUtil

生成唯一序列号的工具类(单机唯一)。

4ed574e9bc236e75b40344c6b1d1aecb.png

Utils.SerializeUtil

序列化工具类。

e9521a68993216b7a2cb257ca7698245.png

Utils.Server

NIO通信模型服务器类,在某个端口上监听消息。

f063b5233de2cadb2d9c3a895b0e16b2.png

使用示例

Producer

SequenceUtil 

Broker

//创建Broker(主从复制,push模式)

Consumer

//创建Consumer(Push模式)
        IpNode ipNode1 = new IpNode("127.0.0.1", 81);
        IpNode ipNode2 = new IpNode("127.0.0.1", 8888);//消费者地址
        try {
            ConsumerFactory.createConsumer(ipNode1, ipNode2);
        } catch (IOException e1) {
            System.out.println("Broker未上线!");
        }
        while(true) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Message m1 = ConsumerFactory.getMessage(8888);
            if(m1!=null) 
                System.out.println("消费者"+ipNode2.getIp()+ipNode2.getPort()+"收到消息:"+m1.getMessage());    
        }
//创建Consumer(Pull模式)
        IpNode ipNode3 = new IpNode("127.0.0.1", 81);
        IpNode ipNode4 = new IpNode("127.0.0.1", 8888);
        try {
            ConsumerFactory.createConsumer(ipNode3, ipNode4);
        } catch (IOException e) {
            System.out.println("Broker未上线!");
        }
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            ConsumerFactory.Pull(ipNode3, ipNode4);
    }

主要架构与功能实现详解

消息结构

public class Message implements Serializable{

    private static final long serialVersionUID = 1L;
    private int num;//消息序号
    private String message;//消息
    private int type;//消息类型
    private Topic topic;//消息主题
    ...
    }

Message类实现了序列化接口,每个Message有消息序号(该序号是否具有唯一性由使用者决定),消息内容,消息类型和消息主题。消息内容由使用者自己定义,可以是某个手机号(用于给该手机号发送短信)或订单信息(用于更新数据库)等等。消息类型定义了5种:

public static final int ONE_WAY = 0;//单向消息
    public static final int REPLY_EXPECTED = 1;//需要得到回复的消息
    public static final int REQUEST_QUEUE = 2;//请求包,用户生产者向Broker申请队列
    public static final int REGISTER = 3;//用于消费者向Broker注册
    public static final int PULL = 4;//用于消费者向Broker注册

消息主题类Topic定义如下:

private HashSet<Integer> queueId;//该Topic在Broker中对应的queueId
    private HashSet<IpNode> consumer_address;//该Topic对应的cunsumer
    String topic_name;//主题名称
    int queueNum;//请求队列数

该类同样实现了序列化接口,主要用于记录消息主题名称,请求队列数,请求队列号和消费者地址。当用户首次定义一个Topic时,需要向Broker申请分配可用的消息队列号,之后将可用的队列号存储进Topic中,以后使用该Topic时就无需申请队列。

消息存储

public class MyQueue implements Serializable{
    private static final long serialVersionUID = 1L;
    private ConcurrentLinkedDeque<Message> queue;
    }

MyQueue定义了消息存储队列,它的实现是一个同步的双向队列,一个Broker中可以同时存在一个或多个队列。

消息过滤

public HashMap<IpNode, List<Message>> filter(List<Message> list) {
        //将Message按照分发地址分类
        HashMap<IpNode, List<Message>> map = new HashMap<IpNode, List<Message>>();
        //初始化
        for(IpNode address:index) {
            if(map.get(address)==null) {
                map.put(address, new ArrayList<Message>());
            }
        }
        //遍历消息,将每条message分类
        Iterator<Message> iterator = list.iterator();
        while(iterator.hasNext()) {
            Message message = iterator.next();
            //每个message可能有很多消费者
            List<IpNode> consumer_address = message.getTopic().getConsumer();
            Iterator<IpNode> it = consumer_address.iterator();
            while(it.hasNext()) {
                IpNode address = it.next();
                List<Message> l = map.get(address);
                if(l!=null)
                    l.add(message);
            }
        }
        return map;
    }

过滤器的主要作用就是将要发送的消息按照消费者地址分类,一个消息可能有一个或多个消费者。

消息分发(Push模式与Pull模式)

//为消费者推送消息
    private void pushMessage() {
        HashMap<IpNode, List<Message>> map = filter(index,poll(1));
        for(IpNode ip:map.keySet())
                {
                    List<Message> message = map.get(ip);
                    for(Message m:message) {
                        Client client = clients.get(ip);
                        if(client!=null) {
                            int i=0;
                            for(i=0;i<reTry_Time;i++) {//失败重试三次
                                String ack=null;
                                try {
                                    ack = client.SyscSend(m);
                                } catch (IOException e) {
                                    System.out.println("发送失败!正在重试...");
                                }
                                if(ack!=null)
                                    break;
                            }
                        if(i>=reTry_Time) {
                            //todo 进入死信队列
                        }
                        }else {
                            System.out.println("消费者不存在");
                            //todo 进入死信队列
                        }                   
                    }       
                }
    }
    //push模式
    public void push() {
        new Thread(){
            public void run() {
                while(true) {
                    try {
                        Thread.sleep(push_Time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pushMessage();
                }

            };
    }.start();
    }

push模式启动一个线程,每次push过程是所有队列出队一个元素,使用过滤器将所有消息分类,发送给相应的消费者,如果发送失败则重试一定次数(默认16次),次数达到上限后依然失败的话会进入死信队列,并告知相应的生产者。

负载均衡

public static List<Integer> balance(ConcurrentHashMap<String,MyQueue> queueList,int queueNum){
        //此时queueList的size一定大于queueNum
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<queueNum;i++) {
            int index = 0;
            int min = Integer.MAX_VALUE;
            for(java.util.Map.Entry<String, MyQueue> entry:queueList.entrySet()) {
                if(entry.getValue().size()<min&&!list.contains(Integer.valueOf(entry.getKey()))) {
                    min = entry.getValue().size();
                    index = Integer.valueOf(entry.getKey());
                }
            }
            list.add(index);
        }
        return list;
    }

负载均衡器提供一个负载均衡的方法,遍历队列找到前queueNum小的队列号。

主从备份

//slave同步
        new Thread(){
          public void run() {
                while(true) {
                    if(hasSlave) {
                        try {
                            Thread.sleep(sync_Time);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        Synchronizer sync = new Synchronizer(queueList, index);
                        try {
                            String s = SerializeUtil.serialize(sync);
                            for(IpNode ip:slave) {
                                Client client = new Client(ip.getIp(), ip.getPort());
                                client.Send(s);
                            }
                        } catch (IOException e) {
                            System.out.println("Slave未上线!");
                        }
                    }
                }
          };
      }.start();

Broker会在init方法中创建一个线程。如果创建带Slave节点备份的消息队列的话,该线程会不停的向Slave节点同步消息,同步不可保证强一致性。

持久化存储(同步或异步刷盘)与冗机恢复

//持久化
        new Thread(){
            public void run() {
                while(true) {
                    if(startPersistence) {
                        try {
                            Thread.sleep(store_Time);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        try {
                            String path = PersistenceUtil.class.getResource("").getPath().toString().substring(1);
                            File file = new File(path);
                            String newPath1 = file.getParentFile().getParent()+"QueueList.json";
                            String newPath2 = file.getParentFile().getParent()+"ConsumerAddress.json";
                            PersistenceUtil.Export(PersistenceUtil.persistenceQueue(broker.queueList),newPath1);
                            PersistenceUtil.Export(PersistenceUtil.persistenceConsumer(broker.index),newPath2);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
        }.start();

Broker在init方法中创建一个线程。如果用户开启持久化功能,该线程会每隔一段时间将队列内容写入磁盘,存储格式为2个json,一个存队列内容,一个存消费者地址。 若不幸冗机,用户可根据recover方法来恢复Broker。

//恢复Broker
    public void recover() {
        String path = PersistenceUtil.class.getResource("").getPath().toString().substring(1);
        File file = new File(path);
        String newPath1 = file.getParentFile().getParent()+"QueueList.json";
        String newPath2 = file.getParentFile().getParent()+"ConsumerAddress.json";
        ConcurrentHashMap<String,MyQueue> queueList = PersistenceUtil.Extraction(PersistenceUtil.Import(newPath1));
        this.setQueueList(queueList);
        List<IpNode> address= PersistenceUtil.ExtractionConsumer(PersistenceUtil.Import(newPath2));
        for(IpNode ipNode:address)
            addConsumer(ipNode);
    }

死信队列

死信队列机制因为楼主时间有限,没有在代码中实现。主要思路就是借鉴了TCP的捎带确认机制设计的捎带回复机制:当消息队列服务器Broker 向消费者发送消息时,可能由于一系列原因导致发送失败(例如消费者不在线或网络延时较高),此时若达到了系统设置的重试次数上限,则向定时线程池ScheduledThreadPoolExecutor提交任务,该任务为5秒后向该消息的生产者发送消息失败信息。若5秒内该生产者再次发来非单向消息,则捎带回复失败消息的信息。

生产者工厂(这里以延时同步工厂为例)

private static ConcurrentHashMap<IpNode, Boolean> requestMap= new ConcurrentHashMap<IpNode, Boolean>();
    private static int reTry_Time = 16;
    private static int Delay_Time = 2000;//延时时间默认2s

requestMap用于记录该消费者地址是否已向Broker注册,reTry_Time定义发送失败重试的次数,Delay_Time定义了延时发送时间。 生产者需先向Broker申请队列:

public static Topic RequestQueue(Topic topic,String ip,int port){//输入为一个topic,里面包含请求的队列个数
        System.out.println("请求向Broker申请队列...");
        Topic t = topic;
        Message m = new Message("RequestQueue",MessageType.REQUEST_QUEUE,t, -1);
        String queue = DelaySyscProducerFactory.SendQueueRegister(m, ip, port);
        String[] l = queue.substring(7).split(" ");
        for(String i:l)
            topic.addQueueId(Integer.parseInt(i));
        IpNode ipNode = new IpNode(ip, port);
        requestMap.put(ipNode, true);
        return t;
    }

申请队列时向Broker发送一个MessageType.REQUEST_QUEUE类型的消息:

private static String SendQueueRegister(Message msg,String ip,int port) {//未申请队列返回null
        Client client;
    if(msg.getType()!=MessageType.REPLY_EXPECTED&&msg.getType()!=MessageType.REQUEST_QUEUE)
            msg.setType(MessageType.REPLY_EXPECTED);
        try {
            client = new Client(ip, port);
            //失败重复,reTry_Time次放弃
            for(int i=0;i<reTry_Time;i++) {
                String result = client.SyscSend(msg);
                if(result!=null) {
                    System.out.println("队列申请成功!");
                    return result;
                }   
                if("".equals(result))
                    return null;
            }
        } catch (IOException e) {
            System.out.println("Broker未上线!");
        }
        return null;
    }

Broker收到该消息后会返回可用的消息队列序号,生产者工厂将这些消息序号添加到topic中,之后就可用该topic发送消息了:

//发送成功返回值为消息号+ACK
//发送失败返回值为null
    public static String Send(Message msg,String ip,int port) {//未申请队列返回null
        IpNode ipNode = new IpNode(ip, port);
        if(requestMap.get(ipNode)==null) {
            System.out.println("未向Broker申请队列!");
            return null;
        }
        //等待Delay_Time秒
        try {
            Thread.sleep(Delay_Time);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
            return null;
        }
        Client client;
        if(msg.getType()!=MessageType.REPLY_EXPECTED&&msg.getType()!=MessageType.REQUEST_QUEUE)
            msg.setType(MessageType.REPLY_EXPECTED);
        //失败重复,reTry_Time次放弃
        for(int i=0;i<reTry_Time;i++) {
            try {
                client = new Client(ip, port);
                String result = client.SyscSend(msg);
                if(result!=null)
                    return result;
                if("".equals(result))
                    return null;
            } catch (IOException e) {
                System.out.println("生产者消息发送失败,正在重试第"+(i+1)+"次...");
            }
        }
        return null;
    }

若发送成功返回值为消息号+空格+ACK,发送失败返回值为null。

消费者工厂

private static ConcurrentHashMap<Integer, ConcurrentLinkedQueue<Message>> map = new ConcurrentHashMap<Integer,ConcurrentLinkedQueue<Message>>();

这个map用于缓存Broker发来的消息,键为本地端口号,值为该消费者的消息缓冲区。 消费者工厂调用createConsumer向Broker注册消费者:

public static void createConsumer(IpNode ipNode1/*Broker地址*/,IpNode ipNode2/*本地地址*/) throws IOException {
        if(map.containsKey(ipNode2.getPort())) {
            System.out.println("端口已被占用!");
            return;
        }
        ConsumerFactory.register(ipNode1,ipNode2);
        ConsumerFactory.waiting(ipNode2.getPort());
        map.put(ipNode2.getPort(), new ConcurrentLinkedQueue<Message>());
    }

register方法向Broker发送注册消息:

//向Broker注册
    private static void register(IpNode ipNode1/*目的地址*/,IpNode ipNode2/*本地地址*/){
        System.out.println("正在注册Consumer...");
        Client client;
        try {
            client = new Client(ipNode1.getIp(), ipNode1.getPort());
            RegisterMessage msg = new RegisterMessage(ipNode2, "register", 1);
            if(client.SyscSend(msg)!=null)
                System.out.println("注册成功!");
            else
                System.out.println("注册失败!");
        } catch (IOException e) {
            System.out.println("Connection Refuse.");
        }

    }

waiting方法的作用是在某个端口监听,接受消息队列发送来的消息。

//在某个端口监听
    private static void waiting(int port) throws IOException {
        DefaultRequestProcessor defaultRequestProcessor = new DefaultRequestProcessor();
        ConsumerResponeProcessor consumerResponeProcessor = new ConsumerResponeProcessor();
        new Thread(){
            public void run() {
                System.out.println("Consumer在本地端口"+port+"监听...");
                try {
                    new Server(port,defaultRequestProcessor,consumerResponeProcessor);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                };
        }.start();
    }

性能测试

我写了个简单的测试类来模拟高并发,主要用了CountDownLatch类:

public class PressTest {
	
	public static void main(String[] args) {
		
		int time = 100000;//并发线程数
		final CountDownLatch send = new CountDownLatch(time);
	    final CountDownLatch timing = new CountDownLatch(time);
		
		//记录消息是否被成功发送
		ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
		for(int i=0;i<time;i++){
			map.put(i+"", 0);
		}
		//创建Producer
		SequenceUtil Sequence = new SequenceUtil();
		Topic topic = SyscProducerFactory.RequestQueue(new Topic("topic",1), "127.0.0.1", 81);
        topic.addConsumer(new IpNode("127.0.0.1", 8888));
        long startTime=System.currentTimeMillis();   //获取开始时间
        for(int i=0;i<time;i++) {
        	new Thread(){
                public void run() {
	        int num = Sequence.getSequence();
			Message msg = new Message("message"+num,topic, num);
			SyscProducerFactory.setReTry_Time(16);//设置发送失败重试次数
			try {
				send.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			String string = SyscProducerFactory.Send(msg, "127.0.0.1", 81);//同步发送
			if(string!=null) {
			    String[] a = string.split(" ");
			    map.put(a[0], map.get(a[0])+1);
			 }
			System.out.println(string);
			timing.countDown();
                }
            }.start();
            send.countDown();
        }
        try {
			timing.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
        long endTime=System.currentTimeMillis(); //获取结束时间     
        System.out.println("程序运行时间: "+(endTime-startTime)+"ms");
    //打印未发送成功的消息序号
    for(Entry<String, Integer> entry:map.entrySet())
    	if(entry.getValue()==0)
    		System.out.print(entry.getKey()+" ");
	}

8eb5f41f98ca193c7be3b8c7a3eb0e76.png

最终测试结果还不错,不过由于楼主只是在本地不同进行测试,所以测试数据成立的前提是忽略网络延时。

尚有不足

本项目中还有很多功能没有实现,希望有兴趣的读者可以在github上贡献你们的代码。

  • “心跳机制”
  • 统一的注册中心
  • 生产者消费者连接池
  • ......
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值