仿RabbitMQ,实现的消息队列 (上)

目录

1.核心概念

1.1 Broker Server内部涉及的概念

1.2 Broker Server 中核心的API

1.3 Broker Server中交换机的类型

1.4 持久化概念

1.5 消息应答模式

1.6模块划分

2.Broker Server核心类的创建

2.1Exchange类 (交换机)

2.2 MSGQueue类 (队列)

2.3 Binding类(绑定)

2.4 Message类(消息)

2.5 BasicProperties类

3.持久化管理

 3.1数据库存储

3.1.1 DataBaseManager类的编写

3.1.2 DataBaseManagerTest代码编写

3.2 文件存储

3.2.1 BinaryTool类

3.2.2 消息文件的存储格式

3.2.3MessageFileManager类的编写

3.2.4 MessageFileManagerTest类的编写 

3.3 整合DataBaseManager,MessageFileManager类

​编辑

4.内存管理

4.1 MemoryDataCenter类的设计

4.2 MemoryDatatCenterTests类


   本篇文章主要是深入学习消息队列知识并且实现消息队列的核心功能,本篇文章学习并实现的消息队列为 --- RabbitMQ

   消息队列主要涉及到的角色可以分为三类: 生产者,中间人,消费者. 生产者在生产出一条消息的时候,会先将这条消息发给中间人,再由中间人转发给消费者.在这一过程中中间人(Broker),就是消息队列中核心枢纽

1.核心概念

 1.生产者(Producer)

 2.消费者(Consumer)

 3.中间人(Broker)

 4.发布(Publish) --- 生产者向中间人这里投递消息的过程

 5.订阅(Subscribe) ---- 哪些消费者要从中间人这里取数据,这个注册过程,称为"订阅"

 6.消费(Consume) ------ 消费者从中间人这里取数据的动作

这里举一个订牛奶的"栗子":

    假设我这个月有点米了,想喝点牛奶,我们可以跑到一些牛奶专卖店里去订购一个月的牛奶(订阅),然后店员就跟我说,每天早上的7点至12点来店里面取牛奶(消费),或者等到下午的时候可以配送上门,达成协议之后每天过来店里取牛奶即可.

   在买牛奶的例子中,牛奶生产商 牛奶专卖店 我(消费者),就对应上图上的关系,如果转换到编程思想中就是3个服务器,当然对应着不同的需求生产者 和 消费者的数量肯定是有变动的.

   看到这张图,学过生产者和消费者模型的铁铁可能就会有疑惑了,这个图中的生产者和消费者直接连起来然后内部随便搞一个地方存储起来不就可以了吗,为啥还要搞一个Broker Server 专门搞一个服务器来去实现转发关系呢?

   此处我们接着使用牛奶的例子,比如牛奶店来了一批牛奶,我不能说有人来就给取的吧,有些人买的的月卡,有的人买的是隔一天一瓶的,有的人单天现买的,此时这个牛奶店(队列)要处理的事务就会变得多起来,此时我们就需要对这个队列去设定一些方案和措施确保其效率. 然后我们再来看看Broker Server里面都有啥.

1.1 Broker Server内部涉及的概念

    1.虚拟主机(Virtual Host).类似于MySQL中的Database,算是一个"逻辑"上的数据集合, 一个Broker Server 上也可以组织多种不同类别的数据,可以使用Virtual Host 做出逻辑上的区分实际开发中,一个Broker Server 可能会同时用来管理多组 业务线 上的数据,此时就可以使用Virtual Host做出区分

2.交换机(Exchange)

     生产者把消息投递给Broker Server,实际上是先把消息交给了Broker Server上的某个交换机,再由交换机把消息转发给了对应的队列交换机就类似于公司的前台

3.队列(Queue)

  真正用来存储管理消息的实体,后续消费者也是从队列中取得数据,一个大队列中可以有很多个小的队列

4.绑定(Binding)

  绑定是用来描述交换机和队列之间的关系的,建立交换机和队列的关联关系,此处可以把交换机和对列之间的关系视为"多对多", 一个交换机可以于多个队列绑定关系,一个队列也可以被多个交换机对应.

5.消息(Message)

 具体来说,可以认为是服务器A给B发的请求(通过MQ转发),就是一个消息服务器B给A返回的响应(通过MQ转发),也是一个消息一个消息,可以视为一个字符串(二进制数据)消息中具体包含了啥样的数据,都是程序猿自定义的

1.2 Broker Server 中核心的API

   在BrokerServer中我们要提供的API主要有9个

  1, 创建队列(queueDeclare) : 此处的创建队列主要是存在则使用,不存在则创建,所以使用的是Declare 而不是 Create

  2.销毁队列(queueDelete)

  3.创建交换机(exchangeDeclare)

  4.销毁交换机(exchangeDelete)

  5.绑定关系(queueBind)

  6.解除绑定(queueUnbind)

  7.发布消息(basicPublish)

  8.订阅消息(basicConsume)

  9.确认消息(basicAck)

  看完这些API,可能会存在一些疑惑,这个BrokerServer不就是消息的中转站吗,不去设置一个消费消息吗.不提供一个API给消费者提取消息吗.

  实际上MQ和消费者之间的工作模式有两种:
1.push(推) : Broker把收到的数据,主动的发给所有的订阅者 (RabbitMQ只支持推的方式)

2.pull(拉) : 消费者主要调用Broker的API获取数据 (其他的消息队列可以支持这么模式,例如kafka)

1.3 Broker Server中交换机的类型

在RabbitMQ中主要实现了4种交换机

1.Direct 直接交换机

2.Fanout 扇形交换机

3.Topic 主题交换机

4.Header 消息头交换机(消息头交换机使用的规则比较复杂且应用场景比较少,我们在后面的文章就不进行讲解和实现)

1.Direct 直接交换机

  生产者发送消息的时候,会有一个"目标队列"的名字,交换机收到之后,就会去查看是否有匹配的队列,如果有就直接转发过去(把消息塞到对应的队列中);如果没有匹配的队列,那么这条消息会被丢弃

2.Fanout 扇出交换机

  简单来说扇出交换机是直接交换机的一个升级版,在直接交换机中,是遇到指定的目标队列才会进行转发,在扇出交换机中,只要你的队列有跟这个交换机进行绑定,就会转发给你(如下图所示)

3.Topic 主题交换机

  在主题交换机中有两个概念:
   1)bindingKey : 将队列和交换机绑定的时候,会指定一个单词

   2)routingKey : 生产者发送消息给交换机的时候,也会指定一个单词

  这两个"单词"就跟暗号一样,如果当前的routingKey 和 bindingKey 能够对上暗号,此时交换机就可以把这个消息发给这个队列;如何没有对上则就下一个队列试试

1.4 持久化概念

 虚拟主机,交换机,队列,绑定,消息 这些东西都需要BrokerServer进行组织管理,上述这些概念的数据,也需要存储和管理起来.

  此时采用的策略为: 以内存为主,硬盘为辅

在内存中存储的原因:

  对于MQ来说,能够高效的转发并处理数据,是非常关键的,因此使用内存来组织上述数据,得到的效率会比存放在硬盘来说要高的多.

在硬盘中存储的原因:

  对于消息队列来说,你可能会同时接受到大量的数据们这些数据有些是要在你的MQ中停留一阵子的,此时如果进程重启或者主机关机了,如果将这些数据只存储在了内存中,不是都没了? 所以有些数据是要存储在硬盘中进行备份的.  所以才说是以内存为主,硬盘为辅的持久化策略.

1.5 消息应答模式

  在这里我们设计两个消息应答模式:

 1.自动应答:消费者把这个消息取走了,就算应答(相当于没应答)

 2.手动应答:basicAck方法属于手动应答(消费者需要手动调用这个API去进行应答)

  消息应答的作用:客户端(消费者)在从队列中获取到消息之后.这个消息肯定是要进行删除的操作的吧,那如果此处消费者还没消费完这个消息就给你删除了,那不久很尴尬,此处的自动应答机制就是在消费者取走消息且消费完之后就把消息从内存和硬盘中删除;手动应答则是当客户取走消息之后,我这块不就不对这条消息进行处理了,而是提供一个API给你,当你彻底执行完这条消息的时候,消费者就去调用这个API就进行手动应答,手动应答的过程中就把该条消息的数据给删除.

  至于给用户取走的消息但是还没给删除的数据,我们创建一个集合对这类消息进行管理 ---- 待确认消息队列(就是一个Map,里面的key是消息的身份标识,value是消息的主体)

1.6模块划分

  消息队列中的数据我们首先会在内存中进行数据的存储,是否要将数据存储进硬盘我们有在类中设定属性进行标识(RabbitMQ是这样设计的)

2.Broker Server核心类的创建

 在BrokerServer中,要创建的几个关键类可以从上文体现出来,分别有交换机,队列,绑定,消息,接下来我们把这些类中的需要用到的元素先添加上去.

2.1Exchange类 (交换机)

    在Exchange类中,需要一个元素进行身份标识,我们采用RabbitMQ中的使用Name进行身份标识,

 交换机的类型由于是有限个的(3种),此处我们就自定义一个枚举类型来便于我们后续的使用

//交换机类型
public enum ExchangeType {
    DIRECT(0), //直接交换机
    FANOUT(1), //扇出交换机
    TOPIC(2);  //主题交换机
    private final int type;
    ExchangeType(int type) {
        this.type = type;
    }

    public int getType() {
        return type;
    }
}

   autoDelete:交换机的数据在内存中进行删除的时候采用伪删除的方式进行处理而不是直接丢弃(一份数据在着,万一后续使用的到还可以及时找回),

   durable:则是标识该交换机支付需要在硬盘中进行存储,交换机,队列,绑定,消息这些都是存储在内存中的,有一些数据可能会长期使用,此时为了确保在重启服务器(主动/被动)之后数据可以被记录下来,就可以选择存储在硬盘中(备份)

  arguments:是一个配置属性,此处就是单纯的写在这块不进行进一步的实现(RabbitMQ人家有,这块就写出来用一下)

public class Exchange {
    //使用name来作为交换机的身份标识
    private String name;
    //交换机类型DIRECT,FANOUT,TOPIC
    private ExchangeType type = ExchangeType.DIRECT;
    //durable 表示该交换机是否要进行持久化 是为ture
    private boolean durable = false;
    //如果当前交换机没人使用,自动删除   目前先实现伪删除
    private boolean autoDelete = false;
    // arguments 表示创建交换机时 指定的一些额外选项  跟autDelete一样RabbitMQ有,但是先不实现
    // 此处为了把arguments存储到数据库中,就要把数据(map)进行序列化(json)
    private Map<String,Object> arguments = new HashMap<>();

  还有这些元素的getter 和 setter 方法,可以采用 win + insert 里面的一键生成来完成,也可以采用JavaSpring中提供的注解@Data来进行完成,后续需要特定的setter和getter再来指定即可.

2.2 MSGQueue类 (队列)

在MSGQueue中的属性和Exchange的差不多,但是后续还是会对MSGQueue进行扩展的,因为一个消费者他进行订阅,此时他订阅的目标不能是交换机吧(人家只是一个转发的),也不能是消息吧(这个是产物),这个时候消费者订阅的目标只能是MSGQueue这个中间商(牛奶店),但在此处先不扩张属性

//存储消息队列
@Data
public class MSGQueue {
    //表示队列的身份标识
    private String name;
    //表示队列是否持久化  ture表示持久化保存, false表示不持久化
    private boolean durable = false;
    //这个属性为ture 表示这个队列只能被一个消费者使用
    //先不实现
    private boolean exclusive = false;
    private boolean autoDelete = false;
    private Map<String,Object> arguments = new HashMap<>();
}

2.3 Binding类(绑定)

//队列和交换机之间的关联关系
@Data
public class Binding {
    private String exchangeName;
    private String queueName;
    //bindingKey 就是在出题(主题交换机),
    private String bindingKey;
}

  在绑定这块的属性就很简单了,要绑定的交换机和队列的身份标识(此处就是名称),然后还有一个bindingKey,bindingKey主要是在绑定的交换机类型为Topic的时候会使用到,此时交换机和队列之间就像有一条桥,消息先要通过这条桥就必须和守卫对暗号,如果暗号对上了就可以过桥到达队列中进行存储.

2.4 Message类(消息)

@Data
public class Message implements Serializable {
    private BasicProperties basicProperties = new BasicProperties();
    private byte[] body;  //这个getBody传出去,由于是一个数组直接打印是打印的地址(SE)
    //辅助属性
    //message 后续会存储到文件中(如果选择持久化)
    //一个文件中会有存储很多的消息
    //此时就使用两个偏移量来表示 [offsetBag,offsetEnd)
    //这两哥们不需要序列化的文件中,此时消息一旦被写入文件之后,所在位置就固定住了,不需要单独存储 (transient)针对序列化场景
    private transient long offsetBeg = 0;//表示数据的开头距离文件的开头的位置偏移(字节)
    private transient long offsetEnd = 0;//表示数据的结尾距离文件的开头的位置偏移(字节)
    //使用这个属性表示消息在文件中是否是有效消息(针对文件中的消息逻辑删除,如果删除,就使用逻辑删除)
    //0x1表示有效,0x0表示无效(为了在文件上表示)
    private byte isValid = 0x1;

    //创建一个工程方法,让工厂创建一下message对象的过程
    //这个方法中创建的message对象,会自动生成唯一的MessageId
    public static Message createMessageWithId(String routingKey,BasicProperties basicProperties,byte[] body) {
        Message message = new Message();
        if(basicProperties != null) {
            message.setBasicProperties(basicProperties);
        }
        message.setMessageId("M-" + UUID.randomUUID());
        //万一routingKey 和 basicProperties里面的routingKey冲突了以外面的为主
        message.basicProperties.setRoutingKey(routingKey);
        message.body = body;
        return message;
    }
}

  BasicProperties 这个类主要是存放Message的一些关键属性,在下面会展开.

  此时会发现,这么Message这块会有一个offerBeg 和 offerEnd,由于我们消息数量多且内容大小不定,总不能一直放到内存中玩推山吧,晚点跑着跑着内存直接给你干满了就不好了,所以消息是先存储到一个数据文件中,等使用到了在从硬盘调用到内存中,此时为了在硬盘中快速找到目标消息,就需要有辅助功能帮助我们进行定位,offerBag 和 offerEnd就是分别记录消息在数据文件中的起始位置和终止位置.

 isValid:由于数据是存储到数据文件中的,要当数据要进行删除的话会有点麻烦,所以当数据要删除的时候,先在硬盘中做一下标识,等到触发GC(垃圾回收)的时候,会对要删除的消息进行处理

2.5 BasicProperties类

这个类主要就是存放Message的核心信息

@Data
public class BasicProperties implements Serializable {
    //消息的唯一身份标识,此处为了保证id的唯一性,使用uuid来作为messageId
    private String messageId;
    //是以一个消息上带有的内容,和bindingKey做匹配
    //如果当前的交换机类型是DIRECT,此时的routingKey 就表示要转发的队列名
    //如果当前的交换机类型是FANOUT,此时的routingKey 无意义(FANOUT是全部转发)
    //如果当前的交换机是TOPIC,此时的 routingKey就要和 bindingKey做匹配,符合要求才能转发消息给对应队列
    private String routingKey;
    //表示消息是否要持久化 1表示不持久化 2表示持久化
    private int deliverMode = 1;

    //RabbitMQ来说,BasicProperties内部还有很多属性,此处把主要的给实现
}

 此处的routingKey就是在Topic交换机的场景下,针对bindingKey进行对暗号的

注:由于MessageId 和 routingKey是属于BasicProperties中的属性,为了后续的调用方便,建议在Message类中生成对应的Getter和Setter方法.

3.持久化管理

 3.1数据库存储

   在进行持久化存储的时候,交换机,队列,绑定,这些元素的信息我们肯定是首先去考虑存储到一个数据库中的,而且我们进行持久化存储的目的之一是能够在进程结束或者突然断电的因素导致内存中的数据丢失的情况下,在重启服务器之后可以通过数据库把数据快速读到内存中.

   我们在最初学习数据库存储的时候首先接触的是MySQL,但是此处我们使用更加轻量化的数据库SqLite数据库,这个数据库的使用方式非常简单,直接在配置文件中配置即可

  此处的url就表示数据库启动的时候文件所在位置,我们就把它放到Data目录底下,SQLite也是可以通过MyBatis进行使用的,我们在编写sql代码的时候就定义一个XML文件编写sql指令,在封装一个类进行引用,并向外界提供操作数据库接口

 在sql指令中,我们先外界提供的方法有,创建Exchange表;创建Queue表;创建Binding表;添加一条Exchange数据;删除一条Exchange数据;添加一条Queue数据;删除一条Queue数据;添加一条Binding数据;删除一条Binding数据和每张表的Select(全遍历)

 .xml文件代码如下:

<mapper namespace="com.example.demo.mqserver.mapper.MetaMapper">
    <!--在xml语句中可以通过update完成建表操作-->
    <update id="createExchangeTable">
        create table if not exists exchange(
           name varchar(50) primary key,
           type int,
           durable boolean,
           autoDelete boolean,
           arguments varchar(1024)
        );
    </update>
    
    <update id="createQueueTable">
        create table if not exists queue(
           name varchar(50) primary key,
           durable boolean,
           exclusive boolean,
           autoDelete int,
           arguments varchar(1024)
        );
    </update>
    
    <update id="createBindingTable">
        create table if not exists binding(
            exchangeName varchar(50),
            queueName varchar(50),
            bindingKey varchar(256)
        );
    </update>

    <insert id="insertExchange" parameterType="com.example.demo.mqserver.core.Exchange">
        insert into exchange values(#{name},#{type},#{durable},#{autoDelete},#{arguments});
    </insert>
    
    <delete id="deleteExchange" parameterType="java.lang.String">
        delete from exchange where name = #{exchangeName};
    </delete>

    <insert id="insertQueue" parameterType="com.example.demo.mqserver.core.MSGQueue">
        insert into queue values(#{name},#{durable},#{exclusive},#{autoDelete},#{arguments});
    </insert>

    <delete id="deleteQueue" parameterType="java.lang.String">
        delete from queue where name = #{queueName};
    </delete>

    <insert id="insertBinding" parameterType="com.example.demo.mqserver.core.Binding">
        insert into binding values(#{exchangeName},#{queueName},#{bindingKey});
    </insert>
    <delete id="deleteBinding" parameterType="com.example.demo.mqserver.core.Binding">
        delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};
    </delete>


    <select id="selectAllExchanges" resultType="com.example.demo.mqserver.core.Exchange">
        select * from exchange;
    </select>

    <select id="selectAllQueues" resultType="com.example.demo.mqserver.core.MSGQueue">
        select * from queue;
    </select>

    <select id="selectAllBindings" resultType="com.example.demo.mqserver.core.Binding">
        select * from binding;
    </select>

</mapper>

在创建对外访问的接口:

@Mapper
public interface MetaMapper {
    //提供建表方法
    void createExchangeTable();
    void createQueueTable();
    void createBindingTable();

    //针对上述三个基本概念进行插入和删除
    void insertExchange(Exchange exchange);
    List<Exchange> selectAllExchanges();
    void deleteExchange(String exchangeName);
    void insertQueue(MSGQueue queue);
    List<MSGQueue> selectAllQueues();
    void deleteQueue(String queueName);
    void insertBinding(Binding binding);
    List<Binding> selectAllBindings();
    //binding由于没有主键,所以binding的删除操作针对exchangeName 和 queueName两个变量进行操作
    void deleteBinding(Binding binding);

}

这里要注意,为了能和对应的xml方法形成连接,这个接口的位置和名称是不可以随便设计的

我们在xml中配置的是

<mapper namespace="com.example.demo.mqserver.mapper.MetaMapper">

那么我们接口的位置得是 com.example.demo.mqserver.mapper下,且接口名称得是MetaMapper

  这个接口完成之后我们还需要编写一个类对在数据库中存储给一个完整的方法,我们在这里就定义DataBaseManager这个类来进行数据库数据方法的实现

3.1.1 DataBaseManager类的编写

    在DataBaseManager这个类中只有一个元素,就是我们上面编写的metaMapper这个类.

  DataBaseManager这个方法中我们先给一个init作为初始化的方法,我们在之前明确了数据库文件是存放到./data/meta.db这个文件中,我们先要检查该进程中是否已近提前创建或者上次创建使用过后但未被删除干净的情况,上述的情况定义为非法行为,如果有直接回应初始化失败.

  那么我们先把检查数据库文件是否已经提前存在的方法checkDBExists这个方法给实现了

private boolean checkDBExists() {
        File file = new File("./data/meta.db");
        if(file.exists()) {
            return true;
        }
        return false;
    }

  这个方法的实现非常简单,直接new一个File到我们指定的目录下,看是否存在该目录,不存在就证明可以继续进行初始化操作.

  下面我们就来写数据库初始化init方法

  //针对数据库进行初始化 ---- 建库建表 + 默认数据
    //1,如果数据库已经存在了(表 数据都有了),不进行任何操作
    //2.如果数据库不存在,则创建库 表 构造默认数据
    // 判定的标准:meta.db 这个文件是否存在
    public void init() {
        //手动获取到metaMapper
        metaMapper = DemoApplication.context.getBean(MetaMapper.class);//手动配置

        if(!checkDBExists()) {
            //数据库不存在,进行建库建表操作
            //先创建data目录
            File dateDir = new File("./data");
            dateDir.mkdirs();
            //创建数据表
            createTable();
            //插入默认数据
            createDefaultData();
            System.out.println("[DataBaseMapper] 初始化完成!");
        } else {
            //数据库已经存在则不需要进行任何操作
            System.out.println("[DataBaseMapper] 数据库已经存在!");
        }
    }

  在编写初始化操作之后,我们也要把配套的删除数据文件的方法给一起实现了(使用完下线了就要进行删除操作),在进行数据文件删除的时候一定要先把下级文件删除干净了在去删除这一级的文件.

public void deleteDB() {
        File file = new File("./data/meta.db");
       boolean ret = file.delete();
       if(ret) {
           System.out.println("[DataBaseMapper] 删除数据库文件成功!!!");
       } else{
           System.out.println("[DataBaseMapper] 删除数据库文件失败!!!!");
       }

       //删目录之前要确保目录里面内容删完先
       File dateDir = new File("./data");
       ret = dateDir.delete();
       if(ret) {
           System.out.println("[DataBaseMapper] 删除数据库目录成功");
       } else {
           System.out.println("[DataBaseMapper] 删除数据库目录失败");
       }
    }

在进行初始化和删除的之后,我们就把在MetaMapper的接口给对外封装方法,此处就和上述一起把完整的代码给写出来

package com.example.demo.mqserver.datacenter;

import com.example.demo.DemoApplication;
import com.example.demo.mqserver.core.Binding;
import com.example.demo.mqserver.core.Exchange;
import com.example.demo.mqserver.core.ExchangeType;
import com.example.demo.mqserver.core.MSGQueue;
import com.example.demo.mqserver.mapper.MetaMapper;

import java.io.File;
import java.util.List;

//整合数据库操作

public class DataBaseManager {
    //由于DataBaseMapper这个类后续还是希望进行手动管理所以控制权就不给出去了 同时@Autowired也无法使用
    private MetaMapper metaMapper;

    //针对数据库进行初始化 ---- 建库建表 + 默认数据
    //1,如果数据库已经存在了(表 数据都有了),不进行任何操作
    //2.如果数据库不存在,则创建库 表 构造默认数据
    // 判定的标准:meta.db 这个文件是否存在
    public void init() {
        //手动获取到metaMapper
        metaMapper = DemoApplication.context.getBean(MetaMapper.class);//手动配置

        if(!checkDBExists()) {
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值