leyou商城day10 MQ介绍及详情静态页

16 篇文章 0 订阅
2 篇文章 0 订阅

02、商品详情:分析渲染商品详情页需要的数据

分析item.html页面,得到渲染所需要的数据如下:

List categories:三个分类对象的集合
Brand brand:品牌对象
String spuName:spu的名称
String subTitle:spu的副标题
SpuDetail detail:商品详情对象
List skus:spu下对应的sku的对象集合
List specs:规格组对象列表
​ 其中每个规格组对象中包含一个List规格参数集合

分析如何获取上面所需的数据:

现在唯一的条件就是spuId。

第一:根据spuId获取SpuDTO对象,里面要包含SpuDeail对象和sku对象的集合两个属性。(还没有

第二:根据三个分类id的集合查询分类对象的集合。(已经有了

第三:根据品牌id查询品牌对象。(还没有

第四:根据分类id查询出所有的规格组集合,里面包含规格参数集合属性。(还没有

03、商品详情:提供渲染商品详情所需Feign方法

1) 根据spuId获取SpuDTO对象

GoodsController

    /**
     * 根据spuId查询SpuDTO对象
     */
    @GetMapping("/spu/{id}")
    public ResponseEntity<SpuDto> findSpuDtoById(@PathVariable("id") Long id){
        SpuDto spuDTO = goodsService.findSpuDTOById(id);
        return ResponseEntity.ok(spuDTO);
    }

GoodsService

        SpuDto findSpuDTOById(Long id);

GoodsService

	@Override
    public SpuDto findSpuDTOById(Long id) {
        //1.根据spuId查询Spu对象
        TbSpu tbSpu = spuMapper.selectById(id);
        //2.拷贝数据
        SpuDto spuDto = BeanHelper.copyProperties(tbSpu, SpuDto.class);
        //3.查询SpuDetail
        TbSpuDetail spuDetail = spuDetailService.getById(id);
        spuDto.setSpuDetail(spuDetail);
        //4.查询所有Sku对象
        List<TbSku> skus = skuservice.findSkusBySpuId(id);
        spuDto.setSkus(skus);

        return spuDto;
    }

提供feign接口

    /**
     * 根据spuId查询SpuDTO对象
     */
    @GetMapping("/spu/{id}")
    SpuDto findSpuDtoById(@PathVariable("id") Long id);

2) 根据分类id集合查询分类对象的集合。

已经有了

3) 根据品牌id查询品牌对象

直接提供feign接口即可

    @GetMapping("/brand/{id}")
    TbBrand findBrandById(@PathVariable("id") Long id);

4) 根据分类id查询所有规格参数组

提供规格组对象的dto

package com.leyou.item.dto;

import com.leyou.item.pojo.TbSpecGroup;
import com.leyou.item.pojo.TbSpecParam;
import lombok.Data;

import java.util.List;

/**
 * @author yy
 */
@Data
public class SpecGroupDto extends TbSpecGroup {

    private List<TbSpecParam> params;//组内参数

}


SpecController提供处理器方法

    /**
     * 根据分类ID查询规格参数组(包含组内参数)
     */
    @GetMapping("/of/specGroupDto")
    public ResponseEntity<List<SpecGroupDto>> findSpecGroupDtoByCid(@RequestParam("id") Long id){
        List<SpecGroupDto> specGroupDTOS = specGroupService.findSpecGroupDtoByCid(id);
        return ResponseEntity.ok(specGroupDTOS);
    }

SpecService提供service

 @Override
    public List<SpecGroupDto> findSpecGroupDtoByCid(Long id) {
        //1.根据分类ID查询规格组
        List<TbSpecGroup> specGroups = findSpecGroupByCid(id);
        //2.拷贝数据
        List<SpecGroupDto> specGroupDTOS = BeanHelper.copyWithCollection(specGroups, SpecGroupDto.class);
        //3.遍历所有SpecGroupDTO
        specGroupDTOS.forEach(specGroupDTO -> {
            //3.1 根据规格组ID查询规格参数
            List<TbSpecParam> specParams = specParamService.findSpecParams(null, specGroupDTO.getId(), null);
            //设置组内的规格参数
            specGroupDTO.setParams(specParams);
        });
        return specGroupDTOS;
    }

提供feign接口

    /**
     * 根据分类ID查询规格参数组(包含组内参数)
     */
    @GetMapping("/spec/groups/of/specGroupDto")
    List<SpecGroupDto> findSpecGroupDtoByCid(@RequestParam("id") Long id);

04、商品详情:查询item.html模板所需数据

1) 编写处理器PageController

package com.leyou.controller;

import com.leyou.service.impl.PageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.Map;

/**
 * @author yy
 */

@Controller // 必须是Controller,不能@RestController
public class PageController {
    @Autowired
    private PageService pageService;

    /**
     * 接收商品详情
     */
    @GetMapping("/item/{id}.html")
    public String showGoodsDetail(@PathVariable("id") Long id, Model model){
        //1.调用业务
        Map<String,Object> resultMap = pageService.getDetailData(id);

        //2.把Map集合数据存入Model
        model.addAllAttributes(resultMap);

        //返回th的模板
        return "item";
    }
}



2) 编写PageService

package com.leyou.page.service;

import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.SpecGroupDTO;
import com.leyou.item.dto.SpuDTO;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class PageService {
    @Autowired
    private ItemClient itemClient;

    public Map<String,Object> getDetailData(Long id) {

      public Map<String,Object> getDetailData(Long id) {
        //1.查询SpuDTO对象
        SpuDto spuDTO = itemClient.findSpuDtoById(id);
        //2.根据分类ID集合查询分类对象集合
        List<TbCategory> categories = itemClient.findCategoriesByIds(Arrays.asList(
                spuDTO.getCid1(),
                spuDTO.getCid2(),
                spuDTO.getCid3()));
        //3.根据品牌ID查询品牌
        TbBrand brand = itemClient.findBrandById(spuDTO.getBrandId());
        //4.根据分类ID查询规格组(包含组内参数)
        List<SpecGroupDto> specGroupDTOList = itemClient.findSpecGroupDtoByCid(spuDTO.getCid3());

        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("categories",categories);
        resultMap.put("brand",brand);
        resultMap.put("spuName",spuDTO.getName());
        resultMap.put("subTitle",spuDTO.getSubTitle());
        resultMap.put("detail",spuDTO.getSpuDetail());
        resultMap.put("skus",spuDTO.getSkus());
        resultMap.put("specs",specGroupDTOList);
        return resultMap;
    }
}



3)页面测试数据是否可用

我们在item.html模板页面中先写一段JS,把模型中的数据取出观察,看是否成功:

<script th:inline="javascript">
    const categories = /*[[${categories}]]*/ [];
    const brand = /*[[${brand}]]*/ {};
    const spuName = /*[[${spuName}]]*/ "";
    const subTitle = /*[[${subTitle}]]*/ "";
    const detail = /*[[${detail}]]*/ {};
    const skus = /*[[${skus}]]*/ [];
    const specs = /*[[${specs}]]*/ [];
</script>

然后查看页面源码:

数据都成功查到了!

05、商品详情:渲染item.html模块页面

1)分类、品牌、spu名称

<div class="crumb-wrap">
                <ul class="sui-breadcrumb">
                    <li th:each="c :${categories}">
                        <a href="#" th:text="${c.name}">手机</a>
                    </li>
                    <li>
                        <a href="#" th:text="${brand.name}">Apple</a>
                    </li>
                    <li class="active" th:text="${spuName}">Apple iPhone 6s</li>
                </ul>
            </div>

2)副标题

副标题是在spu中,所以我们直接通过Thymeleaf渲染:

在第146行左右:

<div class="sku-name">
                        <h4>Apple iPhone 6s(A1700)64G玫瑰金色 移动通信电信4G手机</h4>
                    </div>
                    <div class="news"><span th:utext="${subTitle}">推荐选择下方[移动优惠购],手机套餐齐搞定,不用换号,每月还有花费返</span></div>

副标题中可能会有超链接(包含HTML标记),因此这里也用th:utext来展示,效果:

在这里插入图片描述

3)商品详情

商品详情是HTML代码,我们不能使用 th:text,应该使用th:utext

大概在页面的第485行左右:

<!--商品详情-->
<div class="intro-detail" th:utext="${detail.description}">
	<img src="/img/_/intro01.png" />
	<img src="/img/_/intro02.png" />
	<img src="/img/_/intro03.png" />
</div>

在这里插入图片描述

最终展示效果:

在这里插入图片描述

4)展示特有规格参数

取出当前商品的特殊规格参数 及 该分类下的所有特殊规格参数值,并放入Vue的data中

<script th:inline="javascript">
    //取出SpuDetail对象
    const detail = /*[[${detail}]]*/ {};

    //取出特有规格参数列表
    let specialSpec = JSON.parse(detail.specialSpec);

    //设计用于存储特有规格参数数据对象,格式:{4:"机身颜色",12:"内存"....}
    let params = {};
    //取出specs规格组和规格参数
    const specs = /*[[${specs}]]*/ [];
    specs.forEach(group=>{
        group.params.forEach(param=>{
             //只取出特有参数
            if(!param.generic){
                params[param.id] = param.name;
            }
        });
    });

</script>

<script>
    var itemVm = new Vue({
        el:"#itemApp",
        data:{
            specialSpec,
            params,
        },
        components:{
            lyTop: () => import('/js/pages/top.js')
    }
    });
</script>

调试查看结果如下:

在这里插入图片描述

然后,找到页面对应位置渲染数据(大约188行)

<div class="clearfix choose">
                        <div id="specification" class="summary-wrap clearfix">
                            <dl v-for="(value,key,index) in specialSpecJson" :key="index">
                                <dt>
                                    <div class="fl title">
                                        <i>{{paramsMap[key]}}</i>
                                    </div>
                                </dt>
                                <dd v-for="(v,i) in value" :key="i">
                                    <a href="javascript:;" class="selected">
                                        {{v}}<span title="点击取消选择">&nbsp;</span>
                                    </a>
                                </dd>
                            </dl>
                        </div>

结果如下:

在这里插入图片描述

5)记录选中哪个特殊规格参数值

思路分析

规格参数的格式是这样的:

在这里插入图片描述

每一个规格项是数组中的一个元素,因此我们只要保存被选择的规格项的索引,就能判断哪个是用户选择的了!

我们需要一个对象indexes来保存用户选择的索引,格式如下:

{
    "4":0,
    "12":0,
    "13":0
}

但问题是,第一次进入页面时,用户并未选择任何参数。因此索引应该有一个默认值,我们将默认值设置为0。

我们在script标签中,对索引对象进行初始化:

<script th:inline="javascript">
    //取出SpuDetail对象
    const detail = /*[[${detail}]]*/ {};

    //取出specialSpec特有参数
    let specialSpec = JSON.parse(detail.specialSpec);

    //定义一个对象存储所有特有参数:格式: {"4":"机身颜色"....}
    let paramJson = {};

    //取出所有参数组
    const specs = /*[[${specs}]]*/ [];
    //遍历参数组
    specs.forEach(specGroup=>{
        //遍历组内参数
        specGroup.params.forEach(specParam=>{
            //只需要取出特有参数
            if(!specParam.generic){
                paramJson[specParam.id] = specParam.name;
            }
        });
    })

    //设计一个对象记录每个特有参数的选中下标值  格式: { "4": 2, "12":0,...}
    let indexes = {};
    //初始化值,全部参数默认第一个选中
    for(let key in specialSpec){
        indexes[key] = 0;
    }
</script>


<script>
    var itemVm = new Vue({
        el:"#itemApp",
        data:{
            specialSpec,
            paramJson,
            indexes,
        },
        components:{
            lyTop: () => import('/js/pages/top.js')
    }
    });
</script>

然后在vue中保存:

在这里插入图片描述

添加样式和点击事件代码

我们在页面中,通过判断indexes的值来判断当前规格是否被选中,并且给规格绑定点击事件,点击规格项后,修改indexes中的对应值:

 <div id="specification" class="summary-wrap clearfix">
                            <dl v-for="(value,key,index) in specialSpec" :key="index">
                                <dt>
                                    <div class="fl title">
                                        <i>{{paramJson[key]}}</i>
                                    </div>
                                </dt>
                                <dd v-for="(v,i) in value" :key="i" @click="indexes[key]=i">
                                    <a href="javascript:;" :class="{selected:indexes[key]==i}">
                                        {{v}}<span title="点击取消选择">&nbsp;</span>
                                    </a>
                                </dd>
                            </dl>
                        </div>

效果:

在这里插入图片描述

6)如何切换不同的Sku

取出所有Sku对象

我们可以获取skus值,并放入Vue的data中。

在这里插入图片描述

使用Vue插件查看数据:

在这里插入图片描述

添加计算属性存储选中的Sku对象

现在问题是,如何确定我们选择的规格参数,属于哪个Sku呢?

我们设计sku数据的时候,就已经在tb_sku表添加了一个字段:indexes。这其实就是规格参数的索引组合。

而我们在页面中,用户点击选择规格后,就会把对应的索引保存起来:

在这里插入图片描述

因此,我们可以根据这个indexes来确定用户要选择的sku

我们在vue中定义一个计算属性,来计算与索引匹配的sku:

<script th:inline="javascript">
    //取出SpuDetail对象
    const detail = /*[[${detail}]]*/ {};

    //取出specialSpec特有参数
    let specialSpec = JSON.parse(detail.specialSpec);

    //定义一个对象存储所有特有参数:格式: {"4":"机身颜色"....}
    let paramJson = {};

    //取出所有参数组
    const specs = /*[[${specs}]]*/ [];
    //遍历参数组
    specs.forEach(specGroup=>{
        //遍历组内参数
        specGroup.params.forEach(specParam=>{
            //只需要取出特有参数
            if(!specParam.generic){
                paramJson[specParam.id] = specParam.name;
            }
        });
    })

    //设计一个对象记录每个特有参数的选中下标值  格式: { "4": 2, "12":0,...}
    let indexes = {};
    //初始化值,全部参数默认第一个选中
    for(let key in specialSpec){
        indexes[key] = 0;
    }

    //取出所有Sku
    const skus = /*[[${skus}]]*/ [];
</script>


<script>
    var itemVm = new Vue({
        el:"#itemApp",
        data:{
            ly,
            specialSpec,
            paramJson,
            indexes,
            skus,
        },
        computed:{
          //定义选中的Sku对象
            selectedSku(){
                //1.把当前选择的indexes下表转换为字符串:0_0_0
                //Object.values():取出对象的value值,返回一个数组
                //join(): 数组的方法,使用指定分隔符拼接成一个新的字符串
                let indexes = Object.values(this.indexes).join("_");

                //find()方法: 在数组中查询满足条件的对象
                return this.skus.find(sku=>sku.indexes==indexes);
            },
            //处理多种图片
            images(){
                return this.selectedSku.images.split(",") || this.selectedSku.images;
            }
        },
        components:{
            lyTop: () => import('/js/pages/top.js')
    }
    });
</script>

在浏览器工具中查看:

在这里插入图片描述

7)展示Sku信息

既然已经拿到了用户选中的sku,接下来,就可以在页面渲染数据了

图片列表

商品图片是一个字符串,以,分割,页面展示比较麻烦,所以我们编写一个**计算属性:**images(),将图片字符串变成数组:

<script th:inline="javascript">
    //取出SpuDetail对象
    const detail = /*[[${detail}]]*/ {};

    //取出specialSpec特有参数
    let specialSpec = JSON.parse(detail.specialSpec);

    //定义一个对象存储所有特有参数:格式: {"4":"机身颜色"....}
    let paramJson = {};

    //取出所有参数组
    const specs = /*[[${specs}]]*/ [];
    //遍历参数组
    specs.forEach(specGroup=>{
        //遍历组内参数
        specGroup.params.forEach(specParam=>{
            //只需要取出特有参数
            if(!specParam.generic){
                paramJson[specParam.id] = specParam.name;
            }
        });
    })

    //设计一个对象记录每个特有参数的选中下标值  格式: { "4": 2, "12":0,...}
    let indexes = {};
    //初始化值,全部参数默认第一个选中
    for(let key in specialSpec){
        indexes[key] = 0;
    }

    //取出所有Sku
    const skus = /*[[${skus}]]*/ [];
</script>


<script>
    var itemVm = new Vue({
        el:"#itemApp",
        data:{
            ly,
            specialSpec,
            paramJson,
            indexes,
            skus,
        },
        computed:{
          //定义选中的Sku对象
            selectedSku(){
                //1.把当前选择的indexes下表转换为字符串:0_0_0
                //Object.values():取出对象的value值,返回一个数组
                //join(): 数组的方法,使用指定分隔符拼接成一个新的字符串
                let indexes = Object.values(this.indexes).join("_");

                //find()方法: 在数组中查询满足条件的对象
                return this.skus.find(sku=>sku.indexes==indexes);
            },
            //处理多种图片
            images(){
                return this.selectedSku.images.split(",") || this.selectedSku.images;
            }
        },
        components:{
            lyTop: () => import('/js/pages/top.js')
    }
    });
</script>

页面改造:

<div class="product-info">
                <div class="fl preview-wrap">
                    <!--放大镜效果-->
                    <div class="zoom">
                        <!--默认第一个预览-->
                        <div id="preview" class="spec-preview">
							<span class="jqzoom">
								<img :jqimg="images[0] || images" :src="images[0] || images" width="400px" height="400px"/>
							</span>
                        </div>
                        <!--下方的缩略图-->
                        <div class="spec-scroll">
                            <a class="prev">&lt;</a>
                            <!--左右按钮-->
                            <div class="items">
                                <ul>
                                    <li v-for="(img,i) in images" :key="i">
                                        <img :src="img" :bimg="img" onmousemove="preview(this)" />
                                    </li>
                                </ul>
                            </div>
                            <a class="next">&gt;</a>
                        </div>
                    </div>
                </div>
                <div class="fr itemInfo-wrap">
                    <div class="sku-name">
                        <h4>{{selectedSku.title}}</h4>
                    </div>
                    <div class="news"><span th:utext="${subTitle}">推荐选择下方[移动优惠购],手机套餐齐搞定,不用换号,每月还有花费返</span></div>
                    <div class="summary">
                        <div class="summary-wrap">
                            <div class="fl title"><i>价  格</i></div>
                            <div class="fl price">
                                <i>¥</i><em>{{ly.formatPrice(selectedSku.price)}}</em><span>降价通知</span>
                            </div>
                            <div class="fr remark"><i>累计评价</i><em>612188</em></div>
                        </div>
                        <div class="summary-wrap">
                            <div class="fl title">
                                <i>促  销</i>
                            </div>
                            <div class="fl fix-width">
                                <i class="red-bg">加价购</i>
                                <em class="t-gray">满999.00另加20.00元,或满1999.00另加30.00元,或满2999.00另加40.00元,即可在购物车换
                                    购热销商品</em>
                            </div>
                        </div>
                    </div>
                    <div class="support">
                        <div class="summary-wrap">
                            <div class="fl title">
                                <i>支  持</i>
                            </div>
                            <div class="fl fix-width">
                                <em class="t-gray">以旧换新,闲置手机回收  4G套餐超值抢  礼品购</em>
                            </div>
                        </div>
                        <div class="summary-wrap">
                            <div class="fl title">
                                <i>配 送 至</i>
                            </div>
                            <div class="fl fix-width">
                                <em class="t-gray">上海 <span>有货</span></em>
                            </div>
                        </div>
                    </div>

效果:

在这里插入图片描述

标题和价格

在这里插入图片描述

记得在Vue的data加入ly对象,这样才可以使用ly对象的方法

在这里插入图片描述

完整效果

在这里插入图片描述

8)参考item.html渲染其它数据

我们课堂上只渲染一部分数据,大家可以参考资料提供的item.html抽空再把其他数据进行渲染。

06、页面静态化:实现页面静态化

本次课程我们采用Thymeleaf来实现页面静态化。

1)概念

先说下Thymeleaf中的几个概念:

  • Context:运行上下文
  • TemplateResolver:模板解析器
  • TemplateEngine:模板引擎

Context

上下文: 用来保存模型数据,当模板引擎渲染时,可以从Context上下文中获取数据用于渲染。

当与SpringBoot结合使用时,我们放入Model的数据就会被处理到Context,作为模板渲染的数据使用。

TemplateResolver

模板解析器:用来读取模板相关的配置,例如:模板存放的位置信息,模板文件名称,模板文件的类型等等。

当与SpringBoot结合时,TemplateResolver已经由其创建完成,并且各种配置也都有默认值,比如模板存放位置,其默认值就是:templates。比如模板文件类型,其默认值就是html。

TemplateEngine

模板引擎:用来解析模板的引擎,需要使用到上下文、模板解析器。分别从两者中获取模板中需要的数据,模板文件。然后利用内置的语法规则解析,从而输出解析后的文件。来看下模板引起进行处理的函数:

templateEngine.process("模板名", context, writer);

三个参数:

  • 模板名称
  • 上下文:里面包含模型数据
  • writer:输出目的地的流

在输出时,我们可以指定输出的目的地,如果目的地是Response的流,那就是网络响应。如果目的地是本地文件,那就实现静态化了。

而在SpringBoot中已经自动配置了模板引擎,因此我们不需要关心这个。现在我们做静态化,就是把输出的目的地改成本地文件即可!

2) 在nginx服务器创建item目录

说明,静态页面服务器一般选择使用nginx。

搭建思路

在这里插入图片描述

具体实现就是在nginx的html目录下创建一个item文件夹即可

在这里插入图片描述

3) 在配置文件中配置静态化页面的信息

在application.yml中配置生成静态文件的目录:


ly:
  static:
    itemDir: D:\java\nginx-1.21.6\html\item #静态页服务器地址
    itemTemplate: item #模板名称

4) 编写生成静态化页面的service方法

 /**
     * 生成商品的静态页面
     */
    public void createStaticPage(Long spuId){
        //1)创建Context上下文对象(读取模板文件需要的动态数据)
        Context context = new Context();
        //设置动态数据
        context.setVariables(getDetailData(spuId));
        //2)定义静态模板((item.html)
        String tmepName = itemTemplate+".html";
        //3)使用模板引擎对象读取静态目标,把动态数据写入模板,使用IO输出流生成一个静态文件

        //生成 静态页名称
        String fileName = spuId+".html";

        PrintWriter writer = null;

        try {
            writer = new PrintWriter(new File(itemDir,fileName));
            templateEngine.process(tmepName,context,writer);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意:一旦涉及自定义输出流,必须关闭输出流,否则后续无法删除该文件
            writer.close();
        }

    }

5) 使用测试类生成静态页

package com.leyou;

import com.leyou.page.service.PageService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = LyPageApplication.class)
public class PageServiceTest {
    @Autowired
    private PageService pageService;

    @Test
    public void testcreateStaticPage(){
        pageService.createStaticPage(113L);
    }
}


效果为

在这里插入图片描述

发现请求速度得到了极大提升:

在这里插入图片描述

而且你会发现即使停止ly-page微服务也可以访问页面,因为访问的是nginx服务器的静态页面!

07、数据同步:数据同步问题

我们思考这样几个问题:

  • 商品详情页应该在什么时候生成呢?不能每次都用单元测试生成吧?
  • 如果商品数据修改以后,静态页的内容与商品实际内容不符,该如何完成同步?
  • 商品下架后,用户应该不能再搜索该商品,且静态页也不需要了吧,如何处理?

思考上面问题的同时,我们会想起一件事情,其实商品数据如果发生了增、删、改,不仅仅静态页面需要处理,我们的索引库数据也需要同步!!这又该如何解决?

因为商品新增后需要上架用户才能看到,商品修改需要先下架,然后修改,再上架。因此上述问题可以统一的设计成这样的逻辑处理:

  • 商品上架:
    • 生成静态页
    • 新增索引库数据
  • 商品下架:
    • 删除静态页
    • 删除索引库数据

这样既可保证数据库商品与索引库、静态页三者之间的数据同步。

那么,如何实现上述逻辑呢?

先看两种解决方案:

  • 方案1:在商品微服务的上下架业务后,加入修改索引库数据及静态页面的代码
  • 方案2:搜索服务和静态页服务对外提供操作索引库和静态页接口,商品微服务在商品上下架后,调用接口。

以上两种方式都有同一个严重问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立原则,而且严重违背了开闭原则。

所以,我们会通过另外一种方式来解决这个问题:消息队列

解决方案架构图:

在这里插入图片描述

MQ:三大功能,异步化,业务解耦合,提高系统吞吐量,流量削峰。

08、数据同步:RabbitMQ回顾

1)什么是消息队列

消息队列,即MQ,Message Queue。

在这里插入图片描述

消息队列是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。

结合前面所说的问题:

  • 商品服务对商品上下架以后,无需去操作索引库或静态页面,只是发送一条消息,也不关心消息被谁接收。
  • 搜索服务和静态页面服务接收消息,分别去处理索引库和静态页面。

如果以后有其它系统也依赖商品服务的数据,同样监听消息即可,商品服务无需任何代码修改。

2)AMQP和JMS

MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。

在这里插入图片描述

在这里插入图片描述

两者间的区别和联系:

  • JMS是定义了统一的接口,来对消息操作进行统一,AMQP是通过规定协议来统一数据交互的格式。
  • JMS限定了必须使用Java语言,AMQP只是协议,不规定实现方式,因此是跨语言的。
  • JMS规定了两种消息模型,分别是点对点和发布订阅两种,而AMQP的消息模型更加丰富。

3)常见MQ产品

在这里插入图片描述

  • ActiveMQ:基于JMS, Apache

  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会

    ​ RabbitMQ vs RocketMQ

    ​ Queue Queue

    ​ Exchange Topic

    ​ Router Tag

  • Kafka:分布式消息系统,高吞吐量

4)RabbitMQ

RabbitMQ是基于AMQP的一款消息管理系统

官网: http://www.rabbitmq.com/

官方教程:http://www.rabbitmq.com/getstarted.html

在这里插入图片描述

在这里插入图片描述

RabbitMQ基于Erlang语言开发:

在这里插入图片描述

5)RabbitMQ下载

官网下载地址:http://www.rabbitmq.com/download.html

6)RabbitMQ安装

安装,这里就忽略了。

7)RabbitMQ五种模型(*)

RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。

但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。

在这里插入图片描述

8)RabbitMQ基本消息模型

说明

官方文档说明:

RabbitMQ是一个消息的代理者(Message Broker):它接收消息并且传递消息。

你可以认为它是一个邮局:当你投递邮件到一个邮箱,你很肯定邮递员会终究会将邮件递交给你的收件人。与此类似,RabbitMQ 可以是一个邮箱、邮局、同时还有邮递员。

不同之处在于:RabbitMQ不是传递纸质邮件,而是二进制的数据

基本消息模型图:

在这里插入图片描述

在上图的模型中,有以下概念:

  • P:生产者,也就是要发送消息的程序
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
生产者

连接工具类:

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("192.168.56.101");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/leyou");
        factory.setUsername("leyou");
        factory.setPassword("leyou");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

生产者发送消息:

public class Send {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 从连接中创建通道,使用通道才能完成消息相关的操作
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 消息内容
        String message = "Hello World!";
        // 向指定的队列中发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        
        System.out.println(" [x] Sent '" + message + "'");

        //关闭通道和连接
        channel.close();
        connection.close();
    }
}

控制台:
在这里插入图片描述

web控制台查看消息

进入队列页面,可以看到新建了一个队列:simple_queue

在这里插入图片描述

点击队列名称,进入详情页,可以查看消息:

在这里插入图片描述

在控制台查看消息并不会将消息消费,所以消息还在。

消费者获取消息
public class Recv {
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

控制台:

在这里插入图片描述

这个时候,队列中的消息就没了:

在这里插入图片描述

消费者的消息确认机制

通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。

那么问题来了:RabbitMQ怎么知道消息被接收了呢?

这就要通过消息确认机制(Acknowlege)来实现了。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK
  • 手动ACK:消息接收后,不会发送ACK,需要手动调用

大家觉得哪种更好呢?

这需要看消息的重要性:

  • 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
  • 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。

我们之前的测试都是自动ACK的,如果要手动ACK,需要改动我们的代码:

public class Recv2 {
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        final Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
                // 手动进行ACK
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        // 监听队列,第二个参数false,手动进行ACK
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

注意到最后一行代码:

channel.basicConsume(QUEUE_NAME, false, consumer);

如果第二个参数为true,则会自动进行ACK;如果为false,则需要手动ACK。方法的声明:

在这里插入图片描述

9)RabbitMQ的work消息模型

说明

在刚才的基本模型中,一个生产者,一个消费者,生产的消息直接被消费者消费。比较简单。

Work queues,也被称为(Task queues),任务模型。

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。

在这里插入图片描述

角色:

  • P:生产者:任务的发布者
  • C1:消费者,领取任务并且完成任务,假设完成速度较慢
  • C2:消费者2:领取任务并完成任务,假设完成速度快
生产者

生产者与案例1中的几乎一样:

public class Send {
    private final static String QUEUE_NAME = "test_work_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 循环发布任务
        for (int i = 0; i < 50; i++) {
            // 消息内容
            String message = "task .. " + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep(i * 2);
        }
        // 关闭通道和连接
        channel.close();
        connection.close();
    }
}

不过这里我们是循环发送50条消息。

消费者1

在这里插入图片描述

消费者2

在这里插入图片描述

与消费者1基本类似,就是没有设置消费耗时时间。

这里是模拟有些消费者快,有些比较慢。

接下来,两个消费者一同启动,然后发送50条消息:

在这里插入图片描述

可以发现,两个消费者各自消费了25条消息,而且各不相同,这就实现了任务的分发。

能者多劳

刚才的实现有问题吗?

  • 消费者1比消费者2的效率要低,一次任务的耗时较长
  • 然而两人最终消费的消息数量是一样的
  • 消费者2大量时间处于空闲状态,消费者1一直忙碌

现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。

怎么实现呢?

我们可以修改设置,让消费者同一时间只接收一条消息,这样处理完成之前,就不会接收更多消息,就可以让处理快的人,接收更多消息 :

在这里插入图片描述

再次测试:

在这里插入图片描述

10)RabbitMQ订阅模型分类

订阅模型示意图:

在这里插入图片描述

前面2个案例中,只有3个角色:

  • P:生产者,也就是要发送消息的程序
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

而在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
  • C:消费者,消息的接受者,会一直等待消息到来。
  • Queue:消息队列,接收消息、缓存消息。
  • Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

11)RabbitMQ订阅模型-Fanout

Fanout,也称为广播。

流程说明

流程图:

在这里插入图片描述

在广播模式下,消息发送流程是这样的:

  • 1) 可以有多个消费者
  • 2) 每个消费者有自己的queue(队列)
  • 3) 每个队列都要绑定到Exchange(交换机)
  • 4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
  • 5) 交换机把消息发送给绑定过的所有队列
  • 6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
生产者

两个变化:

  • 1) 声明Exchange,不再声明Queue
  • 2) 发送消息到Exchange,不再发送到Queue
public class Send {

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        
        // 声明exchange,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        
        // 消息内容
        String message = "Hello everyone";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [生产者] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

消费者1
public class Recv {
    private final static String QUEUE_NAME = "fanout_exchange_queue_1";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

要注意代码中:队列需要和交换机绑定

消费者2
public class Recv2 {
    private final static String QUEUE_NAME = "fanout_exchange_queue_2";

    private final static String EXCHANGE_NAME = "fanout_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

测试

我们运行两个消费者,然后发送1条消息:

在这里插入图片描述

12)RabbitMQ订阅模型-Direct

说明

在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
  • 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey
  • Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息

流程图:

在这里插入图片描述

图解:

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
  • C1:消费者,其所在队列指定了需要routing key 为 error 的消息
  • C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
生产者

此处我们模拟商品的增删改,发送消息的RoutingKey分别是:insert、update、delete

public class Send {
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "商品新增了, id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

消费者1

我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品。

public class Recv {
    private final static String QUEUE_NAME = "direct_exchange_queue_1";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要update和delete消息
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2

我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。

public class Recv2 {
    private final static String QUEUE_NAME = "direct_exchange_queue_2";
    private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

测试

我们分别发送增、删、改的RoutingKey,发现结果:

在这里插入图片描述

13)RabbitMQ订阅模型-Topic

说明

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

举例:

item.#:能够匹配item.spu.insert 或者 item.spu

item.*:只能匹配item.spu

图示:

在这里插入图片描述

解释:

  • 红色Queue:绑定的是usa.# ,因此凡是以 usa.开头的routing key 都会被匹配到
  • 黄色Queue:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配
生产者

使用topic类型的Exchange,发送消息的routing key有3种: item.isnertitem.updateitem.delete

public class Send {
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 消息内容
        String message = "新增商品 : id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

消费者1

我们此处假设消费者1只接收两种类型的消息:更新商品和删除商品

public class Recv {
    private final static String QUEUE_NAME = "topic_exchange_queue_1";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。需要 update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.update");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.delete");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者1] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

消费者2

我们此处假设消费者2接收所有类型的消息:新增商品,更新商品和删除商品。

/**
 * 消费者2
 */
public class Recv2 {
    private final static String QUEUE_NAME = "topic_exchange_queue_2";
    private final static String EXCHANGE_NAME = "topic_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        
        // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insert、update、delete
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [消费者2] received : " + msg + "!");
            }
        };
        // 监听队列,自动ACK
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

14)RabbitMQ持久化

如何避免消息丢失?

1) 消费者的ACK机制。可以防止消费者丢失消息。

2) 但是,如果在消费者消费之前,MQ就宕机了,消息就没了。

所以我们需要将消息持久化到硬盘,以防服务宕机。

要将消息持久化,前提是:队列、Exchange都持久化

交换机持久化

在这里插入图片描述

队列持久化

在这里插入图片描述

消息持久化

在这里插入图片描述

09、数据同步:创建rabbitMQ用户并授权

1) 创建用户

在这里插入图片描述

效果如下

在这里插入图片描述

2) 创建虚拟机

在这里插入图片描述

效果如下

在这里插入图片描述

3) 给leyouxxx用户授权

在这里插入图片描述

效果如下

在这里插入图片描述

4) 切换到leyouxxx用户

在这里插入图片描述

10、数据同步:SpringAMQP的使用入门

1)简介

Sprin有很多不同的项目,其中就有对AMQP的支持:

在这里插入图片描述

Spring AMQP的页面:http://projects.spring.io/spring-amqp/

在这里插入图片描述

注意这里一段描述:

     Spring-amqp是对AMQP协议的抽象实现,而spring-rabbit 是对协议的具体实现,也是目前的唯一实现。底层使用的就是RabbitMQ。


2)依赖和配置

添加AMQP的启动器:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ithiema</groupId>
    <artifactId>spring-boot-rabbitmq</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

application.yml中添加RabbitMQ地址:

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    virtual-host: /leyou
    username: leyou
    password: leyou

编写SpringBoot启动类:

package cn.itcast;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 *
 */
@SpringBootApplication
public class RabbitMQApplication {
    public static void main(String[] args) {
        SpringApplication.run(RabbitMQApplication.class,args);
    }
}

3)监听者

在SpringAmqp中,对消息的消费者进行了封装和抽象,一个普通的JavaBean中的普通方法,只要通过简单的注解,就可以成为一个消费者。

package com.itheima.consumer;

import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * 消费方
 */
@Component
public class MQListener {

    /**
     * 接收消费的方法
     *   value: 绑定队列信息
     *   exchange: 绑定交换机信息
     *   key: 绑定路由信息
     */
    @RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(name = "ly_test_queue"),
                    exchange = @Exchange(name = "ly_test_exchange",type = ExchangeTypes.TOPIC),
                    key = "user.#"
            )
    )
    public void retrireMsg(String msg){
        System.out.println("接收的消息:"+msg);
    }

}



  • @Componet:类上的注解,注册到Spring容器
  • @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性:
    • bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性:
      • value:这个消费者关联的队列。值是@Queue,代表一个队列
      • exchange:队列所绑定的交换机,值是@Exchange类型
      • key:队列和交换机绑定的RoutingKey

类似listen这样的方法在一个类中可以写多个,就代表多个消费者。

4)消息发送:AmqpTemplate

Spring最擅长的事情就是封装,把他人的框架进行封装和整合。

Spring为AMQP提供了统一的消息处理模板:AmqpTemplate,非常方便的发送消息,其发送方法:

在这里插入图片描述

红框圈起来的是比较常用的3个方法,分别是:

  • 指定交换机、RoutingKey和消息体
  • 指定消息
  • 指定RoutingKey和消息,会向默认的交换机发送消息

5)测试代码

package com.itheima;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMQApplication.class)
public class MQTest {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Test
    public void testSendMs(){
        amqpTemplate.convertAndSend(
                "ly_test_exchange",
                "user.insert",
                "测试MQ是否可以发送消息");

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}


运行后查看日志:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值