/*
前面是通用mapper的CRUD,接着canal与mq的运用在广告更新,商品上下架
再接着就是商品的es搜索
在接着就是单点登录,用户认证的问题
后面就是购物车,订单,添加订单的时候很多步骤,很多细节
最后就是分布式事务和微信支付
*/
//在启动eureka的时候报错
StandardEngine[Tomcat].StandardHost[localhost].TomcatEmbeddedContext[] failed to start
//解决办法
进入Project Structure,修改左侧Project、Modules,把其中的Project SDK修改为自己定义的jdk1.8;
docker stop $(docker ps -a -q) // stop停止所有容器
//本项目用的通用mapper,想要分页查询,模糊查询之类如下
public Page<Brand> findPage(Map<String, Object> searchMap, int page, int size) {
//设置分页
PageHelper.startPage(page,size);
//设置查询条件
Example example = new Example(Brand.class);
Example.Criteria criteria = example.createCriteria();
if (searchMap!=null){
//设置品牌名称模糊查询
if (searchMap.get("name")!=null && !"".equals(searchMap.get("name"))){
criteria.andLike("name","%"+searchMap.get("name")+"%");
}
//设置品牌首字母的精确查询
if (searchMap.get("letter")!=null && !"".equals(searchMap.get("letter"))){
criteria.andEqualTo("letter",searchMap.get("letter"));
}
}
Page<Brand> pageInfo = (Page<Brand>) brandMapper.selectByExample(example);
return pageInfo;
}
跨域问题
基于的浏览器的同源策略限制,浏览器会阻止一个域的javascript脚本和另外一个域的内容进行交互,所谓同源或者说同一个域,就是两个页面具有相同的协议,主机和端口,如果有一个不同,那么就是跨域。由于我们采用的是前后端分离开发的编程方式,前端和后端必定存在跨域的问题。解决跨域问题可以用CORS。
CORS – CROSS ORIGIN RESOURCES SHARING
CORS是W3C的一个标准,全称是跨域资源共享,需要浏览器和服务器同时支持,现在基本上所有浏览器都支持,所有只要我们服务器实现了CORS接口就可以了,在SpringMVC 4.2版本及以上版本只需要在controller上面加上@CrossOrigin注解就可以了。或者,在配置文件中配置也行,比如在网关的yml配置文件中加上
spring:
cloud:
gateway:
enabled: true
globalcors:
cors-configurations:
'[/**]':
# allowedOrigins: * # 这种写法或者下面的都可以,*表示全部
allowedOrigins:
- "*"
allowedMethods:
- GET
- POST
FastDFS
是一个开源的轻量级分布式文件系统,解决了大容量存储和负载均衡的问题。其架构包括Tracker server 和Storage server 。storage server定时向tracker发送状态信息,当客户端client向tracker发送请求,tracker就会搜索可用storage,把它的ip和端口响应给client,client再根据ip和端口向storage上传文件,storage收到了文件之后会生成file_id,然后将上传内容写入磁盘,再返回file_id(即路径信息和文件名),client这端一定要存储文件的信息,这样才能找的到上传的信息。
------使用Docker搭建FastDFS的开发环境 这里省略,主要讲IDEA开发
//引入依赖
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs‐client‐java</artifactId>
<version>1.27.0.0</version>
</dependency>
//在resources文件夹下创建fasfDFS的配置文件fdfs_client.conf
connect_timeout = 60
network_timeout = 60
charset = UTF‐8
http.tracker_http_port = 8080
tracker_server = 192.168.200.128:22122
//在resources文件夹下创建application.yml
spring:
servlet:
multipart:
max‐file‐size: 10MB
max‐request‐size: 10MB
server:
port: 9008
eureka:
client:
service‐url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer‐ip‐address: true
feign:
hystrix:
enabled: true
//导入两个工具类
FastDFSFile
FastDFSClient
//FileController
@RestController
@RequestMapping("/file")
public class FileController {
@PostMapping("/upload")
public Result uploadFile(MultipartFile file){
try{
//判断文件是否存在
if (file == null){
throw new RuntimeException("文件不存在");
}
//获取文件的完整名称
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)){
throw new RuntimeException("文件不存在");
}
//获取文件的扩展名称 abc.jpg jpg
String extName =
originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
//获取文件内容
byte[] content = file.getBytes();
//创建文件上传的封装实体类
FastDFSFile fastDFSFile = new
FastDFSFile(originalFilename,content,extName);
//基于工具类进行文件上传,并接受返回参数 String[]
String[] uploadResult = FastDFSClient.upload(fastDFSFile);
//封装返回结果
String url =
FastDFSClient.getTrackerUrl()+uploadResult[0]+"/"+uploadResult[1];
return new Result(true,StatusCode.OK,"文件上传成功",url);
}catch (Exception e){
e.printStackTrace();
}
return new Result(false, StatusCode.ERROR,"文件上传失败");
}
}
/*
这两天主要是完成商品管理和网站首页高可用
1.snowflake的使用,生成id性能好,能满足高并发情况的id需求,以及能够较好的排序
2.SPU和SKU的概念
① SPU = Standard Product Unit (标准产品单位)
SPU 是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特 性。通俗点讲,属性值、特性相同的货品就可以称为一个 SPU。例如:华为P30 就是一个 SPU
② SKU=stock keeping unit( 库存量单位)
SKU 即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU 是物理上不可分割的最小存货单元。在 使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。例如:华为P30 红色 64G 就是一个 SKU
3.表里面有一个SPU和SKU,为了更好的封装前端发来的数据,以及更加灵活的扩展参数,可以用一个Goods的pojo 类来封装
public class Goods implements Serializable {
private Spu spu;
private List skuList;
}
4.接下来就是商品的增删改查,注意好业务逻辑的实现,其中品牌与分类的关联还要创建一个CategoryBrand的 pojo类,这个表是联合主键,所以templateId和brandId都有@Id注解
@Table(name="tb_category_brand")
public class CategoryBrand implements Serializable {
@Id
private Integer categoryId;
@Id
private Integer brandId;
}
为它创建接口,
public interface CategoryBrandMapper extends Mapper {}
5.商品审核和上下架
这里面其实就是一些逻辑方面的修改,比如审核字段status的"0"和"1",上下架字段IsMarketable 的"0"和"1",删除字段IsDelete的"0"和"1"
*/
/*
商品添加业务代码需要注意的地方
*/
//设置sku名称(spu名称+规格)
String name = spu.getName();
//将规格json转换为map,将map中的value进行名称的拼接
Map<String,String> specMap = JSON.parseObject(sku.getSpec(), Map.class);
if (specMap != null && specMap.size()>0){
for (String value : specMap.values()) {
name+=" "+value;
}
}
关于网站首页的高可用
使用nginx+Lua+redis实现广告缓存
/*
1.了解lua这个脚本语言
2.了解OpenResty
3.了解缓存预热
http://192.168.200.128/ad_update?position=web_index_lb
4.了解二级缓存-加入openresty本地缓存
*/
数据同步解决方案-canal
canal可以用来监控数据库数据的变化,从而获得新增数据,或者修改的数据。
原理相对比较简单:
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)
(1)查看当前mysql是否开启binlog模式。
SHOW VARIABLES LIKE '%log_bin%'
如果log_bin的值为OFF是未开启,为ON是已开启。
(2)修改/etc/my.cnf 需要开启binlog模式。
[mysqld]
log-bin=mysql-bin
binlog-format=ROW
server_id=1
修改完成之后,重启mysqld的服务。
(3) 进入mysql
mysql -h localhost -u root -p
(4)创建账号 用于测试使用
使用root账号创建用户并授予权限
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
(1)下载地址canal
https://github.com/alibaba/canal/releases/tag/canal-1.0.24
(2)下载之后 上传到linux系统中,解压缩到指定的目录/usr/local/canal
解压缩之后的目录结构如下:
(3)修改 exmaple下的实例配置
vi conf/example/instance.properties
修改如图所示的几个参数。
(3)指定读取位置
进入mysql中执行下面语句查看binlog所在位置
如果file中binlog文件不为 mysql-bin.000001 可以重置mysql
mysql> reset master;
查看canal配置文件
vim /usr/local/canal/conf/example/meta.dat
找到对应的binlog信息更改一致即可
"journalName":"mysql-bin.000001","position":120,"
注意:如果不一致,可能导致以下错误
2019-06-17 19:35:20.918 [New I/O server worker #1-2] ERROR c.a.otter.canal.server.netty.handler.SessionHandler - something goes wrong with channel:[id: 0x7f2e9be3, /192.168.200.56:52225 => /192.168.200.128:11111], exception=java.io.IOException: Connection reset by peer
(4)启动服务:
[root@localhost canal]# ./bin/startup.sh
(5)查看日志:
cat /usr/local/canal/logs/canal/canal.log
这样就表示启动成功了。
当用户执行数据库的操作的时候,binlog 日志会被canal捕获到,并解析出数据。我们就可以将解析出来的数据进行相应的逻辑处理。
我们这里使用的一个开源的项目,它实现了springboot与canal的集成。比原生的canal更加优雅。
https://github.com/chenqian56131/spring-boot-starter-canal
使用前需要将starter-canal安装到本地仓库。
我们可以参照它提供的canal-test,进行代码实现。
(1)创建工程模块changgou_canal,pom引入依赖
<dependency>
<groupId>com.xpandgroupId>
<artifactId>starter-canalartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
(2)创建包com.changgou.canal ,包下创建启动类
@SpringBootApplication
@EnableCanalClient //声明当前的服务是canal的客户端
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
(3)添加配置文件application.properties
canal.client.instances.example.host=192.168.200.128
canal.client.instances.example.port=11111
canal.client.instances.example.batchSize=1000
spring.rabbitmq.host=192.168.200.128
(4)创建com.changgou.canal.listener包,包下创建类
@CanalEventListener //声明当前的类是canal的监听类
public class BusinessListener {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
*
* @param eventType 当前操作数据库的类型
* @param rowData 当前操作数据库的数据
*/
@ListenPoint(schema = "changgou_business",table = "tb_ad")
public void adUpdate(CanalEntry.EventType eventType,CanalEntry.RowData rowData){
System.out.println("广告表数据发生改变");
//获取改变之前的数据
rowData.getBeforeColumnsList().forEach((c)-> System.out.println("改变前的数据:"+c.getName()+"::"+c.getValue()));
//获取改变之后的数据
rowData.getAfterColumnsList().forEach((c)-> System.out.println("改变之后的数据:"+c.getName()+"::"+c.getValue()));
}
}
测试:启动数据监控微服务,修改changgou_business的tb_ad表,观察控制台输出。
首页广告缓存更新
需求分析 :当tb_ad(广告)表的数据发生变化时,更新redis中的广告数据。
看图写代码
新增rabbitMQ配置类
@Configuration
public class RabbitMQConfig {
//定义队列名称
public static final String AD_UPDATE_QUEUE="ad_update_queue";
//声明队列
@Bean
public Queue queue(){
return new Queue(AD_UPDATE_QUEUE);
}
}
修改BusinessListener类
@CanalEventListener //声明当前的类是canal的监听类
public class BusinessListener {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
*
* @param eventType 当前操作数据库的类型
* @param rowData 当前操作数据库的数据
*/
@ListenPoint(schema = "changgou_business",table = "tb_ad")
public void adUpdate(CanalEntry.EventType eventType,CanalEntry.RowData rowData){
System.out.println("广告表数据发生改变");
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if ("position".equals(column.getName())){
System.out.println("发送最新的数据到MQ:"+column.getValue());
//发送消息
rabbitTemplate.convertAndSend("", RabbitMQConfig.AD_UPDATE_QUEUE,column.getValue());
}
}
}
}
com.changgou.business包下创建listener包,包下创建类
@Component
public class AdListener {
@RabbitListener(queues = "ad_update_queue")
public void receiveMessage(String message){
System.out.println("接收到的消息为:"+message);
//发起远程调用
OkHttpClient okHttpClient = new OkHttpClient();
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("请求成功:"+response.message());
}
});
}
}
测试,启动eureka和business微服务,观察控制台输出和数据同步效果。
商品上架索引库导入数据
需求分析 :商品上架将商品的sku列表导入或更新索引库
看图写代码
更新rabbitmq配置类
@Configuration
public class RabbitMQConfig {
//定义交换机名称
public static final String GOODS_UP_EXCHANGE="goods_up_exchange";
public static final String GOODS_DOWN_EXCHANGE="goods_down_exchange";
//定义队列名称
public static final String AD_UPDATE_QUEUE="ad_update_queue";
public static final String SEARCH_ADD_QUEUE="search_add_queue";
public static final String SEARCH_DEL_QUEUE="search_del_queue";
//声明队列
@Bean
public Queue queue(){
return new Queue(AD_UPDATE_QUEUE);
}
@Bean(SEARCH_ADD_QUEUE)
public Queue SEARCH_ADD_QUEUE(){
return new Queue(SEARCH_ADD_QUEUE);
}
@Bean(SEARCH_DEL_QUEUE)
public Queue SEARCH_DEL_QUEUE(){
return new Queue(SEARCH_DEL_QUEUE);
}
//声明交换机
@Bean(GOODS_UP_EXCHANGE)
public Exchange GOODS_UP_EXCHANGE(){
return ExchangeBuilder.fanoutExchange(GOODS_UP_EXCHANGE).durable(true).build();
}
@Bean(GOODS_DOWN_EXCHANGE)
public Exchange GOODS_DOWN_EXCHANGE(){
return ExchangeBuilder.fanoutExchange(GOODS_DOWN_EXCHANGE).durable(true).build();
}
//队列与交换机的绑定
@Bean
public Binding GOODS_UP_EXCHANGE_BINDING(@Qualifier(SEARCH_ADD_QUEUE)Queue queue,@Qualifier(GOODS_UP_EXCHANGE)Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("").noargs();
}
@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();
}
}
数据监控微服务新增SpuListener,添加以下代码:
@CanalEventListener
public class SpuListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@ListenPoint(schema = "changgou_goods",table = "tb_spu")
public void goodsUp(CanalEntry.EventType eventType,CanalEntry.RowData rowData){
//获取改变之前的数据并将这部分数据转换为map
Map<String,String> oldData=new HashMap<>();
rowData.getBeforeColumnsList().forEach((c)->oldData.put(c.getName(),c.getValue()));
//获取改变之后的数据并这部分数据转换为map
Map<String,String> newData = new HashMap<>();
rowData.getAfterColumnsList().forEach((c)->newData.put(c.getName(),c.getValue()));
//获取最新上架的商品 0->1
if ("0".equals(oldData.get("is_marketable")) && "1".equals(newData.get("is_marketable"))){
//将商品的spuid发送到mq
rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_UP_EXCHANGE,"",newData.get("id"));
}
//获取最新下架的商品 1->0
if ("1".equals(oldMap.get("is_marketable")) && "0".equals(newMap.get("is_marketable"))){
//将商品的spuid发送到mq
rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_DOWN_EXCHANGE,"",newMap.get("id"));
}
}
}
创建search模块,包括api和service两块
api中有pojo类
@Document(indexName = "skuinfo", type = "docs")
public class SkuInfo implements Serializable {
//商品id,同时也是商品编号
@Id
@Field(index = true, store = true, type = FieldType.Keyword)
private Long id;
//SKU名称
@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;
//getter & setter略
}
service模块的yml
server:
port: 9009
spring:
application:
name: search
rabbitmq:
host: 192.168.200.128
redis:
host: 192.168.200.128
main:
allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
data:
elasticsearch:
cluster-name: elasticsearch
cluster-nodes: 192.168.200.128:9300
thymeleaf:
cache: false
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
client:
config:
default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
connectTimeout: 600000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
readTimeout: 600000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: false
isolation:
strategy: SEMAPHORE
创建引导类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.changgou.goods.feign"})
public class SearchApplication {
public static void main(String[] args) {
SpringApplication.run(SearchApplication.class,args);
}
}
将rabbitmq配置类放入该模块下
@GetMapping("/spu/{spuId}")
public List<Sku> findSkuListBySpuId(@PathVariable("spuId") String spuId){
Map<String,Object> searchMap = new HashMap<>();
if (!"all".equals(spuId)){
searchMap.put("spuId",spuId);
}
searchMap.put("status","1");
List<Sku> skuList = skuService.findList(searchMap);
return skuList;
}
在goods_api中新增feign包与feign接口
@FeignClient(name="goods")
@RequestMapping("/sku")
public interface SkuFeign {
/***
* 多条件搜索品牌数据
* @param spuId
* @return
*/
@GetMapping("/sku/spu/{spuId}")
public List<Sku> findSkuListBySpuId(@PathVariable("spuId") String spuId);
}
创建 com.changgou.search.dao包,并新增ESManagerMapper接口
public interface ESManagerMapper extends ElasticsearchRepository<SkuInfo,Long> {
}
创建 com.changgou.search.service包,包下创建接口EsManagerService
public interface SkuSearchService {
/**
* 创建索引库结构
*/
public void createIndexAndMapping();
/**
* 导入全部数据到ES索引库
*/
public void importAll();
/**
* 根据spuid导入数据到ES索引库
* @param spuId 商品id
*/
public void importDataToESBySpuId(String spuId);
}
创建com.changgou.search.service.impl包,包下创建服务实现类
@Service
public class ESManagerServiceImpl implements ESManagerService {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
@Autowired
private SkuFeign skuFeign;
@Autowired
private ESManagerMapper esManagerMapper;
//创建索引库结构
@Override
public void createMappingAndIndex() {
//创建索引
elasticsearchTemplate.createIndex(SkuInfo.class);
//创建映射
elasticsearchTemplate.putMapping(SkuInfo.class);
}
//导入全部sku集合进入到索引库
@Override
public void importAll() {
//查询sku集合
List<Sku> skuList = skuFeign.findSkuListBySpuId("all");
if (skuList == null || skuList.size()<=0){
throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
}
//skulist转换为json
String jsonSkuList = JSON.toJSONString(skuList);
//将json转换为skuinfo
List<SkuInfo> skuInfoList = JSON.parseArray(jsonSkuList, SkuInfo.class);
for (SkuInfo skuInfo : skuInfoList) {
//将规格信息转换为map
Map specMap = JSON.parseObject(skuInfo.getSpec(), Map.class);
skuInfo.setSpecMap(specMap);
}
//导入索引库
esManagerMapper.saveAll(skuInfoList);
}
//根据spuid查询skuList,添加到索引库
@Override
public void importDataBySpuId(String spuId) {
List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
if (skuList == null || skuList.size()<=0){
throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
}
//将集合转换为json
String jsonSkuList = JSON.toJSONString(skuList);
List<SkuInfo> skuInfoList = JSON.parseArray(jsonSkuList, SkuInfo.class);
for (SkuInfo skuInfo : skuInfoList) {
//将规格信息进行转换
Map specMap = JSON.parseObject(skuInfo.getSpec(), Map.class);
skuInfo.setSpecMap(specMap);
}
//添加索引库
esManagerMapper.saveAll(skuInfoList);
}
}
创建com.changgou.search.controller.定义ESManagerController
@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,"导入全部数据成功");
}
}
在search工程创建com.changgou.search.listener包,包下创建类
@Component
public class GoodsUpListener {
@Autowired
private ESManagerService esManagerService;
@RabbitListener(queues = RabbitMQConfig.SEARCH_ADD_QUEUE)
public void receiveMessage(String spuId){
System.out.println("接收到的消息为: "+spuId);
//查询skulist,并导入到索引库
esManagerService.importDataBySpuId(spuId);
}
}
然后测试
商品下架索引库删除数据
需求分析 : 商品下架后将商品从索引库中移除。
看图写代码
这里只把业务代码贴一下,即ESManagerServiceImpl实现方法
@Override
public void delDataBySpuId(String spuId) {
List<Sku> skuList = skuFeign.findSkuListBySpuId(spuId);
if (skuList == null || skuList.size()<=0){
throw new RuntimeException("当前没有数据被查询到,无法导入索引库");
}
for (Sku sku : skuList) {
esManagerMapper.deleteById(Long.parseLong(sku.getId()));
}
}
商品搜索
/*
本章中主要完成商品的搜索功能,主要涉及:
1.根据搜索关键字查询
2.条件查询
3.规格过滤
4.价格区间搜索
5.分页查询
6.排序查询
7.高亮查询
那么关键点就是如何用ElasticsearchTemplate去完成这些搜索查询
主要有以下步骤:
1.造一个原生搜索实现类,可以理解为sql语句构造器,用它来造(build)sql---个人理解
NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder();
2.组合条件对象,自己想用什么条件查询可以封装进去,交给 原生搜索实现类 去造相应的sql
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
3.所以关键就是组合条件对象怎么封装条件
--(1):根据name模糊查询,用must+matchQuery,matchQuery代表模糊查询
boolQuery.must 这个是 针对 需要分词的 来进行 过滤查询匹配
----------------------------------------------------------------------------------- boolQuery.must(QueryBuilders.matchQuery("name",searchMap.get("keywords")).operator(Operator.AND));//后面跟这个operator(Operator.AND)代表着多条件查询
-----------------------------------------------------------------------------------
--(2):根据品牌过滤查询,用filter+termQuery,filter代表过滤,termQuery代表精确查询
boolQuery.filter 针对不需要分词的,例如品牌 就使用 filter
-----------------------------------------------------------------------------------
boolQuery.filter(QueryBuilders.termQuery("brandName",searchMap.get("brand")));
-----------------------------------------------------------------------------------
--(3):根据价格进行价格区间过滤查询,用filter+rangeQuery
-----------------------------------------------------------------------------------
String[] prices = searchMap.get("price").split("-");
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(prices[1]));
boolQuery.filter(QueryBuilders.rangeQuery("price").gte(prices[0]));
-----------------------------------------------------------------------------------
// 不管根据多少条件,用boolQuery封装完,都要加到构建器里面去
nativeSearchQueryBuilder.withQuery(boolQuery);
// 这时候就像写完了select ··from ·· where ··=·· and ·· like ··等等
// 还没有涉及聚合查询,聚合查询不能用boolQuery了,得用构建器自己了
如
--(4):按照品牌进行分组(聚合)查询
----------------------------------------------------------------------------------
String skuBrand="skuBrand"; nativeSearchQueryBuilder.addAggregation(AggregationBuilders.terms(skuBrand).field("brandName"));//这里面的terms(xxx)里面填的是聚合查询出的那列的列名,自己起的,field(xxx)里面就是根据哪个域来进行聚合查询
----------------------------------------------------------------------------------
--(5):设置分页
----------------------------------------------------------------------------------
nativeSearchQueryBuilder.withPageable(PageRequest.of(pageNum-1,pageSize))
----------------------------------------------------------------------------------
--(6):按照相关字段进行排序查询
---------------------------------------------------------------------------------- nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).
order(SortOrder.ASC));//升序
nativeSearchQueryBuilder.withSort(SortBuilders.fieldSort((searchMap.get("sortField"))).
order(SortOrder.DESC));//降序
----------------------------------------------------------------------------------
--(7):设置高亮
----------------------------------------------------------------------------------
HighlightBuilder.Field field = new HighlightBuilder.Field("name")//高亮域
.preTags("")//高亮样式的前缀
.postTags("");//高亮样式的后缀
nativeSearchQueryBuilder.withHighlightFields(field);
----------------------------------------------------------------------------------
--(8):开启查询,用elasticsearchTemplate.queryForPage(三个参数);
第一个参数: 条件构建对象
第二个参数: 查询操作实体类
第三个参数: 查询结果操作对象
*/
AggregatedPage<SkuInfo> resultInfo = elasticsearchTemplate.queryForPage(nativeSearchQueryBuilder.build(), SkuInfo.class, new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
//查询结果操作
List<T> list = new ArrayList<>();
//获取查询命中结果数据
SearchHits hits = searchResponse.getHits();
if (hits != null){
//有查询结果
for (SearchHit hit : hits) {
//SearchHit转换为skuinfo
SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (highlightFields != null && highlightFields.size()>0){
//替换数据
skuInfo.setName(highlightFields.get("name").getFragments()[0].toString());
}
list.add((T) skuInfo);
}
}
return new AggregatedPageImpl<T>(list,pageable,hits.getTotalHits(),searchResponse.getAggregations());
}
});
//封装最终的返回的结果
//总记录数
resultMap.put("total",resultInfo.getTotalElements());
//总页数
resultMap.put("totalPages",resultInfo.getTotalPages());
//数据集合
resultMap.put("rows",resultInfo.getContent());
//封装品牌的分组结果
StringTerms brandTerms = (StringTerms) resultInfo.getAggregation(skuBrand);
List<String> brandList = brandTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
resultMap.put("brandList",brandList);
//封装规格分组结果
StringTerms specTerms= (StringTerms) resultInfo.getAggregation(skuSpec);
List<String> specList = specTerms.getBuckets().stream().map(bucket -> bucket.getKeyAsString()).collect(Collectors.toList());
resultMap.put("specList",specList);
//当前页
resultMap.put("pageNum",pageNum);
return resultMap;
遇到的主要问题就是url传参问题,不知道为什么要转来转去
controller里面转了,注意这里,这个replace中谁是被转换的
说明前面一个是被转的,那么controller里面被转的就是“+”,说明传过来的就是“+”
后面service里面又转回来
说实话这里不是蛮明白
可以看看这个博文:https://www.cnblogs.com/chris-oil/p/7923296.html
名词解释
单点登录:简称为sso,sso的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统----可以实现单点登录技术有很多,比如说下面:
1、Apache Shiro.
2、CAS
3、Spring security
//谈谈自己对于本项目的用户认证的流程的理解
/*
畅购的用户认证服务单独为一个微服务--叫user-oauth。一个用户如果想访问资源服务,首先需要经过网关,网关中有一个AuthFilter,用来验证请求中的cookie中是否带有jti,并且通过jti能否在redis中查到token令牌,如果都能,说明之前已经通过了认证服务,先不用关心其正确性,资源服务自己会去验证,该过滤器接下来给这个请求的头增强,带上Authorization,就放行。
如果在AuthFilter中发现cookie中没有jti或者redis中没有token,直接跳转至自定义登录页面,输入用户账号密码之后,请求会到user-oauth服务,该服务中的config中有四个配置文件,需要我们修改的只有UserDetailsServiceImpl和WebSecurityConfig,其中我们客户端发到这个服务的请求经过controller再到业务层,业务层通过RestTemplate请求Spring Security所暴露的申请令牌接口来申请令牌,这个申请请求会被Spring Security接收到,然后请求会调用UserDetailsService接口的实现类的loadUserByUsername方法,里面会先判断是否身份被验证过,如果没有,那就进行httpbasic认证,认证通过后再进入该UserDetailsService的实现类,通过feign调用user服务查询用户正确的密码,然后如果都正确就可以得到令牌,之后accessToken存入redis,jti存入cookie返回客户端,客户端携带cookie访问资源服务
*/
可以结合着下图看
//本块主要是完成购物车的流程和购物车的渲染,以及微服务间的认证访问
/*
购物车流程从前端的渲染讲起:
请求发到网关,网关转到订单渲染服务(包括购物车),html发送请求到渲染服务controller。 controller调用订单微服务中的cart那条线,先是cart的controller,收到请求后转到业务层,如果是add请 求,业务层将传过来的skuId和商品数量num存入redis;如果是list请求,从redis中查出购物车中的 orderItemList存入map,同时将totalMoney和totalNum封入map返回请求端,请求端将返回的数据进行页面渲 染;
因为涉及到服务之间的调用,所有访问保护起来的资源需要携带令牌,所以在common服务中放了一个 FeignInterceptor,注意的是,谁要调用被保护起来的资源服务,就在谁的启动类注入一个FeignInterceptor 的Bean
*/
//完成订单结算页渲染 完成用户下单实现 完成库存变更实现
/*
用户从购物车页面点击结算跳转到订单结算页,需要发送请求得到用户对应的地址,请求通过网关到订单渲染微服务 的订单controller,用feign调用用户的查询用户地址服务,不仅如此还要用feign调用cartFeign查询出购物车 的信息,将地址list,商品list,totalMoney,totalNum以及默认收件人信息一起封入model中返回前端订单 结算页渲染页面。
下单的时候要有如下几个动作,往订单表,订单明细表插入数据,还要减少库存。
添加订单业务层要完善订单数据,添加订单明细(orderItem)并存入数据库,通过feign调用商品微服务减少库 存,清除redis中购物车信息
*/
//完成通过seata实现分布式事务,完成通过消息队列实现分布式事务
//创建fescar的模块,导入fescar需要的依赖
<properties>
<fescar.version>0.4.2</fescar.version>
</properties>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou_common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-tm</artifactId>
<version>${fescar.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fescar</groupId>
<artifactId>fescar-spring</artifactId>
<version>${fescar.version}</version>
</dependency>
</dependencies>
将fescar配置文件
文件夹中的所有配置文件拷贝到resources工程下,如下图:
其中file.conf有2个配置:
service.vgroup_mapping.my_test_tx_group 映射到相应的 Fescar-Server 集群名称,然后再根据集群名称.grouplist 获取到可用服务列表。
导入一个filter,一个interceptor,一个config
FescarRMRequestFilter,是给每个线程绑定一个XID。
FescarRestInterceptor过滤器,是为了每次请求其他微服务的时候,都将XID携带过去。
涉及到分布式事务的数据库添加表undo_log,核心在于对业务sql进行解析,转换成undolog,所以只要支持Fescar分布式事务的微服务数据都需要导入该表结构
需要添加分布式事务的微服务(商品微服务、订单微服务)添加对 changgou_transaction_fescar的依赖
在订单微服务的OrderServiceImpl的add方法上增加@GlobalTransactional(name = "order_add")注解
启动Fescar-server,打开seata包/fescar-server-0.4.2/bin,双击fescar-server.bat启动fescar-server
基于消息队列实现分布式事务
参照下图:
/*
涉及到任务表,历史任务表,积分日志表
1.在订单业务层的add的方法下添加局部代码,完成添加任务表记录
2.添加一个定时任务类,定时启动
3.用户微服务修改积分,在该服务中添加监听类,监听类中
判断redis中有记录,如果有直接返回
如果没有,调用业务层方法更新积分
向mq中返回通知消息
4.在业务层添加积分,需要
先判断在积分日志表是否有该订单的记录,如果有,直接返回
如果没有,先存入redis,
然后调用mapper修改积分,
之后添加积分日志表记录,
最后删除redis中的记录
在上面存入redis的时候注意:
redisTemplate.boundValueOps(task.getId()).set("exist",1,TimeUnit.MINUTES);
是根据task的id作为key存的,还要设置过期时间,这个id代码中没有主动设置,应该是数据库自增加上的
5.在订单业务层定义一个监听类,监听用户微服务那边发到mq中的完成添加积分成功的消息
调用接口方法删除任务表的记录
完成业务层删除任务表记录的方法,该方法中,有一些需要注意
@Override
@Transactional
public void delTask(Task task) {
//1.记录删除时间
task.setDeleteTime(new Date());
Long taskId = task.getId();
task.setId(null);//这里设为null是因为taskHis中的id是自增的
//bean拷贝
TaskHis taskHis = new TaskHis();
BeanUtils.copyProperties(task,taskHis);
//记录历史任务数据
taskHisMapper.insertSelective(taskHis);
//删除原有任务数据
task.setId(taskId);
taskMapper.deleteByPrimaryKey(task);
System.out.println("订单服务完成了添加历史任务并删除原有任务的操作");
}
*/
/*
理一理从我的购物车点击结算--到订单结算页点击提交订单---到选择支付方式选微信支付--到微信扫码支付--到支付成功页面
1.购物车点击结算后请求到订单渲染微服务controller的toOrder方法
结算
|
\|/
@GetMapping("/ready/order")
public String toOrder(Model model)
该方法携带着地址list,orderItemList以及totalMoney,totalNum以及默认收件人信息一起封入model中返 回前端订单结算页渲染页面
2.订单结算页面点击提交订单,发送请求
axios.post('/api/worder/add',this.order),
订单渲染服务的controller会调用订单服务的添加订单方法完成订单添加,添加成功后回到订单结算页会跳转至支 付页:
location.href = "/api/worder/toPayPage?orderId=" + orderId;
3.到了支付页之后,当选择微信支付之后,发送请求:
到了订单渲染服务的PayController了,然后调用支付微服务的payFeign的nativePay方法,向微信发下单请 求,返回的结果包括二维码字符串,跳转到微信支付页
4.跳转到微信支付页之后,如果支付成功,回调地址(即http: 127.0.0.1:9010//wxpay/notify)会收到回 调结果,这个地址是支付微服务的WxPayController的notifyLogic方法,里面会判断基于微信查询订单信息的返 回结果,如果查询结果是成功,发送订单消息到MQ,订单服务会收到消息,然后修改订单状态,如下图:
*/
/*
5.在上一步发送订单消息到MQ之后,紧接着完成双向通信,即借助于 RabbitMQ 的 Web STOMP 插件,实现浏览 器与服务端的全双工通信。说白了也是向MQ发送一条消息,然后浏览器微信支付页是能收到消息的,可以直接判断 orderId和MQ中的orderId是否一致,如果一致,发送请求:
location.href="/api/wxpay/toPaySuccess?payMoney="+[[${payMoney}]]
那么PayController的toPaySuccess方法会携带model转到支付成功页面
需要注意的是,只有回调方法接受到了成功的微信订单返回消息才会向MQ发送修改订单状态的消息,如果有超时未支 付的订单,我们就要处理这些订单,先调用微信支付api,查询该订单的支付状态。如果未支付调用关闭订单的api, 并修改订单状态为已关闭,并回滚库存数。如果该订单已经支付,则做补偿操作。具体来说,
1.在添加订单的微服务的业务层中,最后要向MQ发送一条消息
rabbitTemplate.convertAndSend( "","queue.ordercreate", id);
当该消息过期后,会被该微服务的监听器收到消息(延迟队列效果),收到消息后,会调用业务层关闭订单的 closeOrder方法判断该订单是否该关闭,如果该关闭,就通过feign调用pay微服务中的微信支付工具类关闭订 单。
*/
/*
秒杀商品由B端存入Mysql,设置定时任务,每隔一段时间就从Mysql中将符合条件的数据从Mysql中查询出来并存入缓存中,redis以Hash类型进行数据存储。
分两步:
1.秒杀渲染服务的前端页面先发送获取时间段数据的请求获取到时间段,然后返回数据后在发送请求查询该时间段的商品list,然后页面就渲染就可以了
2.立即抢购下单服务,发送请求到秒杀渲染服务的订单controller,调用秒杀微服务的添加订单方法,业务层有很 多细节,一是要防止恶意刷单,二是防止相同商品重复购买,然后在redis中实行库存预扣减(注意这里的库存预扣 减需要之前查询秒杀商品业务层方法中把库存添加进redis,不然后面预扣减无法扣减),再基于mq完成mysql的数 据同步,进行异步下单并扣减库存(mysql),即定义一个consume微服务,在里面的业务层扣减秒杀商品的库存
需要注意的是,秒杀下单接口隐藏和秒杀下单接口限流的处理方法
1.隐藏的处理方法是在在用户每一次点击抢购的时候,都首先去生成一个随机数并存入redis,接着用户携带着这个 随机数去访问秒杀下单,下单接口首先会从redis中获取该随机数进行匹配,如果匹配成功,则进行后续下单操 作,如果匹配不成功,则认定为非法访问。
2.接口限流的处理方法是用google提供的guava工具包中的RateLimiter进行实现,其内部是基于令牌桶算法进行 限流计算,涉及到自定义注解,可以研究研究
*/
基于guava实现限流
//先导入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
//自定义限流注解
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {}
//自定义切面类
@Component
@Scope
@Aspect
public class AccessLimitAop {
@Autowired
private HttpServletResponse response;
//设置令牌的生成速率
private RateLimiter rateLimiter = RateLimiter.create(2.0); //每秒生成两个令牌存入桶中
@Pointcut("@annotation(com.changgou.seckill.web.aspect.AccessLimit)")
public void limit(){}
@Around("limit()")
public Object around(ProceedingJoinPoint proceedingJoinPoint){
boolean flag = rateLimiter.tryAcquire();
Object obj = null; //返回值
if (flag){
//允许访问
try {
obj = proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}else{
//不允许访问,拒绝
String errorMessage = JSON.toJSONString(new Result<>(false, StatusCode.ACCESSERROR,"fail"));
//将信息返回到客户端上
this.outMessage(response,errorMessage);
}
return obj;
}
private void outMessage(HttpServletResponse response,String errorMessage){
ServletOutputStream outputStream = null;
try {
response.setContentType("application/json;charset=utf-8");
outputStream = response.getOutputStream();
outputStream.write(errorMessage.getBytes("utf-8"));
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}