微服务编程避坑

一:微服务相关

1. feign的远程调用详解
  • item-service 模块对外提供服务,需要导入feign-api的模块依赖,
<!--feign-api模块依赖-->
  <dependency>
    <groupId>com.hmall</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
  </dependency>

 <!--feign的starter依赖-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>

  <!--nacos服务注册发现依赖-->
  <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  </dependency>

提供服务的接口如下:

		/**
     * 分页查询
     */
	@GetMapping("/list")
  public PageDTO<Item> list(@RequestParam("page") int page,@RequestParam("size") int size) {
    return itemService.load(page, size);
}
  • feign-api 模块需要提供远程调用item-service的接口,引入feign的starter依赖,

接口:

@FeignClient(value = "itemservice")//value为item-service模块的服务名
public interface IItemClient {
    /**
     * 分页查询
     */
    @GetMapping("/item/list")
    public PageDTO<Item> list(@RequestParam("page") int page,@RequestParam("size") int size);
}
  <!--<!--feign的starter依赖-->-->
	<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>
  • search-service模块远程调用的消费者,调用item-service 模块的服务,
  • 启动类上需要写@EnableFeignClients(basePackages = “com.hmall.common.cilent”),为feign-api模块中远程调用服务类的全路径,需要导入feign-api的模块依赖,nacos服务注册发现依赖,feign

    的starter依赖,在需要远程调用的地方,自动注入feign-api模块中的接口类名即可

启动类如下:

@Slf4j
@SpringBootApplication
//@EnableFeignClients(clients = {IItemClient.class})
@EnableFeignClients(basePackages = "com.hmall.common.cilent")
public class SearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class, args);
        log.info("search-service  启动成功");
    }

application.yaml文件如下:

server:
  port: 8082
spring:
  application:
    name: searchservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
logging:
  level:
    com.hmall: debug
  pattern:
    dateformat: HH:mm:ss:SSS
<!--feign-api模块依赖-->
  <dependency>
    <groupId>com.hmall</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
  </dependency>

 <!--feign的starter依赖-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
  </dependency>

  <!--nacos服务注册发现依赖-->
  <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  </dependency>
2. 网关的通用配置:
server:
  port: 10010 # 网关端口
spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        corsConfigurations:
          '[/**]':
            allowedOrigins:
              - "http://localhost:9001"
              - "http://localhost:9002"
              - "http://127.0.0.1:9001"
              - "http://127.0.0.1:9002"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

      routes: # 网关路由配置
        - id: item-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://itemservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/item/** # 这个是按照路径匹配,只要以/user/开头就符合要求
        - id: user-service
          uri: lb://userservice
          predicates:
            - Path=/user/**,/address/**
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**,/pay/**
        - id: search-service
          uri: lb://searchservice
          predicates:
            - Path=/search/**
2. springboot 网关 过滤器 添加请求头
routes: # 网关路由配置
        - id: item-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://itemservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/item/** # 这个是按照路径匹配,只要以/user/开头就符合要求
          filters: # 过滤器
            - AddRequestHeader=authorization,2
3. 拦截器拦截校验
  1. 书写拦截器
@Slf4j
@Component
public class ItemInterceptor implements HandlerInterceptor {
    //原始方法调用前执行的内容
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userId = request.getHeader("authorization");
        if (StringUtils.isBlank(userId)) {
            log.warn("非法用户访问!请求路径:{}",request.getRequestURI());
            throw new RuntimeException("用户未登录");
        }

        //将userId保存到ThreadLocal
        BaseContext.setCurrentId(Long.valueOf(userId));
        return true;
    }

    //原始方法调用后执行的内容
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    //原始方法调用完成后执行的内容
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //从线程中删除用户id
        BaseContext.removeCurrentId();
    }
}
  1. 将拦截器添加到WebMvcConfigurer(否则不会生效)
@Component
public class Interceptor implements WebMvcConfigurer {
    @Override
    //将拦截器添加到WebMvcConfigurer(否则不会生效)
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ItemInterceptor()).addPathPatterns("/**");
    }
}
4. feign远程调用 ———— 添加请求头

原因:feign远程调用服务,不会经过网关,所以网关的添加请求头不会生效

//添加请求头的方法
@Slf4j
public class MyFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        log.info("===request: {}", template.url());
        template.header("authorization", "2");
    }
}
//使feign添加请求头的方法生效
@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new MyFeignInterceptor();
    }
}
//具体的远程调用方法		使用configuration = FeignConfig.class  ,让spring扫描到FeignConfig
@FeignClient(value = "userservice",configuration = FeignConfig.class)
public interface IUserCilent {
    /**
     * 根据addressId查询地址
     */
    @GetMapping("/address/{id}")
    public Address findAddressById(@PathVariable("id") Long id);
}
5. 自动装配

描述:创建的类,没被spring扫描到,不会加载到容器,不生效

解决1:在启动类上添加: @ComponentScan(value = “类的路径”),让spring扫描到,就能加载到容器

解决2:resources配置下面添加 META-INF —— spring.factories配置文件,在里面添加类的全路径,这样spring的自动装配 就会把这个类加载到容器

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.heima.common.exception.ExceptionCatch,\
  com.heima.common.swagger.SwaggerConfiguration,\
  com.heima.common.knife4j.Swagger2Configuration

image-20221010205650997image-20221010205726529

image-20221010205159783image-20221010130817372

6. mybatis 的分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
7. 全局过滤器与拦截器实现登录鉴权
1.全局过滤器在请求头中添加用户id信息(写在网关服务里)
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
          //2.判断是否是登录
        if(request.getURI().getPath().contains("/login")){
            //放行
            return chain.filter(exchange);
        }
             //在header中添加用户id信息
            ServerHttpRequest httpRequest = request.mutate().headers(new Consumer<HttpHeaders>() {
                @Override
                public void accept(HttpHeaders httpHeaders) {
                    httpHeaders.add("userId", id.toString());
                }
            }).build();
         //将新的request替换掉旧的,写上build才会生效
            exchange.mutate().request(httpRequest).build();
      
        //6.放行
        return chain.filter(exchange);
    }
2.自媒体微服务使用拦截器获取到header中的的用户信息,并放入到threadlocal中
@Slf4j
public class WmTokenInterceptor implements HandlerInterceptor {
    /**
     * 前置处理    获取用户信息,存入线程中
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从请求头中获取登录用户的id
        String userId = request.getHeader("userId");
        //排除登录请求
        String path = request.getRequestURI().toString();
        if (path.contains("/in")) {
            return true;
        }

        //判断userId是否存在
        if (StringUtils.isBlank(userId)) {
            log.error("当前用户没有登录,请求被禁止");
            return false;
        }

        //userId不为空
        WmUser wmUser = new WmUser();
        wmUser.setId(Integer.parseInt(userId));
        //将用户id设置到线程中
        WmThreadLocalUtil.setUser(wmUser);
        return true;
    }

    /**
     * 后置处理,清理threadlocal中的数据,防止内存溢出
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        WmThreadLocalUtil.delete();
    }
}
3.注册拦截器,使拦截器生效
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");
    }
}
4.ThreadLocalUtil 工具类设置用户id到线程中
public class WmThreadLocalUtil {
    private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 添加到线程
     */
    public static void setUser(WmUser wmUser) {
        WM_USER_THREAD_LOCAL.set(wmUser);
    }


    /**
     * 从线程中获取
     */
    public static WmUser getUser() {
        return WM_USER_THREAD_LOCAL.get();
    }

    /**
     * 从线程中清理
     */
    public static  void  delete() {
        WM_USER_THREAD_LOCAL.remove();
    }
}

二:MQ相关

1.交换机、队列、key

1.交换机、队列、key常量
public class MqConstants {
    /**
     * 交换机
     */
    public final static String HOTEL_EXCHANGE = "item.topic";
    /**
     * 监听新增和修改的队列
     */
    public final static String HOTEL_INSERT_QUEUE = "item.insert.queue";
    /**
     * 监听删除的队列
     */
    public final static String HOTEL_DELETE_QUEUE = "item.delete.queue";
    /**
     * 新增或修改的RoutingKey
     */
    public final static String HOTEL_INSERT_KEY = "item.insert";
    /**
     * 删除的RoutingKey
     */
    public final static String HOTEL_DELETE_KEY = "item.delete";
}
2.声明交换机、队列、key,以及关系绑定
@Configuration
public class MqConfig {
    @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
    }

    /**
     * 新增或者修改的队列
     */
    @Bean
    public Queue insertQueue(){
        return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
    }

    /**
     * 删除的队列
     */
    @Bean
    public Queue deleteQueue(){
        return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
    }

    /**
     * 新增
     */
    @Bean
    public Binding insertQueueBinding(){
        return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
    }

    /**
     * 删除
     */
    @Bean
    public Binding deleteQueueBinding(){
        return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
    }
}
3.监听mq信息,实现es与数据库的数据同步
@Component
public class ItemListener {
    
    @Autowired
    private SearchService searchService;

    /**
     * 监听商品上架的信息——新增到es
     */
    @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
    public void listenHotelInsertOrUpdate(Long id) {
        searchService.updateHotel(id);
    }

    /**
     * 监听商品下架的信息——删除某条数据
     */
    @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
    public void listenHotelDelete(Long id) {
        searchService.deleteHotelById(id);
    }
}


    /**
     * 全量更新
     */
    @Override
    public void updateHotel(Long id) {
        try {
            Item item = iItemClient.getById(id);

            ItemDoc itemDoc = new ItemDoc(item);

            //准备request
            IndexRequest request = new IndexRequest("item").id(itemDoc.getId().toString());

            request.source(JSON.toJSONString(itemDoc), XContentType.JSON);

            client.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 文档删除
     */
    @Override
    public void deleteHotelById(Long id) {
        try {
            DeleteRequest request = new DeleteRequest("item", id.toString());
            client.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

2.java操作ES

1.操作索引库、文档
1.创建索引库
    @Test
    void testCreateIndex() throws IOException {
        //创建request对象
        CreateIndexRequest request = new CreateIndexRequest("item");
        //准备参数
        request.source(MAPPING_TEMPLATE, XContentType.JSON);
        //发送请求
        client.indices().create(request, RequestOptions.DEFAULT);
    }
2.删除索引库
    @Test
    void testDeleteIndex() throws IOException {
        //创建request对象
        DeleteIndexRequest request = new DeleteIndexRequest("itcast");
        //发送请求
        client.indices().delete(request, RequestOptions.DEFAULT);
    }
3.添加文档
    @Test
    void testAddDocument() throws IOException {
        //从数据库查询单个数据
        Hotel hotel = service.getById(56852L);
        //进行文档类型转换
        HotelDoc hotelDoc = new HotelDoc(hotel);
        //准备request对象
        IndexRequest request = new IndexRequest("itcast").id(hotelDoc.getId().toString());
        //准备文档数据
        request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
        //发送请求
        client.index(request, RequestOptions.DEFAULT);
    }
4.根据id查询文档
    @Test
    void testGetDocument() throws IOException {
        //准备request
        GetRequest request = new GetRequest("itcast").id("56852");
        //发送请求
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        //解析结果
        HotelDoc hotelDoc = JSON.parseObject(response.getSourceAsString(), HotelDoc.class);
        System.out.println(hotelDoc);
    }
5.根据id修改文档
    @Test
    void testUpdateDocument() throws IOException {
        //准备request
        UpdateRequest request = new UpdateRequest("itcast", "56852");
        //准备参数
        request.doc(
                "name", "魔都",
                "price", "10000"
        );
        //发送请求
        client.update(request, RequestOptions.DEFAULT);
    }
6.根据id删除文档
    @Test
    void testDeleteDocument() throws IOException {
        //准备request
        DeleteRequest request = new DeleteRequest("itcast", "56852");
        //发送请求
        client.delete(request, RequestOptions.DEFAULT);
    }
7.批量添加文档1
    @Test
    void testBulk() throws IOException {
        //批量查询数据
        List<Hotel> hotels = service.list();
        //准备request
        BulkRequest request = new BulkRequest();

        //文档类型转换
        for (Hotel hotel : hotels) {
            HotelDoc hotelDoc = new HotelDoc(hotel);
            //准备参数
            request.add(new IndexRequest("itcast")
                    .id(hotelDoc.getId().toString())
                    .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
        }
        //发送请求
        client.bulk(request, RequestOptions.DEFAULT);
    }
8.批量添加文档2
    @Test
    void testSaveAllDocument() throws IOException {
        int page = 1;

        while (true) {
            BulkRequest request = new BulkRequest();
            PageDTO<Item> list = iItemClient.list(page, 500);
            List<Item> itemList = list.getList();

            if (itemList.size() <= 0) {
                break;
            }

            for (Item item : itemList) {
                //如果商品为下架状态,则不添加到es,跳过
                if (item.getStatus() == 2) {
                    continue;
                }
                //类型转换
                ItemDoc itemDoc = new ItemDoc(item);
                //批量添加
                request.add(new IndexRequest("item")
                        .id(itemDoc.getId().toString())
                        .source(MAPPER.writeValueAsString(itemDoc), XContentType.JSON));
            }
            //发送请求
            client.bulk(request, RequestOptions.DEFAULT);
            page++;
        }
    }
2.ES查询相关
package cn.itcast.hotel;

/**
 * 查询文档
 */
@SpringBootTest
public class HotelSearchTest {
    @Autowired
    private IHotelService hotelService;

    private RestHighLevelClient client;

    //初始化RestHighLevelClient
    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.145.100:9200")
        ));
    }

    //关闭RestHighLevelClient
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }

    //查询文档快速入门:查询全部
    @SneakyThrows
    @Test
    void testMatchAll() {
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResponse(response);
    }

    /**
     * 解析查询出的结果为java对象
     */
    private void handleResponse(SearchResponse response) {
        //解析结果
        SearchHits searchHits = response.getHits();
        //获取总条数
        long total = searchHits.getTotalHits().value;
        System.out.println("查询到总条数为:" + total);

        //获取结果数组
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            //获取source
            String json = hit.getSourceAsString();
            //转换为java对象
            HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

            //获取高亮结果
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if (!CollectionUtils.isEmpty(highlightFields)) {
                //根据字段名获取高亮结果
                HighlightField highlightField = highlightFields.get("name");
                if (highlightField != null) {
                    //获取高亮值
                    String name = highlightField.getFragments()[0].string();
                    //覆盖非高亮结果
                    hotelDoc.setName(name);
                }
            }
            System.out.println(hotelDoc);
        }
    }

    //查询文档:单字段
    @SneakyThrows
    @Test
    void testMatch() {
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        request.source().query(QueryBuilders.matchQuery("all", "如家"));

        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResponse(response);
    }

    //查询文档:按条件查询-->bool
    @SneakyThrows
    @Test
    void testBool() {
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        //准备boolQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        //添加term条件
//        boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
        //添加range过滤条件
        boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
        request.source().query(boolQuery);

        //响应
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResponse(response);
    }

    //分页和排序
    @SneakyThrows
    @Test
    void testPageAndSort() {
        //模拟前端传递的页码参数
        int page = 1, size = 5;
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        request.source().query(QueryBuilders.matchAllQuery());
        //添加排序条件
        request.source().sort("price", SortOrder.ASC);
        //添加分页条件
        request.source().from((page - 1) * size).size(5);
        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        handleResponse(response);
    }

    //高亮
    @Test
    void testHighlight() throws IOException {
        //准备request
        SearchRequest request = new SearchRequest("hotel");
        //准备DSL
        //query
        request.source().query(QueryBuilders.matchQuery("all", "如家"));
        //高亮
        request.source().highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
        //发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        //解析响应
        handleResponse(response);
    }
}

三:MQ高级

1.消息可靠性

1.生产者消息确认

作用:确保消息投递到RabbitMQ的队列中

1.application.yml配置
logging:
  pattern:
    dateformat: HH:mm:ss:SSS
  level:
    cn.itcast: debug
spring:
  rabbitmq:
    host: 192.168.200.129 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: qincun
    password: 123456
    virtual-host: /
    publisher-confirm-type: correlated        #异步回调,定义ConfirmCallback
    publisher-returns: true     #定义ReturnCallback
    template:
      mandatory: true     #true,则调用ReturnCallback;false:则直接丢弃消息
2.消息发送者配置
package cn.itcast.mq.config;

/**
 * 消息发送者
 * 每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置
 * 异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
 */
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback			new   ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                    replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}
消息发送的格式:
 		@Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 生产者消息确认
     * 发送消息时
     * 定义ConfirmCallback
     */
    @Test
    public void testSendMessage2SimpleQueue1() throws InterruptedException {
        String exchange = "simple.direct";
        String queue = "simple.queue";
        String key = "simple";
        String message = "hello, yyyy 永远的神!";
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 3.添加callback
        correlationData.getFuture().addCallback(new SuccessCallback<CorrelationData.Confirm>() {
            @Override
            public void onSuccess(CorrelationData.Confirm confirm) {
                if (confirm.isAck()) {
                    //消息成功
                    log.debug("消息发送成功, ID:{}", correlationData.getId());
                } else {
                    //消息失败
                    log.error("消息发送失败, ID:{}, 原因{}", correlationData.getId(), confirm.getReason());
                }
            }
        }, new FailureCallback() {
            @Override
            public void onFailure(Throwable throwable) {
                log.error("消息发送异常, ID:{}, 原因{}", correlationData.getId(), throwable.getMessage());
            }
        });
        //发送消息
        rabbitTemplate.convertAndSend(exchange, key, message, correlationData);

        // 休眠一会儿,等待ack回执
        Thread.sleep(2000);
    }
2.消息持久化
1.交换机持久化
  • 事实上,默认情况下,由SpringAMQP声明的交换机都是持久化的。照常声明就行
2.队列持久化
  • 事实上,默认情况下,由SpringAMQP声明的队列都是持久化的。
    /**
     * 队列持久化
     */
    @Test
    public void testSendMessage2SimpleQueue2() throws InterruptedException {
        //发送消息
        String message = "滴滴滴";
        rabbitTemplate.convertAndSend("simple.direct", message);
    }
3.消息持久化
  • 默认情况下,SpringAMQP发出的任何消息都是持久化的,不用特意指定。
    /**
     * 消息持久化
     */
    @Test
    public void testMessage2() throws InterruptedException {
        //消息持久化
        Message message = MessageBuilder.withBody("ddd".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        //消息id,需要封装到
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());

        rabbitTemplate.convertAndSend("error.direct", message,correlationData);
        log.debug("发送消息成功");
    }
3.消费者消息确认

作用:通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。

SpringAMQP配置确认模式

  • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack
1.application.yml配置
logging:
  pattern:
    dateformat: HH:mm:ss:SSS
  level:
    cn.itcast: debug
spring:
  rabbitmq:
    host: 192.168.200.129 # rabbitMQ的ip地址
    port: 5672 # 端口
    username: qincun
    password: 123456
    virtual-host: /
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto  #关闭ack
        retry:
          enabled: true #开启消费者重试
          initial-interval: 1000    # 初始的失败等待时长为1秒
          multiplier: 1   # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3     # 最大重试次数
          stateless: true     # true无状态;false有状态。如果业务中包含事务,这里改为false
4.消费失败重试机制
1.本地重试

利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

结论:

  • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
  • 重试达到最大次数后,Spring会返回ack,消息会被丢弃
2.失败策略

建议策略为:RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

package cn.itcast.mq.config;
/**
 * 消费者
 */
@Configuration
public class ErrorMessageConfig {
  	//在 消费者服务中 定义处理失败消息的交换机和队列
    @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }
    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue", true);
    }
    @Bean
    public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
        return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
    }

  	//定义一个RepublishMessageRecoverer,关联队列和交换机
    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    }
}
总结

如何确保RabbitMQ消息的可靠性?

  • 开启生产者确认机制,确保生产者的消息能到达队列
  • 开启持久化功能,确保消息未消费前在队列中不会丢失
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

2.死信交换机

1.死信交换机
1.含义

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递
2.处理

队列将死信投递给死信交换机时,必须知道两个信息:

  • 死信交换机名称
  • 死信交换机与死信队列绑定的RoutingKey

这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。

image-20221014110808852

image-20221014110853727

3.利用死信交换机接收死信(拓展)

在消费者服务中,定义一组死信交换机、死信队列:

// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
    return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
    return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
    return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}
2.TTL

一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:

  • 消息所在的队列设置了超时时间
  • 消息本身设置了超时时间

image-20221014112315714

2.1.接收超时死信的死信交换机

在消费者服务的SpringRabbitListener中,定义一个新的消费者,并且声明 死信交换机、死信队列:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "dl.ttl.queue", durable = "true"),
    exchange = @Exchange(name = "dl.ttl.direct"),
    key = "ttl"
))
public void listenDlQueue(String msg){
    log.info("接收到 dl.ttl.queue的延迟消息:{}", msg);
}
2.2.声明一个队列,并且指定TTL

要给队列设置超时时间,需要在声明队列时配置x-message-ttl属性:

@Bean
public Queue ttlQueue(){
    return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间,10秒
        .deadLetterExchange("dl.ttl.direct") // 指定死信交换机
        .build();
}

注意,这个队列设定了死信交换机为dl.ttl.direct

声明交换机,将ttl与交换机绑定:

@Bean
public DirectExchange ttlExchange(){
    return new DirectExchange("ttl.direct");
}
@Bean
public Binding ttlBinding(){
    return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl");
}

发送消息,但是不要指定TTL:

@Test
public void testTTLQueue() {
    // 创建消息
    String message = "hello, ttl queue";
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    // 记录日志
    log.debug("发送消息成功");
}
2.3.发送消息时,设定TTL

在发送消息时,也可以指定TTL:

@Test
public void testTTLMsg() {
    // 创建消息
    Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setExpiration("5000")
        .build();
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    log.debug("发送消息成功");
}
2.4.总结

消息超时的两种方式是?

  • 给队列设置ttl属性,进入队列后超过ttl时间的消息变为死信
  • 给消息设置ttl属性,队列接收到消息超过ttl时间后变为死信

如何实现发送一个消息20秒后消费者才收到消息?

  • 给消息的目标队列指定死信交换机
  • 将消费者监听的队列绑定到死信交换机
  • 发送消息时给消息设置超时时间为20秒
3.延迟交换机(官方插件)
1. DelayExchange原理

DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

  • 接收消息
  • 判断消息是否具备x-delay属性
  • 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
  • 返回routing not found结果给消息发送者
  • x-delay时间到期后,重新投递消息到指定队列
2. 注解方式声明 延迟交换机
    /**
     * 声明DelayExchange交换机
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "delay.queue", durable = "true"),
            exchange = @Exchange(name = "delay.direct", durable = "true"),
            key = "delay"
    ))
    public void listenDelayQueue(String msg) {
        log.info("接收到{}",msg);
    }
3.发送消息
    /**
     * 延迟交换机实现延迟消息
     * 发送消息时,一定要携带x-delay属性,指定延迟的时间
     */
    @Test
    void testDelayedMsg() {
        //创建消息
        Message message = MessageBuilder.withBody("逍遥青春最快乐".getBytes(StandardCharsets.UTF_8))
                .setHeader("x-delay", 10000)
                .build();
        //消息ID,需要封装到CorrelationData中
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        //发送消息
        rabbitTemplate.convertAndSend("delay.direct", "delay", message, correlationData);
        log.info("发送消息成功");
    }
4. 总结

延迟队列插件的使用步骤包括哪些?

•声明一个交换机,添加delayed属性为true

•发送消息时,添加x-delay头,值为超时时间

3. 惰性交换机

1.消息堆积问题

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。

2.惰性队列

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储
3.声明惰性队列
1.基于@Bean声明lazy-queue
    @Bean
    public Queue lazyQueue() {
        return QueueBuilder
                .durable("lazy.queue")
                .lazy()//开启x-queue-mode 为lazy
                .build();
    }
2.基于@RabbitListener声明LazyQueue
    @RabbitListener(queuesToDeclare = @Queue(
            name = "lazy.queue",
            durable = "true",
            arguments =@Argument(name = "x-queue-mode",value = "lazy")
    ))
    public void listenLazyQueue(String msg) {
        log.info("接收到{}",msg);
    }
总 结

消息堆积问题的解决方案?

  • 队列上绑定多个消费者,提高消费速度
  • 使用惰性队列,可以再mq中保存更多消息

惰性队列的优点有哪些?

  • 基于磁盘存储,消息上限高
  • 没有间歇性的page-out,性能比较稳定

惰性队列的缺点有哪些?

  • 基于磁盘存储,消息时效性会降低
  • 性能受限于磁盘的IO

4.MQ集群

1. Java代码创建仲裁队列:
@Bean
public Queue quorumQueue() {
    return QueueBuilder
        .durable("quorum.queue") // 持久化
        .quorum() // 仲裁队列
        .build();
}
2.SpringAMQP连接MQ集群

注意:这里用address来代替host、port方式

spring:
  rabbitmq:
    addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
    username: itcast
    password: 123321
    virtual-host: /

超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是消息堆积问题。

2.惰性队列

从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储
3.声明惰性队列
1.基于@Bean声明lazy-queue
    @Bean
    public Queue lazyQueue() {
        return QueueBuilder
                .durable("lazy.queue")
                .lazy()//开启x-queue-mode 为lazy
                .build();
    }
2.基于@RabbitListener声明LazyQueue
    @RabbitListener(queuesToDeclare = @Queue(
            name = "lazy.queue",
            durable = "true",
            arguments =@Argument(name = "x-queue-mode",value = "lazy")
    ))
    public void listenLazyQueue(String msg) {
        log.info("接收到{}",msg);
    }
总 结

消息堆积问题的解决方案?

  • 队列上绑定多个消费者,提高消费速度
  • 使用惰性队列,可以再mq中保存更多消息

惰性队列的优点有哪些?

  • 基于磁盘存储,消息上限高
  • 没有间歇性的page-out,性能比较稳定

惰性队列的缺点有哪些?

  • 基于磁盘存储,消息时效性会降低
  • 性能受限于磁盘的IO

4.MQ集群

1. Java代码创建仲裁队列:
@Bean
public Queue quorumQueue() {
    return QueueBuilder
        .durable("quorum.queue") // 持久化
        .quorum() // 仲裁队列
        .build();
}
2.SpringAMQP连接MQ集群

注意:这里用address来代替host、port方式

spring:
  rabbitmq:
    addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073
    username: itcast
    password: 123321
    virtual-host: /
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
NetCore微服务是指使用.Net Core框架构建的以微服务架构为基础的应用程序。微服务架构是一种将应用程序拆分为一系列小型、独立部署的服务的设计模式,每个服务都有自己的业务逻辑和数据库。它们通过网络进行通信,以实现整体应用程序的功能。 NetCore是微软开发的跨平台、高性能、开源的开发框架,具备强大的功能和灵活的扩展性。将NetCore框架应用于微服务架构可以提供以下优势: 1. 高性能:NetCore框架采用了先进的异步编程模型和基于内存的缓存技术,能够处理大量并发请求,提高系统的响应速度和吞吐量。 2. 可扩展性:由于每个微服务都是独立部署的,可以根据需求独立扩展和升级。使用NetCore框架的依赖注入功能,可以方便地添加新的服务或替换现有服务,而无需对整个应用程序进行重构。 3. 高可用性:微服务架构的一个重要特点是容错和自愈能力。当某个服务发生故障时,其他服务可以继续运行,从而确保整体系统的稳定性和可用性。 4. 灵活性:NetCore框架支持多种开发语言和工具,使开发人员能够选择适合自己的技术栈,并在不同的微服务中使用不同的编程语言和数据库。 5. 安全性:NetCore框架内置了许多安全功能,如身份验证、授权和数据加密等。这些功能可以帮助开发人员构建安全、可靠的微服务系统。 总之,NetCore微服务通过结合微服务架构和.Net Core框架的优势,可以实现高性能、可扩展、高可用和灵活的应用程序开发。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值