2024.2.21 模拟实现 RabbitMQ —— 实现转发规则

目录

需求分析

[直接交换机(Direct )](#直接交换机(Direct ))

[主题交换机(Topic )](#主题交换机(Topic ))

[扇出交换机(Fanout )](#扇出交换机(Fanout ))

[Topic 交换机转发规则](#Topic 交换机转发规则)

[routingKey 组成](#routingKey 组成)

[bindingKey 组成](#bindingKey 组成)

匹配规则

情况一

情况二

情况三

[实现 Router 类](#实现 Router 类)

[校验 bindingKey 和 routingKey](#校验 bindingKey 和 routingKey)

消息匹配机制

[Topic 交换机匹配规则](#Topic 交换机匹配规则)

[针对 Router 单元测试](#针对 Router 单元测试)


需求分析

直接交换机(Direct )

  • 通过设定 消息的 routingKey = 队列名,以此指定该消息需传给哪个队列

实例理解

  • 如下图所示,此时可直接无视绑定关系,直接从内存中拿到对应队列名的队列
  • 然后再将消息传给该队列即可

主题交换机(Topic )

  • 依据 Topic 交换机的转发规则,判定消息需传给哪些队列

扇出交换机(Fanout )

  • 给该交换机中所有绑定好的队列均传入消息

Topic 交换机转发规则

  • bindingKey:创建绑定时,给绑定指定的特殊字符串(相当于出题)
  • routingKey:发布消息时,给消息上指定的特殊字符串(相当于做答案)
  • 当 routingKey 的答案能够与 bindingKey 相对应时,便可将消息转发给该队列

routingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 routingKey 分成多个部分

实例理解

  • aaa.bbb.ccc 合法
  • aaa.110.bbb 合法
  • aaa 合法

bindingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 bindingKey 分成多个部分
  3. 支持两种特殊的符号作为通配符( * 和 # 必须是作为被 . 分割出来的独立的部分)
  • * ------> 可以匹配任何一个 独立的部分
  • # ------> 可以匹配任何 0 个或者多个独立的部分

实例理解

  • aaa.*.bbb 合法
  • aaa.*bb.ccc 非法

匹配规则

情况一
  • 当 bindingKey 中没有 * 或 # 这两个特殊符号时,必须要求 routingKey 和 bindingKey 一模一样,才能算匹配成功!

实例理解

  • bindingKey = aaa.bbb.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.bbb.ccd (匹配失败)

注意:

  • 情况一非常类似于 Direct 交换机的转发
  • 尤其是将 bindingKey 设置成和队列名字相同,此时就完全等价于 Direct 交换机了!

情况二
  • 当 bindingKey 中有特殊符号 * 时

实例理解

  • bindingKey = aaa.*.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc(匹配失败)

情况三
  • 当 bindingKey 中有特殊符号 # 时

实例理解

  • bindingKey = aaa.#.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc (匹配成功)
  • routingKey = aaa.ccc(匹配成功)
  • routingKey = aaa.b.b.b(匹配失败)

问题:

  • 将交换机中每个队列的 bindingKey 设置成一个 # 时,会有啥效果呢?

回答:

  • 此时,该交换机中的 全部队列 都能匹配所有的 routingKey
  • 即该交换机就相当于 Fanout 交换机了!

注意点一:

  • Direct 交换机 和 Fanout 交换机,均属于 Topic 交换机的 特例

注意点二:

  • 上述规则都是 AMQP 协议所约定的!RabbitMQ 仅仅只是实现了该规则而已!

实现 Router 类

校验 bindingKey 和 routingKey

java 复制代码
//    bindingKey 的构造规则:
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
//    3、允许使用 * 和 # 作为通配符,但是通配符只能作为独立的分段
    public boolean checkBindingKey(String bindingKey) {
        if(bindingKey.length() == 0) {
//            空字符串也是合法情况,比如在使用 direct / fanout 交换机的时候,bindingKey 是用不上的
//            因为我们在使用 direct 交换机时,是直接将 routingKey 作为 消息队列的名字,直接根据名字来进行匹配
//            在使用 fanout 交换机时,无需匹配,直接将该消息转给交换机中绑定的所有消息队列
            return true;
        }
//        检查字符串中不能存在非法字符
        for (int i = 0;i < bindingKey.length();i++) {
            char ch = bindingKey.charAt(i);
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
            if(ch >= '0' && ch <= '9') {
                continue;
            }
            if(ch == '_' || ch == '.' || ch == '*' || ch == '#') {
                continue;
            }
            return false;
        }
//        检查 * 或者 # 是否是独立的部分
//        aaa.*.bbb 合法情况; aaa.a*.bbb 非法情况
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
//            检查 word 长度 > 1 并且包含了 * 或者 #,就是非法的格式了
            if(word.length() > 1 && (word.contains("*") || word.contains("#")) ) {
                return false;
            }
        }
//        约定一下,通配符之间的相邻关系 (人为约定的)
//        为啥这么约定?因为前三种相邻的时候,实现匹配的逻辑会非常繁琐,同时功能性提升不大
//        1、 aaa.#.#.bbb 非法情况
//        2、 aaa.#.*.bbb 非法情况
//        3、 aaa.*.#.bbb 非法情况
//        4、 aaa.*.*.bbb 合法情况
        for (int i = 0;i < words.length;i++) {
//            连续两个 ##
            if(words[i].equals("#") && words[i+1].equals("#")) {
                return false;
            }
//            # *
            if(words[i].equals("#") && words[i+1].equals("*")) {
                return false;
            }
//            * #
            if(words[i].equals("*") && words[i+1].equals("#")) {
                return false;
            }
        }
        return true;
    }

//    routingKey 的构造规则
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
    public boolean checkRoutingKey(String routingKey) {
        if(routingKey.length() == 0) {
//            空字符串,合法的情况,比如在使用 fanout 交换机的时候,routingKey 用不上,就可以设定为 ""
            return true;
        }
        for (int i = 0;i<routingKey.length();i++) {
            char ch = routingKey.charAt(i);
//            判定该字符是否是大写字母
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
//            判定该字符是否是小写字母
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
//            判定该字符是否是阿拉伯数字
            if(ch >= '0' && ch <= '9') {
                continue;
            }
//            判定是否是 _ 或者 .
            if(ch == '_' || ch == '.') {
                continue;
            }
//            该字符,不是上述任何一种合法情况,就直接返回 false
            return false;
        }
//        把每个字符都检查过,没有遇到非法情况,此时直接返回 true
        return true;
    }

问题:

  • 观察下图,为啥 split 方法中的参数 "." 需要加两个反斜杠呢?

回答:

  • 首先 "." 在正则表达式中,是一个特殊的符号,此处是将 . 作为原始文本来进行匹配
  • 要想使用 . 的原始文本,就需要进行转义,即 在正则中使用 "\." 的方式来表示
  • 又因为在 Java 的字符串中,"\" 是一个特殊字符
  • 所以要想写入 "\." 这样的文本,又得在其前面加上一个反斜杠来进行转义,即 "\\."

消息匹配机制

java 复制代码
//    这个方法用来判定该消息是否可以转发给这个绑定对应的队列
    public boolean route(ExchangeType exchangeType,Binding binding,Message message) throws MqException {
//        根据不同的 exhcangeType 使用不同的判定转发规则
        if(exchangeType == ExchangeType.FANOUT) {
//            如果是 fanout 类型,该交换机上绑定的所有队列都需要转发
            return true;
        }else if(exchangeType == ExchangeType.TOPIC) {
//            如果是 topic 主题交换机,规则就要更复杂一些
            return routeTopic(binding,message);
        }else {
//            其他情况是不应该存在的
            throw new MqException("[Router] 交换机类型非法! exchangeType = " + exchangeType);
        }
    }
  • 当为 Fanout 交换机时,无需匹配 bindingKey 和 routingKey,直接返回 true
  • 让该消息转发给所以绑定了 Fanout 交换机的队列
  • 当为 Topic 交换机时,则需要进行 bindingKey 和 routingKey 的匹配

Topic 交换机匹配规则

处理思路

  • 此处我们采用 双指针算法 进行匹配
  • 针对 bindingKey 的下标 ,判定当前下标指向部分的具体情况:
  1. 指向的是普通的字符串,此时要求和 routingKey 对应的下标指向的内容得完全一致!
  2. 指向的是 * ,此时无论 routingKey 这边指向的是啥,双方均同时下标前进
  3. 遇到了 # ,且 # 后面没有其他的内容了,直接返回 true,匹配成功!
  4. 遇到了 # ,但 # 后面还有其他的内容,拿着 # 后面的部分,去 routingKey 中查找,找到后面的部分,在 routingKey 中出现的位置 ,如果后面的部分,在 routingKey 中不存在,直接认为匹配失败,返回 false!如果后面的部分,在 routingKey 中存在,就将 routingKey 的箭头指向这个位置之后,然后继续往后匹配
  5. 两个箭头移动过程中,如果同时到达双方的末尾,则返回 true,如果一个箭头先到了末尾,另一个箭头还没到,则返回 false!

代码实现:

java 复制代码
    private boolean routeTopic(Binding binding,Message message) {
//        先把这两个 key 进行切分
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");

//        引入两个下标,指向上述两个数字,初始情况下都为 0
        int bindingIndex = 0;
        int routingIndex = 0;
//        此处使用 while 更合适,每次循环,下标不一定就是 +1,不适合使用 for
        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if(bindingTokens[bindingIndex].equals("*")) {
//                【情况二】如果遇到 * 号直接进入下一轮,* 可以匹配到任意一个部分!
                bindingIndex++;
                routingIndex++;
                continue;
            }else if(bindingTokens[bindingIndex].equals("#")) {
//                如果遇到 # 先要看看有没有下一个位置
                bindingIndex++;
                if(bindingIndex == bindingTokens.length) {
//                    【情况三】该 # 后面没有东西了,说明此时一定能匹配成功了!
                    return true;
                }
//                # 【情况四】后面还有东西,拿着这个内容,去 routingKey 中往后找,找到对应的位置
//                findNextMatch 这个方法用来查找该部分在 routingKey 的位置,返回该下标,没找到,就返回 -1
                routingIndex = findNextMatch(routingTokens,routingIndex,bindingTokens[bindingIndex]);
                if(routingIndex == -1) {
//                    没找到匹配的结果,匹配失败
                    return false;
                }
//                找到匹配的情况,继续往后匹配
                bindingIndex++;
                routingIndex++;
            }else {
//                【情况一】如果遇到普通字符串,要求两边的内容是一样的
                if(!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }
//        【情况五】判断是否是双方同时到达末尾
//        比如 aaa.bbb.ccc 和 aaa.bbb 是要匹配失败的
        if(bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
            return true;
        }
        return false;
    }

    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if(routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }

针对 Router 单元测试

  • 编写测试用例代码是十分重要的!
java 复制代码
package com.example.demo;

import com.example.demo.common.MqException;
import com.example.demo.mqserver.core.Binding;
import com.example.demo.mqserver.core.ExchangeType;
import com.example.demo.mqserver.core.Message;
import com.example.demo.mqserver.core.Router;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RouterTests {
    private Router router = new Router();
    private Binding binding = null;
    private Message message = null;

    @BeforeEach
    public void setUp() {
        binding = new Binding();
        message = new Message();
    }

    @AfterEach
    private void tearDown() {
        binding = null;
        message = null;
    }

//    【测试用例】
//    bindingKey               routingKey               result
//    aaa                      aaa                      true
//    aaa.bbb                  aaa.bbb                  true
//    aaa.bbb                  aaa.bbb.ccc              false
//    aaa.bbb                  aaa.ccc                  false
//    aaa.bbb.ccc              aaa.bbb.ccc              true
//    aaa.*                    aaa.bbb                  true
//    aaa.*.bbb                aaa.bbb.ccc              false
//    *.aaa.bbb                aaa.bbb                  false
//    #                        aaa.bbb.ccc              true
//    aaa.#                    aaa.bbb                  true
//    aaa.#                    aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.ccc                  true
//    aaa.#.ccc                aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.aaa.bbb.ccc          true
//    #.ccc                    ccc                      true
//    #.ccc                    aaa.bbb.ccc              true

    @Test
    public void test1() throws MqException {
        binding.setBindingKey("aaa");
        message.setRoutingKey("aaa");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test2() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test3() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test4() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test5() throws MqException {
        binding.setBindingKey("aaa.bbb.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test6() throws MqException {
        binding.setBindingKey("aaa.*");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test7() throws MqException {
        binding.setBindingKey("aaa.*.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test8() throws MqException {
        binding.setBindingKey("*.aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test9() throws MqException {
        binding.setBindingKey("#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test10() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test11() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test12() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test13() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test14() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test15() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test16() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }
}
相关推荐
用户8307196840821 天前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者3 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者5 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧6 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖6 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农6 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者6 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀6 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3056 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05096 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式