配置文件 @RefreshScope
实现配置自动更新
项目中的使用:每个微服务创建自己的命名空间,使用配置分组区分环境,dev、test、pro、
application.properties
对应命名空间下,所属分组、应用名.properties。
图片表示application.properties
。对应nacos中coupon
命名空间,prod
分组,Data Id名称为应用名gulimall-coupon.properties
文件。yml同理
配置集 同时加载多个配置集
注释配置文件
使用配置集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 spring.cloud.nacos.config.namespace =29176167-82d9-46b0-9a00-687acca63fd4 spring.cloud.nacos.config.ext-config[0].data-id =datasource.yml spring.cloud.nacos.config.ext-config[0].group =dev spring.cloud.nacos.config.ext-config[0].refresh =true spring.cloud.nacos.config.ext-config[1].data-id =mybatis.yml spring.cloud.nacos.config.ext-config[1].group =dev spring.cloud.nacos.config.ext-config[1].refresh =true spring.cloud.nacos.config.ext-config[2].data-id =other.yml spring.cloud.nacos.config.ext-config[2].group =dev spring.cloud.nacos.config.ext-config[2].refresh =true
此时访问localhost:7000/coupon/coupon/test
值是们项目中配置的值,并没有使用到相对应的配置文件
这是因为
No active profile set, falling back to default profiles: default
我们没有设置使用配置文件的分组,默认使用的是DEFAULT_GROUP
分组。当我们的配置集中没有程序中使用的配置则会使用默认分组的配置文件,但是coupon命名空间下没有这个默认分组的配置文件,则使用原程序的配置application.properties
当取消注释设置使用prod
分组后
Gatewa网关
引入common
模块中引入了跟数据库有关的相关依赖,而网关项目又没配置数据库信息,从而启动报错,主启动程序排除解决报错。
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableDiscoveryClient public class GulimallGatewayApplication { public static void main (String[] args) { SpringApplication.run(GulimallGatewayApplication.class, args); } }
前端发送请求到网关存在跨域问题
协议名、域名、端口都要相同,其中一个不同都会产生跨越问题。
网关统一解决跨域问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class GuliMallCorsConfiguration { @Bean public CorsWebFilter corsWebFilter () { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedHeader("*" ); corsConfiguration.addAllowedMethod("*" ); corsConfiguration.addAllowedOrigin("*" ); corsConfiguration.setAllowCredentials(true ); source.registerCorsConfiguration("/**" , corsConfiguration); return new CorsWebFilter(source); } }
只希望有一个解决跨域,不希望有多个。将另一个注释,这里是renren-fast
解决了。(网关解决跨域,人人就不要解决了,否则重复!没配网关的时候不会出现跨域问题,就是人人后台解决了)
人人快速开发
product/category
转换成路由为product-category
根据规则页面则应该创建在views
目录下product
包下的category.vue
文件中。
前端项目发送请求到网关中
网关重写路径RewritePath
https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#global-filters
uri: lb://renren-fast
: lb负载均衡://应用名
返回的数据Object中data
属性中的data
才是我们想要的。所用我们可以使用ES6的解构赋值语法。
只拿出返回数据的data
属性值,data.data
中则包含我们所要的数据。
前端Vue
框中内容回显,应该使用请求查询当前数据库,得到数据再回显到页面上。避免当多人在页面上操作时一个人修改了,会导致当前数据滞后。
以上图片ES6新语法。
拖拽修改菜单功能 draggable
开启拖拽功能
allow-drop
拖拽时判定目标节点能否被放置`Function(draggingNode, dropNode, type)``
``type` 参数有三种情况:’prev’、’inner’ 和 ‘next’,分别表示放置在目标节点前、插入至目标节点和放置在目标节点后
node-drop
拖拽成功完成时触发的事件。共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event
批量删除
使用组件自带的方法ref="xxx"
this.$refs.menuTree.getCheckedNodes();
调用树形组件的getCheckedNodes 方法
OOS云存储 阿里云OSS云存储首先创建一个Bucket
。
Bucket
公共读
代表读不需要账户密码,写操作则需要。
可以点击上传文件进行上传操作,通过url访问。
SDK https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.923.4c6513c03fK1WG
首先安装SDK
1 2 3 4 5 <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > 3.10.2</version > </dependency >
上传文件 简单上传文件
Endpoint
地域节点,在Bucekt页面概览中可以找到。
在测试中将上面的代码进行测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 String endpoint = "yourEndpoint" ; String accessKeyId = "yourAccessKeyId" ; String accessKeySecret = "yourAccessKeySecret" ; OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket" , "exampledir/exampleobject.txt" , new File("D:\\localpath\\examplefile.txt" )); ossClient.putObject(putObjectRequest); ossClient.shutdown();
上传成功后,就能在我们给定的路径看到我们上传的文件。
Alibaba Cloud OSS https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md
https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample
1 2 3 4 5 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > aliyun-oss-spring-boot-starter</artifactId > <version > 1.0.0</version > </dependency >
将我们之前导入的SDK注释掉。这里要指定一下版本,可以区阿里云的Maven仓库查看依赖https://maven.aliyun.com/mvn/search
坑
配置文件配置OSS
1 2 3 4 5 6 7 alibaba: cloud: access-key: secret-key: oss: endpoint:
可行 1 2 3 4 <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alicloud-oss</artifactId > </dependency >
1 2 3 4 5 6 7 spring: cloud: alicloud: access-key: xxxxxxxxxxxxxx secret-key: xxxxxxxxxxxxxxxxxxxxxx oss: endpoint: oss-cn-shenzhen.aliyuncs.com
我们可以通过OSSClient
对象进行相关操作,省略了创建OSSClient的步骤。
1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootTest class GulimallProductApplicationTests { @Autowired(required = false) OSSClient ossClient; @Test public void testOSSUpload () { ossClient.putObject("mall-kylin" , "exampledir/ossClient.txt" , new File("C:\\Users\\kylin\\Desktop\\OSS.txt" )); ossClient.shutdown(); } }
目前尚未知晓原因。
服务端签名后直传 https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1749.5a3c43d33sLRZe
选择代码示例Java。
应用服务器核心代码解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 protected void doGet (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String accessId = "<yourAccessKeyId>" ; String accessKey = "<yourAccessKeySecret>" ; String endpoint = "oss-cn-hangzhou.aliyuncs.com" ; String bucket = "bucket-name" ; String host = "https://" + bucket + "." + endpoint; String callbackUrl = "http://88.88.88.88:8888" ; String dir = "user-dir-prefix/" ; OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey); try { long expireTime = 30 ; long expireEndTime = System.currentTimeMillis() + expireTime * 1000 ; Date expiration = new Date(expireEndTime); PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0 , 1048576000 ); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte [] binaryData = postPolicy.getBytes("utf-8" ); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); Map<String, String> respMap = new LinkedHashMap<String, String>(); respMap.put("accessid" , accessId); respMap.put("policy" , encodedPolicy); respMap.put("signature" , postSignature); respMap.put("dir" , dir); respMap.put("host" , host); respMap.put("expire" , String.valueOf(expireEndTime / 1000 )); JSONObject jasonCallback = new JSONObject(); jasonCallback.put("callbackUrl" , callbackUrl); jasonCallback.put("callbackBody" , "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}" ); jasonCallback.put("callbackBodyType" , "application/x-www-form-urlencoded" ); String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes()); respMap.put("callback" , base64CallbackBody); JSONObject ja1 = JSONObject.fromObject(respMap); response.setHeader("Access-Control-Allow-Origin" , "*" ); response.setHeader("Access-Control-Allow-Methods" , "GET, POST" ); response(request, response, ja1.toString()); } catch (Exception e) { System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } }
这一段代码是返回Json解决跨域,跨域我们前面解决了。Json则使用的是@RestController
也不用了。所以删掉这段代码直接返回respMap。
由于导入了spring-cloud-starter-alicloud-oss
所以创建OssClient步骤可以省略,文件前缀按日期格式来划分。回调服务器的URL暂时用不到注释掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @RestController public class OssController { @Resource OSSClient ossClient; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @GetMapping("/oss/policy") public Map<String, String> policy () { String host = "https://" + bucket + "." + endpoint; String format = new SimpleDateFormat("yyyy-MM-dd" ).format(new Date()); String dir = format + "/" ; try { long expireTime = 30 ; long expireEndTime = System.currentTimeMillis() + expireTime * 1000 ; Date expiration = new Date(expireEndTime); PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0 , 1048576000 ); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte [] binaryData = postPolicy.getBytes("utf-8" ); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); Map<String, String> respMap = new LinkedHashMap<String, String>(); respMap.put("accessid" , accessId); respMap.put("policy" , encodedPolicy); respMap.put("signature" , postSignature); respMap.put("dir" , dir); respMap.put("host" , host); respMap.put("expire" , String.valueOf(expireEndTime / 1000 )); JSONObject jasonCallback = new JSONObject(); jasonCallback.put("callbackBody" , "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}" ); jasonCallback.put("callbackBodyType" , "application/x-www-form-urlencoded" ); String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes()); respMap.put("callback" , base64CallbackBody); return respMap; } catch (Exception e) { System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return null ; } }
同时配置网关
http://localhost:88/api/thirdparty/oss/policy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 spring: cloud: gateway: routes: - id: test_route uri: https://www.baidu.com predicates: - Query=url,baidu - id: qq_route uri: https://www.qq.com predicates: - Query=url,qq - id: product_route uri: lb://gulimall-product predicates: - Path=/api/product/** filters: - RewritePath=/api/(?<segment>.*),/$\{segment} - id: third_party_route uri: lb://gulimall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment} - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
前端Upload组件 拿到外网访问的Bucket域名
修改成action
的地址
向阿里云OSS地发送请求的时候,出现了跨域问题。
配置跨域设置。
此时就解决了跨域问题。
表格自定义显示logo图片https://element.eleme.io/#/zh-CN/component/table
表单校验
https://element.eleme.io/#/zh-CN/component/form
Form 组件提供了表单验证的功能,只需要通过 rules
属性传入约定的验证规则,并将 Form-Item 的 prop
属性设置为需校验的字段名即可。校验规则参见 async-validator
JSR303校验
javax.validation.constraints
包下注解进行校验。
控制器参数使用@Valid
注解标注,迭代注解@Validated
,获取校验结果BindingResult
统一异常处理 使用@ControllerAdvice
注解这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能:
全局异常处理
全局数据绑定
全局数据预处理
@ExceptionHandler
异常处理方法,处理指定的异常类型。
@RestControllerAdvice
=@RequestBody
+@ControllerAdvice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Slf4j @RestControllerAdvice(basePackages = "com.kylin.gulimall.product.controller") public class MallExceptionControllerAdvice { @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handlerValidException (MethodArgumentNotValidException e) { log.error("数据校验出现问题{},异常类型{}" , e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> map = new HashMap<>(16 ); bindingResult.getFieldErrors().forEach(fieldError -> { map.put(fieldError.getField(), fieldError.getDefaultMessage()); }); return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data" , map); } @ExceptionHandler(value = Throwable.class) public R handleException (Throwable throwable) { return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(), BizCodeEnum.UNKNOWN_EXCEPTION.getMsg()); } }
定义错误代码枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public enum BizCodeEnum { UNKNOWN_EXCEPTION(10000 , "系统未知异常" ), VALID_EXCEPTION(10001 , "参数格式校验失败" ), SMS_CODE_EXCEPTION(10002 , "验证码获取频率太高,稍后再试" ), TO_MANY_REQUEST(10003 , "请求流量过大" ), SMS_SEND_CODE_EXCEPTION(10403 , "短信发送失败" ), USER_EXIST_EXCEPTION(15001 , "用户已经存在" ), PHONE_EXIST_EXCEPTION(15002 , "手机号已经存在" ), LOGINACTT_PASSWORD_ERROR(15003 , "账号或密码错误" ), SOCIALUSER_LOGIN_ERROR(15004 , "社交账号登录失败" ), NOT_STOCK_EXCEPTION(21000 , "商品库存不足" ), PRODUCT_UP_EXCEPTION(11000 , "商品上架异常" ); private int code; private String msg; BizCodeEnum(int code, String msg) { this .code = code; this .msg = msg; } public int getCode () { return code; } public String getMsg () { return msg; } }
分组校验
首先定义两个分组
校验注解中的group
属性指定所属分组
参数中使用@Validated
注解给value赋值,表明该参数所属分组。
group
属性没指定分组,个人感觉所属空分组。每个参数都指定不同分组的话,非常冗余。
自定义校验注解
可以参考别人写好的注解
直接将其复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {}) public @interface ListValue { String message () default " {com.kylin.common.valid.ListValue.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int[] value() default {}; }
导入依赖
1 2 3 4 5 <dependency > <groupId > jakarta.validation</groupId > <artifactId > jakarta.validation-api</artifactId > <version > 2.0.1</version > </dependency >
自定义注解的默认message消息会从ValidationMessages.properties
中读取message名称属性的值。也就是这里配置的com.kylin.common.valid.ListValue.message
接着编写自定义校验器实现ConstraintValidator
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /** * 自定义参数校验器 * * @author kylin */ public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> { private Set<Integer> set = new HashSet<>(); /** * 初始化方法 * * @param constraintAnnotation 注解 */ @Override public void initialize(ListValue constraintAnnotation) { int[] value = constraintAnnotation.value(); for (int i : value) { set.add(i); } } /** * 判断是否校验成功 * * @param value 需要校验的值 * @param context * @return */ @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
在自定义校验注解中@Constraint(validatedBy = {})
指定校验器。@Constraint(validatedBy = {ListValueConstraintValidator.class})
Elasticsearch 安装 docker安装elasticsearch
docker pull elasticsearch:7.4.2
创建目录,挂载文件。授权
配置elasticsearch,允许所有的ip访问
docker允许命令 容器名为elasticsearch
开发9200、9300端口。当前以当节点允许,配置Java内存,-v挂载,-d运行images
1 2 3 4 5 6 7 8 9 10 11 12 13 mkdir -p /mydata/elasticsearch/config mkdir -p /mydata/elasticsearch/data chmod -R 777 /mydata/elasticsearch/ echo "http.host: 0.0.0.0" >>/mydata/elasticsearch/config/elasticsearch.ymldocker run --name elasticsearch -p 9200:9200 -p 9300:9300 --privileged=true \ -e "discovery.type=single-node" \ -e ES_JAVA_OPTS="-Xms64m -Xmx128m" \ -v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \ -v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \ -v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \ -d elasticsearch:7.4.2
free -m
查看内存命令
docker安装kibana
docker pull kibana:7.4.2
1 docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 -p 5601:5601 -d kibana:7.4.2
入门操作 新增
PUT和POST都可以
POST新增。如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号。
PUT可以新增可以修改。PUT必须指定id。由于PUT需要指定id,我们一般用来做修改操作,不指定id会报错。
查询文档
_seq_no
乐观锁?if_seq_no=0&if_primary_term=1
更新文档
删除文档或索引
批量API
进阶操作
检索信息
Query DSL https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
IK分词器 安装 vagrant 使用root
账号连接开启过程
安装wget
yum install wget
1 2 3 4 5 6 7 wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip unzip elasticsearch-analysis-ik-7.4.2.zip rm -rf *.zip mv ./* ik
可以进入到容器里面
docker exec -it elasticsearch /bin/bash
进入到了cd /bin/
运行elasticsearch-plugin list
安装成功。退出容器,重启docker restart elasticserach
词库
可视化插件 https://blog.csdn.net/ljcgit/article/details/103856634
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ls 结果如下: LICENSE.txt README.textile config lib modules NOTICE.txt bin data logs plugins cd config ls 结果如下: elasticsearch.keystore ingest-geoip log4j2.properties roles.yml users_roles elasticsearch.yml jvm.options role_mapping.yml users vi elasticsearch.yml http.cors.enabled: true http.cors.allow-origin: "*"
1 2 3 docker pull mobz/elasticsearch-head:5 docker run -d --name es_admin -p 9100:9100 mobz/elasticsearch-head:5
Nginx
include /etc/nginx/conf.d/*.conf;
这里会将这个目录下的配置文件一并包括生效,所以要注意这里配置文件所配置的信息。
负载均衡到网关 首先配置本机的host
输入gulimall.com访问虚拟机IP地址80端口
虚拟机的80端口运行了Nginx,docker中的/etc/nginx/conf.d/gulimall.conf
中配置了server_name gulimall.com
当访问的时候会携带host参数,代理地址为http://gulimall
而Nginx的配置文件nginx.conf
中配置了gulimall的负载均衡转发到了我们的网关。
我们的网关则把请求转发到了我们的gulimall-product
服务中。
JMeter
nginx测试 向http://192.168.56.10/
发送测试
使用docker stats
实时查看容器运行情况
当我点击运行测试的时候,我们能看见
所以nginx是CPU密集型
这里出现的异常是我们线程组给stop
导致的,从而请求失败。
吞吐量/s:8086.8
90%响应时间:7
99%相应时间:18
网关测试 向http://localhost:88/
发送测试
吞吐量/s:26274.7
90%响应时间:3
99%相应时间:9
简单服务测试 访问http://localhost:10000/hello
吞吐量/s:45000
90%响应时间:2
99%相应时间:4
网关+简单服务测试
访问测试http://localhost:88/hello
吞吐量/s:6409.0
90%响应时间:3
99%相应时间:13
两个一加起来吞吐量大量减少。
nginx+网关+简单服务测试 访问http://gulimall.com/hello
吞吐量/s:2279.2
90%响应时间:34
99%相应时间:52
中间件越多,性能损失越多,大多损失在网络交互了。
首页一级菜单渲染 访问测试http://localhost:10000/
tymeleaf开启缓存
吞吐量/s:2632.7
90%响应时间:27
99%相应时间:50
日志打印级别提高为erro
吞吐量/s:3125.5
90%响应时间:23
99%相应时间:42
字段设置为索引
首页三级分类数据获取 访问测试http://localhost:10000/index/catalog.json
吞吐量/s:323.2(db)
90%响应时间:176
99%相应时间:266
首页全量数据获取 访问测试http://localhost:10000/
高级
设置中勾选从HTML文件获取所有内容的资源
吞吐量/s:62(静态资源)
90%响应时间:1122
99%相应时间:1510
异常是因为有些文件报404
Nginx动静分离优化
将静态目录复制到虚拟机/mydata/nginx/html/static
的目录下
进入docker容器,编写配置/etc/nginx/conf.d/gulimall.conf
文件
1 location /static/ { root /usr/share/nginx/html;}
其中容器内目录/usr/share/nginx/html
目录挂载到了/mydata/nginx/html/
也就是我们存放静态资源的地方。
访问http://gulimall.com
进行测试
缓存
redis 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class StringRedisTemplate extends RedisTemplate <String , String > { public StringRedisTemplate () { setKeySerializer(RedisSerializer.string()); setValueSerializer(RedisSerializer.string()); setHashKeySerializer(RedisSerializer.string()); setHashValueSerializer(RedisSerializer.string()); } public StringRedisTemplate (RedisConnectionFactory connectionFactory) { this (); setConnectionFactory(connectionFactory); afterPropertiesSet(); } protected RedisConnection preProcessConnection (RedisConnection connection, boolean existingConnection) { return new DefaultStringRedisConnection(connection); } }
StringRedisTemplate
是专门操作string
类型数据。
使用redis将数据存入到缓存中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public Map<String, List<Catelog2Vo>> getCatelogJson() { String catalogJson = redisTemplate.opsForValue().get("catalogJson" ); if (StringUtils.isEmpty(catalogJson)) { Map<String, List<Catelog2Vo>> catalogJsonFromDB = getCatalogJsonFromDB(); String s = JSON.toJSONString(catalogJsonFromDB); redisTemplate.opsForValue().set("catalogJson" , s); return catalogJsonFromDB; } Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return result; }
此时压测访问首页三级分类数据获取http://localhost:10000/index/catalog.json
(日志关闭,thymeleaf缓存开启,索引创建)
吞吐量/s:1351
90%响应时间:44
99%相应时间:99
出现堆外内存溢出:OutOfDirectMemoryError
(lettuce5.2.1暂时没出现,5.1.8出现问题)
springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
lettuce的bug导致netty堆外内存溢出
缓存穿透
缓存雪崩
缓存击穿
分布式锁
结果放入缓存这步操作应该放在锁内,否则会因为网络通信延迟导致又查询了一次数据库。
Redisson 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
https://github.com/redisson/redisson/wiki/Table-of-Content
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Bean(destroyMethod = "shutdown") public RedissonClient redisson () { Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.10:6379" ); RedissonClient redissonClient = Redisson.create(config); return redissonClient; }
缓存数据一致性
SpringCache
引入依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 @Cacheable(value = {"category"}, key = "#root.method.name", sync = true) @Override public List<CategoryEntity> getLevel1Categories () { System.out.println("get Level 1 Categories........" ); long l = System.currentTimeMillis(); List<CategoryEntity> categoryEntities = this .baseMapper.selectList( new QueryWrapper<CategoryEntity>().eq("parent_cid" , 0 )); System.out.println("消耗时间:" + (System.currentTimeMillis() - l)); return categoryEntities; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration (CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); CacheProperties.Redis redisProperties = cacheProperties.getRedis(); if (redisProperties.getTimeToLive() != null ) { config = config.entryTtl(redisProperties.getTimeToLive()); } if (redisProperties.getKeyPrefix() != null ) { config = config.prefixKeysWith(redisProperties.getKeyPrefix()); } if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } return config; } }
相关配置文件
1 2 3 4 5 6 7 8 9 10 spring.cache.type =redis spring.cache.redis.time-to-live =3600000 spring.cache.redis.key-prefix =CACHE_ spring.cache.redis.use-key-prefix =true spring.cache.redis.cache-null-values =true
异步
线程池 JUC之线程池 | Kylin (codekylin.cn)
CompletableFuture CompletableFuture
类似前端的promise