二级缓存、商品上下架的ES操作

一、首页加载的解决方案

nginx+lua+redis解决
当天的GIT地址:

商品的上架、商品的下架对应着ES添加、删除
当mysql里的首页广告表数据发生改变。我们需要手动发送一个请求到nginx里执行lua脚本。让其mysql最新的数据发送到redis里。下面使用canal来解决手动方式

  • 1)缓存预热:
  • 缓存预热:通过lua脚本,对广告指定位置的数据进行以及缓存。 lua脚本就会根据位置信息,从mysql数据库中,查询出广告数据,存入到redis中。(缓存预热,只有数据库发生变更的时候,才需要执行。)

在这里插入图片描述

  • 2)二级缓存

    完成广告数据的读取。我们使用了nginx的本地缓存(二级缓存)。先从nginx本地缓存中查询数据,如果没有查询到数据,那么就查询redis中数据,并存入本地缓存,设置缓存时间10分钟

在这里插入图片描述

实现步骤:

  1. 后台数据管理,通过后台管理系统,对mysql中的广告(首页内容)进维护。
  2. 定义广告内容,表中设计了 position 位置字段 ,标识广告图片的显示位置。
  3. 将首页页面,通过nginx进行部署
  4. 通过lua脚本,对广告指定位置的数据进行以及缓存。 lua脚本就会根据位置信息,从mysql数据库中,查询出广告数据,存入到redis中。(缓存预热,只有数据库发生变更的时候,才需要执行。)
  5. 通过lua脚本,完成广告数据的读取。我们使用了nginx的本地缓存(二级缓存)。先从nginx本地缓存中查询数据,如果没有查询到数据,那么就查询redis中数据,并存入本地缓存,设置缓存时间10分钟。
  6. 页面通过VUE,发送ajax请求,访问读取广告数据的lua脚本,展示数据。

二、自动更新缓存预热

解决方式:

  1. 安装了canal,进行mysql数据库修改的监听。
  2. 搭建了数据监控服务,通过配置,可以监听广告表的变化。
  3. 在数据监控服务的监听器中,获取修改的广告信息,将广告的position作为消息的内容发送MQ消息
  4. 运营微服务作为MQ的消费者,接收消息中的position,通过HTTP请求,向nginx发送缓存预热的请求

在这里插入图片描述

代码的实现:

  • 1、创建一个canal模块

  • 2、导入依赖

    <dependencies>
        <!--canal-->
        <dependency>
            <groupId>com.xpand</groupId>
            <artifactId>starter-canal</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    
        <!--rabbit-->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
        </dependency>
    
    </dependencies>
    

    创建一个消息队列类

    @Configuration
    public class RabbitMQConfig {
    
     //定义队列名称
        public static final String SEARCH_ADD_QUEUE="search_add_queue";
        
          //创建一个队列
        @Bean(SEARCH_ADD_QUEUE)
        public Queue SEARCH_ADD_QUEUE() {
            return new Queue(SEARCH_ADD_QUEUE);
        }
        
     }
    

    创建一个canal类监听数据库

    注解 类上声名为:当前的类是canal的监听类 @CanalEventListener

    注解 方法声名 @ListenPoint(schema = “changgou_business”,table = “tb_ad”) 并指定数据库的名称和表名称

    方法参数:CanalEntry.EventType eventType, CanalEntry.RowData rowData

    大致思路:

    • 1、canal监听到了ta_ad表里的数据发生改变了
    • 2、我们通过rowData.getBeforeColumnsList获取到改变前数据。并用map以这个这个行数据的表中字段名称(name)作为key,字段对应的值作为value封装到map中。
    • 3、我们通过rowData.getAfterColumnsList获取到改变后数据。并用map以这个这个行数据的表中字段名称(name)作为key,字段对应的值作为value封装到map中。
    • 4、取出改变前数据position、改后前数据position。对比两个前后这个数据有没有改变。发生改变的话需要发送两个消息。(告诉nginx需要根据这个两个字段分别查询mysql,并将最新的数据存入redis中。)
    • 5、相同的话就发送一个改变后的position的vlaue。放入到消息队列中
    @CanalEventListener  //声明当前的类是canal的监听类
    public class BusinessListener {
    
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @ListenPoint(schema = "changgou_business",table = "tb_ad")
        public void adUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
            System.out.println("广告数据发生变化了");
           
    
            Map<String,String> oldData  = new HashMap<>();
    
            Map<String,String> newData  = new HashMap<>();
    
    
    		//通过rowData获取改变前后的数据
    
            //获取改变后的数据。
            rowData.getBeforeColumnsList().forEach(column -> oldData.put(column.getName(),column.getValue()));
    
            //获取改变前数据
            rowData.getAfterColumnsList().forEach(column -> newData.put(column.getName(),column.getValue()));
    
            //判断position有没有内容有没有发送改变
            if (oldData.get("position") != null && newData.get("position") != null && !oldData.get("position").equals(newData.get("position"))) {
                //告诉redis跟新(老的)的资源
                rabbitTemplate.convertAndSend("", RabbitMQConfig.AD_UPDATE_QUEUE, oldData.get("position"));
                //告诉redis跟新(新的)的资源
                rabbitTemplate.convertAndSend("", RabbitMQConfig.AD_UPDATE_QUEUE, newData.get("position"));
            } else {
                rabbitTemplate.convertAndSend("", RabbitMQConfig.AD_UPDATE_QUEUE, newData.get("position"));
            }
        }
    }
    

在changgou_service_business创建一个消息队列监听类。

  • 1、监听到ad_update_queue队列里有消息
  • 2、根据监听到消息通过okHttpClient来进行远程调用(也可以使用:RestTemplate来进行远程调用)。来进行自动缓存预热
  • 3、注意注解 @Component、@RabbitListener(queues = “ad_update_queue”)
@Component
public class AdListener {

    //监听队列
    @RabbitListener(queues = "ad_update_queue")
    public void receiveMessage(String message) {
        System.out.println("接收到的消息为:"+message);

        //发起远程调用
        OkHttpClient okHttpClient = new OkHttpClient();

        //调用url
        String url="http://192.168.200.128/ad_update?position="+message;

        //创建一个请求体
        Request request = new Request.Builder().url(url).build();

        //发送请求
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //请求失败
                e.printStackTrace();

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                System.out.println(message+"消息请求成功"+response.message());
            }
        });

    }
}

三、商品上架sku导入ES

3.1 消息队列的配置
@Configuration
public class RabbitMQConfig {

    //定义商品上架队列名称
    public static final String SEARCH_ADD_QUEUE="search_add_queue";
    //交换机商品上架名称
    public static final String GOODS_UP_EXCHANGE="goods_up_exchange";
    
    
    
    //创建队列上架的对列
    @Bean(SEARCH_ADD_QUEUE)
    public Queue SEARCH_ADD_QUEUE() {
        return new Queue(SEARCH_ADD_QUEUE);
    }


    //创建队列上架的交换机
    @Bean(GOODS_UP_EXCHANGE)
    public Exchange GOODS_UP_EXCHANGE() {
        return ExchangeBuilder.fanoutExchange(GOODS_UP_EXCHANGE).build();
    }
    
     //交换机绑定队列 设定路由规则为 ""
    @Bean
    public Binding AD_UPDATE_QUEUE_BINDING(@Qualifier(SEARCH_ADD_QUEUE) Queue queue, @Qualifier(GOODS_UP_EXCHANGE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    }

}
3.2 canal的监听的类
  • 1、监听的是changgou_goods数据库下的tb_spu表

  • 2、将更新前的数据和更新后的数据封装到oldMap、newMap中

  • 3、判断更新前后的is_marketable对应的状态码是不是由 0–>1(上架状态)

  • 4、上架状态把spu的id传入到交换机中。

@CanalEventListener   //声明当前的类是canal的监听类
public class SpuListener {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 监听spu表
     * @param eventType
     * @param rowData
     */
    @ListenPoint(schema = "changgou_goods",table = "tb_spu")
    public void goodsUp(CanalEntry.EventType eventType,CanalEntry.RowData rowData){

        Map<String,String> oldData  = new HashMap<>();

        Map<String,String> newData  = new HashMap<>();

        //将数据库老数据封装到map中
        rowData.getBeforeColumnsList().forEach(column -> oldData.put(column.getName(),column.getValue()));

        //将数据库新数据封装到map中
        rowData.getAfterColumnsList().forEach(column -> newData.put(column.getName(),column.getValue()));

        //判断是不是上架状态  上架状态从 0 -> 1
        if (oldData.get("is_marketable").equals("0") && newData.get("is_marketable").equals("1")) {
            rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_UP_EXCHANGE,"",newData.get("id"));
        }
    }
}

3.3 ES的对应的实体类

指定属性值的对应的 1、索引 2、持久化 3、文件类型 4、分词器类型。用于ES的dao层

@Document(indexName = "skuinfo", type = "docs")     //1、索引名称 2、 maiping
public class SkuInfo implements Serializable {
    //商品id,同时也是商品编号
    @Id
    @Field(index = true, store = true, type = FieldType.Keyword)
    private Long id;

    //SKU名称     //1、索引 2、持久化 3、文件类型 4、分词器类型
    @Field(index = true, store = true, type = FieldType.Text, analyzer = "ik_smart")
    private String name;

    //商品价格,单位为:元
    @Field(index = true, store = true, type = FieldType.Double)
    private Long price;

    //库存数量
    @Field(index = true, store = true, type = FieldType.Integer)
    private Integer num;

    //商品图片
    @Field(index = false, store = true, type = FieldType.Text)
    private String image;

    //商品状态,1-正常,2-下架,3-删除
    @Field(index = true, store = true, type = FieldType.Keyword)
    private String status;

    //创建时间
    private Date createTime;

    //更新时间
    private Date updateTime;

    //是否默认
    @Field(index = true, store = true, type = FieldType.Keyword)
    private String isDefault;

    //SPUID
    @Field(index = true, store = true, type = FieldType.Long)
    private Long spuId;

    //类目ID
    @Field(index = true, store = true, type = FieldType.Long)
    private Long categoryId;

    //类目名称
    @Field(index = true, store = true, type = FieldType.Keyword)
    private String categoryName;

    //品牌名称
    @Field(index = true, store = true, type = FieldType.Keyword)
    private String brandName;

    //规格
    private String spec;

    //规格参数
    private Map<String, Object> specMap;
    
    }
3.4 Sku的根据Spu_Id查询(Controller)
  • 1、当传入的spuId不为 “all”字符串的话,查询所有的sku的数据
  • 2、不为“all”字符串的话,查询出spuId对应的Sku且审核状态为“1”的数据
/**
 * 根据spu的id查询对应的sku
 * @param spuId
 * @return
 */
@GetMapping("/spu/{spuId}")
public List<Sku> findSkuListBySpuId(@PathVariable("spuId") String spuId){

    Map<String, Object> searchMap = new HashMap<>();

    //当spuId里传入的不是all,根据传入的id条件查询
    if (!spuId.equals("all")) {
        searchMap.put("spuId", spuId);
    }

    //审核通过
    searchMap.put("status", "1");

    List<Sku> skus = skuService.findList(searchMap);
    return skus;
}
3.5 Sku的feign接口

在changgou_service_goods_api模块定义一个SkuFeign接口,用来被远程调用goods里的服务

@FeignClient(name = "goods")  //从eureka里获取服务地址
public interface SkuFeign {

    /**
     * 远程调用sku的根据spu的id查询服务
     * @param spuId
     * @return
     */
    @GetMapping("/sku/spu/{spuId}")
    List<Sku> findSkuListBySpuId(@PathVariable("spuId") String spuId);
}
3.6 ES模块
3.6.1 启动类

注意需要:加上EnableFeignClients注解来扫描feign接口的位置

@SpringBootApplication
@EnableEurekaClient

//扫描goods_api下的feign接口
@EnableFeignClients(basePackages = {"com.changgou.goods.feign"})
public class SearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class, args);
    }
}
3.6.2 dao层

需要继承ElasticsearchRepository接口并指定泛型为<SkuInfo,Long> (类上通用mapper的用法)

//通用的Elasticsearch的方法
public interface ESManagerMapper extends ElasticsearchRepository<SkuInfo, Long> {}
3.6.3 service层

查询所有Sku,并添加ES:

  • 1、注入skuFeign接口。

  • 2、通过远程调用将“all”字符串传入,获取所有Sku

  • 3、将Sku集合的转换为JSON,再将Sku的JSON转换为SkuInfo集合对象(因为两个对象的属性名称相同)

  • 4、将SkuInfo里的商品规格的字符json转为map

  • 5、调用ES 添加的方法

    /**
     * 导入全部数据进入es
     */
    @Override
    public void importAll() {
        //查询所有
        List<Sku> skuList = skuFeign.findSkuListBySpuId("all");
    
        if (skuList.size() > 0 && skuList == null) {
            throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
        }
    
        //将sku转换为Json
        String jsonSku = JSON.toJSONString(skuList);
    
        //把skuJson转换为skuInfo集合
        List<SkuInfo> infoList = JSON.parseArray(jsonSku,  SkuInfo.class);
    
        //把商品规格的字符json转为map
        for (SkuInfo skuInfo : infoList) {
            Map map = JSON.parseObject(skuInfo.getSpec(), Map.class);
            skuInfo.setSpecMap(map);
        }
    
        //添加进去
        esManagerMapper.saveAll(infoList);
    }
    

根据Spu_id查询所有并添加ES:

​ 需要将监听到的上架的Spu的id,传入到ES添加方法中

@Component
public class GoodsUpListener {


    @Autowired
    private ESManagerService esManagerService;



    //监听上架队列的名称
    @RabbitListener(queues = RabbitMQConfig.SEARCH_ADD_QUEUE)
    public void receiveMessage(String spuId){

        System.out.println("商品上架的id为"+spuId);

        //将spu的id发送ES的service
        esManagerService.importDataBySpuId(spuId);
    }
    
}

查询消息队列传入的SpuId,并添加ES:

  • 1、注入skuFeign接口。
  • 2、从消息队列获取到上架的Sup的id
  • 2、通过远程调用将Sup的Id传入,获取对应的Sku
  • 3、将Sku集合的转换为JSON,再将Sku的JSON转换为SkuInfo集合对象(因为两个对象的属性名称相同)
  • 4、将SkuInfo里的商品规格的字符json转为map
  • 5、调用ES 添加的方法
/**
 * 根据spuid查询skuList,再导入索引库
 * @param spuId
 * 
 */
@Override
public void importDataBySpuId(String spuId) {  //是消息队列监听类,传入的上架的spuId
    //通过spu的id查询上来对应的sku
    List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);

    if (skuList.size() > 0 && skuList == null) {
        throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
    }

    //将sku转换为Json
    String jsonSku = JSON.toJSONString(skuList);

    //把skuJson转换为skuInfo集合
    List<SkuInfo> infoList = JSON.parseArray(jsonSku,  SkuInfo.class);

    //把商品规格的字符json转为map
    for (SkuInfo skuInfo : infoList) {
        Map map = JSON.parseObject(skuInfo.getSpec(), Map.class);
        skuInfo.setSpecMap(map);
    }

    //添加进去
    esManagerMapper.saveAll(infoList);
}

创建索引库结构

要执行ES的添加方法需要,先执行这个方法

/**
 * 创建索引库结构
 */
@Override
public void createMappingAndIndex() {

    //创建索引
    elasticsearchTemplate.createIndex(SkuInfo.class);
    //创建映射
    elasticsearchTemplate.putMapping(SkuInfo.class);
}
3.6.4 controller层
@RestController
@RequestMapping("/manager")
public class ESManagerController {
    @Autowired
    private ESManagerService esManagerService;

    //创建索引库结构
    @GetMapping("/create")
    public Result create(){
        esManagerService.createMappingAndIndex();
        return new Result(true, StatusCode.OK,"创建索引库结构成功");
    }

    //导入全部数据
    @GetMapping("/importAll")
    public Result importAll(){
        esManagerService.importAll();
        return new Result(true, StatusCode.OK,"导入全部数据成功");
    }
}

四、商品下架 ES删除对应的Sku

4.1 消息队列配置
@Configuration
public class RabbitMQConfig {
    
     //定义商品下架队列名称
    public static final String SEARCH_DEL_QUEUE="search_del_queue";
    //交换机商品下架名称
    public static final String GOODS_DOWN_EXCHANGE="goods_down_exchange";
    
    //创建队列下架的对列
    @Bean(SEARCH_DEL_QUEUE)
    public Queue SEARCH_DEL_QUEUE() {
        return new Queue(SEARCH_DEL_QUEUE);
    }
    
    
     //创建队列下架的交换机
    @Bean(GOODS_DOWN_EXCHANGE)
    public Exchange GOODS_DOWN_EXCHANGE() {
        return ExchangeBuilder.fanoutExchange(GOODS_DOWN_EXCHANGE).build();
    }
    
    //交换机绑定队列 设定路由规则为 ""
    @Bean
    public Binding GOODS_DOWN_EXCHANGE_BINDING(@Qualifier(SEARCH_DEL_QUEUE) Queue queue, @Qualifier(GOODS_DOWN_EXCHANGE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("").noargs();
    }
}
4.2 canal的监听类
  • 1、监听到商品的 is_marketable的字段 下架状态从 1 -> 0
  • 2、向交换机里发送消息 消息内容为spu的id
    @CanalEventListener   //声明当前的类是canal的监听类
public class SpuListener {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 监听spu表
     * @param eventType
     * @param rowData
     */
    @ListenPoint(schema = "changgou_goods",table = "tb_spu")
    public void goodsUp(CanalEntry.EventType eventType,CanalEntry.RowData rowData){

        Map<String,String> oldData  = new HashMap<>();

        Map<String,String> newData  = new HashMap<>();

        //将数据库老数据封装到map中
        rowData.getBeforeColumnsList().forEach(column -> oldData.put(column.getName(),column.getValue()));

        //将数据库新数据封装到map中
        rowData.getAfterColumnsList().forEach(column -> newData.put(column.getName(),column.getValue()));

		//判断是不是上架状态  下架状态从 1 -> 0
        if (oldData.get("is_marketable").equals("1") && newData.get("is_marketable").equals("0")) {
            rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_DOWN_EXCHANGE,"",newData.get("id"));
        }
    }
}
4.3 ES消息监听的队列类

将监听到下架的spu的id传入ES删除业务

@Component
public class GoodsUpListener {


    @Autowired
    private ESManagerService esManagerService;
    
    
     //监听下架队列的名称
    @RabbitListener(queues = RabbitMQConfig.SEARCH_DEL_QUEUE)
    public void delDataBySpuId(String spuId){

        System.out.println("商品下架的id为"+spuId);

        //将spu的id发送service
        esManagerService.delDataBySpuId(spuId);
    }
}

4.4 业务层删除的删除
  • 1、获取消息队列的监听到的下架的Spu的Id

  • 2、根据skuFeign接口查询出来对应的Sku的数据

  • 3、将sku集合遍历,根据Sku的id通过ES的通用mapper,进行删除

    //根据souid删除es索引库中相关的sku数据
    @Override
    public void delDataBySpuId(String spuId) {
        //远程调用sku
        List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
        if (skuList.size() == 0 && skuList == null) {
            throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
        }
    
        //循环删除ES里的SKU
        for (Sku sku : skuList) {
            esManagerMapper.deleteById(Long.parseLong(sku.getId()));
        }
    }
    

System.out.println(“商品下架的id为”+spuId);

    //将spu的id发送service
    esManagerService.delDataBySpuId(spuId);
}

}


#### 4.4 业务层删除的删除

* 1、获取消息队列的监听到的下架的Spu的Id

* 2、根据skuFeign接口查询出来对应的Sku的数据

* 3、将sku集合遍历,根据Sku的id通过ES的通用mapper,进行删除

  ```java
  //根据souid删除es索引库中相关的sku数据
  @Override
  public void delDataBySpuId(String spuId) {
      //远程调用sku
      List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
      if (skuList.size() == 0 && skuList == null) {
          throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
      }
  
      //循环删除ES里的SKU
      for (Sku sku : skuList) {
          esManagerMapper.deleteById(Long.parseLong(sku.getId()));
      }
  }
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值