Elastic Search
安装与使用整合到Spring Boot 商城业务
商品的上架Thymeleaf配置Thymeleaf一级分类的渲染二级三级分类的渲染 缓存
Redis的配置将二三级分类的查询存入缓存缓存的穿透、雪崩、击穿分布式锁 Redisson
Elastic Search 安装与使用- Elastic Search的安装与使用
- 新建微服务wlmall-search导入ElasticSearch依赖
//因为Spring Boot会根据其版本来自动更换elasticsearch的依赖,所以在这指定版本 //注意版本对应7.4.2 org.elasticsearch.client elasticsearch-rest-high-level-client 7.4.2
- 添加配置类
@SpringBootConfiguration
public class ElasticSearchConfig {
private static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("Ip",9200,"http")
)
);
return client;
}
}
- 将该微服务加入到注册中心去。插入数据的使用
IndexRequest indexRequest = new IndexRequest("索引名");
indexRequest.id("指定插入数据的id,不指定会默认生成");
//想要插入的数据,封装成对象,放到这里,转化成Json
String s = JSONValue.toJSONString(对象);
indexRequest.source(s, XContentType.JSON);
IndexResponse index = null;
try {
//ElasticSearchConfig.COMMON_OPTIONS是上面的配置类定义的
index = client.index(indexRequest, ElasticSearchConfig.COMMON_OPTIONS);
} catch (IOException e) {
e.printStackTrace();
}
//输出插入的结果
System.out.println(index);
- 查询的使用
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("索引名");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//sourceBuilder.query(查询条件); 如下是查询所有address等于mill的
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = null;
try {
searchResponse = client.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(searchResponse.toString());
注:具体使用可参考官方文档
商城业务 商品的上架大致代码和顺序
Controller
@PostMapping("/{spuId}/up")
public R spuUp(@PathVariable("spuId") Long spuId){
spuInfoService.up(spuId);
return R.ok();
}
Service
public void up(Long spuId) {
//根据spuId来获取SkuInfo的实体对象
List skuInfoEntities = skuInfoService.getSkusBySpuId(spuId);
//根据SpuId获取所有的规格属性
List baseAttrs = productAttrValueService.baseAttrlistforspu(spuId);
//收集所有的规格属性的attrId
List attrIds = baseAttrs.stream().map(ProductAttrValueEntity::getAttrId).collect(Collectors.toList());
//根据attrIds获取所有可以当作检索条件的attr
List searchAttrIds = attrService.selectSearchAttrIds(attrIds);
//将查到的Id都存入Set集合中去
Set idSet = new HashSet<>(searchAttrIds);
//先过滤掉不能被检索的属性,再将过滤后的一些属性赋值给要上架的对象实体并收集起来
List attrsList = baseAttrs.stream()
.filter(item -> idSet.contains(item.getAttrId()))
.map(item -> {
SkuEsModel.Attrs attrs = new SkuEsModel.Attrs();
BeanUtils.copyProperties(item, attrs);
return attrs;
}).collect(Collectors.toList());
//收集所有的SkuId
List skuIdList = skuInfoEntities.stream().map(SkuInfoEntity::getSkuId).collect(Collectors.toList());
//远程调用来判断是否还有库存, 使用try,catch是为了使远程调用失败的时候将库存的值设为null
Map stockMap = null;
try {
R skuHasStock = wareFeignService.getSkusHasStock(skuIdList);
//将调用返回来的数据转化为List 类型,因为两个服务之间使用的是JSON来传输,
TypeReference> typeReference = new TypeReference>() {
};
//简写方式,收集获得库存List集合转化成一个Map集合,其中skuId作为key,stock作为value
stockMap = skuHasStock.getData(typeReference).stream()
.collect(Collectors.toMap(SkuHasStockVo::getSkuId, item -> item.getHasStock()));
} catch (Exception e) {
log.error("库存服务查询异常:原因{}", e);
}
Map finalStockMap = stockMap;
//组合拼装上架的信息
List collect = skuInfoEntities.stream().map(sku -> {
//组装需要的数据
SkuEsModel esModel = new SkuEsModel();
esModel.setSkuPrice(sku.getPrice());
esModel.setSkuImg(sku.getSkuDefaultImg());
// 设置库存信息,如果Map为空也表示有库存,不为空则根据该skuId来判断是否有库存
if (finalStockMap == null) {
esModel.setHasStock(true);
} else {
esModel.setHasStock(finalStockMap.get(sku.getSkuId()));
}
//先将物品的热度评分设置为0 TODO
esModel.setHotScore(0L);
//根据品牌Id 和分类Id 来查询 品牌和分类,并赋值给要上架的对象
BrandEntity brandEntity = brandService.getById(sku.getBrandId());
esModel.setBrandName(brandEntity.getName());
esModel.setBrandId(brandEntity.getBrandId());
esModel.setBrandImg(brandEntity.getLogo());
CategoryEntity categoryEntity = categoryService.getById(sku.getCatalogId());
esModel.setCatalogId(categoryEntity.getCatId());
esModel.setCatalogName(categoryEntity.getName());
// 设置检索属性
esModel.setAttrs(attrsList);
//将剩下对应的数据直接赋值
BeanUtils.copyProperties(sku, esModel);
return esModel;
}).collect(Collectors.toList());
// 远程调用,将数据发给es进行保存
R r = searchFeignService.productStatusUp(collect);
if (r.getCode() == 0) {
// 远程调用成功后,修改当前spu的状态
this.baseMapper.updateSpuStatus(spuId, WareConstant.StatusEnum.SPU_UP.getCode());
} else {
// 远程调用失败
// TODO 以后再来
}
}
远程调用 ware
Cotroller
// 远程调用查询是否还有库存
@PostMapping(value = "/hasStock")
public R getSkuHasStock(@RequestBody List skuIds) {
List vos = wareSkuService.getSkusHasStock(skuIds);
//将获取的数据带回去
System.out.println(R.ok().setData(vos));
return R.ok().setData(vos);
}
public ListgetSkusHasStock(List skuIds) { //使用skuId通过遍历来找出所有的sku库存是否存在 List collect = skuIds.stream().map(skuId -> { SkuHasStockVo vo = new SkuHasStockVo(); Long count = baseMapper.getSkuStock(skuId); vo.setSkuId(skuId); //如果有库存就返回true即可。 vo.setHasStock(count ==null?false:count > 0); return vo; }).collect(Collectors.toList()); return collect; }
远程调用 Search
Controller
@PostMapping("/product")//插入到es中
public R productStatusUp(@RequestBody List skuEsModels){
boolean status = false;
status = elasticSaveService.productStatusUp(skuEsModels);
if (status) {
return R.error(BizCodeEnum.PRODUCT_UP_EXCEPTION.getCode(), BizCodeEnum.PRODUCT_UP_EXCEPTION.getMsg());
} else {
return R.ok();
}
}
Service
@Service
public class ElasticSaveServiceImpl implements ElasticSaveService {
@Autowired
RestHighLevelClient restHighLevelClient;
@Override
public boolean productStatusUp(List skuEsModels) {
// 批量操作,建立es的映射
//在ES中保存这些数据
BulkRequest bulkRequest = new BulkRequest();
for (SkuEsModel skuEsModel : skuEsModels) {
//构造保存请求
IndexRequest indexRequest = new IndexRequest(EsConstant.PRODUCT_INDEX);
//插入时指定Id不指定会给默认值
indexRequest.id(skuEsModel.getSkuId().toString());
//将对象转化为JSON后插入
String jsonString = JSON.toJSONString(skuEsModel);
indexRequest.source(jsonString, XContentType.JSON);
bulkRequest.add(indexRequest);
}
//执行批量操作
BulkResponse bulk = null;
try {
bulk = restHighLevelClient.bulk(bulkRequest, ElasticSearchConfig.COMMON_OPTIONS);
} catch (IOException e) {
e.printStackTrace();
}
//如果批量错误,返回true
boolean hasFailures = bulk.hasFailures();
List collect = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
return hasFailures;
}
}
Thymeleaf
商城的前端页面使用Thymeleaf来写.且.前端页面省略…
配置Thymeleaf添加依赖
一级分类的渲染//用来热部署,不需要每次都重启服务就可以看前端页面效果 org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-devtools true
Controller
@GetMapping({"/","/index.html"}) //访问首页
public String indexPage(Model model){
//查找所有的一级分类
List categoryEntits = categoryService.getLevel1Categorys();
model.addAttribute("categorys",categoryEntits);
return "index"; //字符串类型返回的是页面,thymeleaf会拼接成页面名
}
Service public ListgetLevel1Categorys() { return baseMapper.selectList(new QueryWrapper ().eq("cat_level",1)); }
该部分对应的前端代码
创建实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Catelog2Vo {
private String catalog1Id; // 一级分类的ID
private List catalog3List;//三级分类的集合
private String id;
private String name;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Catelog3Vo{
private String catalog2Id; //二级分类的ID
private String id;
private String name;
}
}
Controller
@ResponseBody
@GetMapping("/index/catalog.json") //前端请求二三级分类所发的路径
public Map> getCatalogJson(){
System.out.println("-------------------------");
Map> map = categoryService.getCatalogJson();
return map;
}
Service public Map缓存 Redis的配置> getCatalogJson() { //获取所有一级分类 List level1Categorys = getLevel1Categorys(); //将数据封装为MAP的形式 Map > parent_cid = level1Categorys.stream().collect(Collectors.toMap(key->key.getCatId().toString(),value->{ //查找所有的二级分类,因为一级分类的ID是二级分类的父ID List categoryEntities = baseMapper.selectList(new QueryWrapper ().eq("parent_cid", value.getCatId())); List catelog2Vos = null; //如果有二级分类 if(categoryEntities != null){ catelog2Vos = categoryEntities.stream().map(item ->{ //有参构造来New一个二级分类对象 Catelog2Vo catelog2Vo = new Catelog2Vo(value.getCatId().toString(),null,item.getCatId().toString(),item.getName()); //查找所有的三级分类,因为二级分类的ID是三级分类的父ID List level3Catalog = baseMapper.selectList(new QueryWrapper ().eq("parent_cid",item.getCatId())); //如果有三级分类 if(level3Catalog != null){ List collect = level3Catalog.stream().map(item3->{ //有参构造来New一个三级分类对象 Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(item.getCatId().toString(),item3.getCatId().toString(),item3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); return parent_cid; }
导入依赖
org.springframework.boot
spring-boot-starter-data-redis
配置yaml
spring:
redis:
host: redis所在的主机Ip
port: redis端口号
将二三级分类的查询存入缓存
public Map缓存的穿透、雪崩、击穿> getCatalogJson() { //先从缓存中获取 String catelogJson = redisTemplate.opsForValue().get("catalogJson"); //如果内存中没有,则从数据库中查询并放入内存 //都以JSON字符串的格式进行存储,方便跨平台,跨语言 if (StringUtils.isEmpty(catelogJson)){ //从数据库中查询,上面二三级分类改为getCatalogJsonDB方法 Map > catalogJsonDB = getCatalogJsonDB(); String s = JSON.toJSONString(catalogJsonDB); redisTemplate.opsForValue().set("catalogJson",s); return catalogJsonDB; } Map > stringCatelog2VoMap = JSON.parseObject(catelogJson, new TypeReference
通俗理解:
①缓存穿透就是当缓存不存在的时候,需要去数据库中查,这个时候,如果几百万个请求(总之就是很多请求),同时访问缓存,因为缓存不存在,所以都会去访问数据库,数据库压力增大,失去了缓存的意义。
②缓存雪崩就是当很多的缓存同时失效,并且很多用户同时访问这些失效的缓存,这些请求都转到了数据库导致数据库压力过重。
③缓存击穿就是当许多用户来进行请求的时候,该缓存刚好失效,以至于这么多请求都会到数据库,就叫做缓存击穿。
- 配置
yaml
org.redisson redisson 3.12.0
配置类
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://47.97.18.245:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
lock锁
①创建一个锁
RLock lock = redissonClient.getLock("锁名");
②加锁
lock.lock(); 阻塞式的等待,默认加的锁都是30S的时间 锁的自动续期:如果业务超长,运行期间会自动给锁续上新的30S,不需要担心业务时间长,锁会自动过期被删掉 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30S以后自动删除
lock.lock(时间,单位); lock.lock(10,TimeUnit.SECONDS)//相当于10秒自动解锁,自动解锁时间一定要大于业务的执行时间. 这样锁的话,在锁的时间到了后不会自动续期 如果规定了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时时间就是指定的时间 如果没指定锁的超时时间,就使用 30 * 1000 即30S,看门狗的默认时间(LockWatchdogTimeout) 只要占锁成功就会启动一个定时任务,重新设置锁的过期时间,新的过期时间就是看门狗的默认时间,每隔十秒都会再次续期,续为30S
一般在设置锁的时候,都会传递超时时间,省掉了整个续期操作,手动解锁



