一、首页加载的解决方案
nginx+lua+redis解决
当天的GIT地址:
商品的上架、商品的下架对应着ES添加、删除
当mysql里的首页广告表数据发生改变。我们需要手动发送一个请求到nginx里执行lua脚本。让其mysql最新的数据发送到redis里。下面使用canal来解决手动方式
- 1)缓存预热:
- 缓存预热:通过lua脚本,对广告指定位置的数据进行以及缓存。 lua脚本就会根据位置信息,从mysql数据库中,查询出广告数据,存入到redis中。(缓存预热,只有数据库发生变更的时候,才需要执行。)
-
2)二级缓存
完成广告数据的读取。我们使用了nginx的本地缓存(二级缓存)。先从nginx本地缓存中查询数据,如果没有查询到数据,那么就查询redis中数据,并存入本地缓存,设置缓存时间10分钟
实现步骤:
- 后台数据管理,通过后台管理系统,对mysql中的广告(首页内容)进维护。
- 定义广告内容,表中设计了 position 位置字段 ,标识广告图片的显示位置。
- 将首页页面,通过nginx进行部署
- 通过lua脚本,对广告指定位置的数据进行以及缓存。 lua脚本就会根据位置信息,从mysql数据库中,查询出广告数据,存入到redis中。(缓存预热,只有数据库发生变更的时候,才需要执行。)
- 通过lua脚本,完成广告数据的读取。我们使用了nginx的本地缓存(二级缓存)。先从nginx本地缓存中查询数据,如果没有查询到数据,那么就查询redis中数据,并存入本地缓存,设置缓存时间10分钟。
- 页面通过VUE,发送ajax请求,访问读取广告数据的lua脚本,展示数据。
二、自动更新缓存预热
解决方式:
- 安装了canal,进行mysql数据库修改的监听。
- 搭建了数据监控服务,通过配置,可以监听广告表的变化。
- 在数据监控服务的监听器中,获取修改的广告信息,将广告的position作为消息的内容发送MQ消息
- 运营微服务作为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()));
}
}