赞
踩
https://blog.csdn.net/weixin_45606067/article/details/109629397
@MapperScan
注解,扫描dao包@EnableDiscoveryClient
开启服务注册发现功能;基础配置如下:
spring.application.name
和 spring.cloud.nacos.config.server-addr
@RefreshScope
注解 动态刷新配置,@Value("${}")
注解 获取到配置。细节配置如下:
命名空间
默认:public(保留空间):默认新增的所有配置都在public空闲下。
1)(dev)开发、(test)测试、(pro)生产:利用命名空间来做环境隔离。
注意:在bootstrap.properties
中配置上需要使用那个命名空间;
spring.cloud.nacos.config.namespace=43e4b62f-d65b-4295-bf06-8be264de464b
2)每个微服务之间相互隔离,每个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置。
配置集:所有的配置集合。
配置集ID:类似于文件名。
配置分组
默认所有的配置集都属于:DEFAULT_GROUP
我们可以给每个微服务创建自己的命名空间,使用配置分租区分环境。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
注意如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
@TableLogic
javax.validation.constraints
,并定义自己的message提示@Valid
BindingResult
,就可以获取到验证的结果。@NotBlank
,在分组校验情况下不会生效,只有在@Validated
生效;@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class【可以指定多个不同的校验器,适配不同类型的校验器】 })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
@RestControllerAdvice
@ExceptionHandler
标注方法可以处理异常。跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是 浏览器对javascript施加的安全限制。
方式一:使用nginx部署为同一域
方式二:配置当次请求允许跨域
gateway网关模块进行配置如下代码:
package com.kuang.gulimall.gateway.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; @Configuration public class GulimallCorsConfiguration { @Bean public CorsWebFilter corsWebFilter(){ UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); //允许那些头可以跨域 corsConfiguration.addAllowedHeader("*"); //允许那些方式可以跨域 corsConfiguration.addAllowedMethod("*"); //允许那个请求来源 corsConfiguration.addAllowedOrigin("*"); //是否允许携带cookie进行跨域 corsConfiguration.setAllowCredentials(true); source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }
普通上传方式
服务端签名后直传
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
spring:
cloud:
alicloud:
access-key: LTAI5tAAoGLQjwxzdxnTjioC
secret-key: 8CNjhNrk03HcxV1mlVkfuBStROb6IT
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket: gulimall-hello-2021
SPU:Standard Product Unit (标准化产品单元)
是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。
SKU:Stock Keeping Unit (库存量单位)
即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU这是对于大型连锁超市
DC (配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。
每个分类下得商品共享规格参数,与销售属性。知识有些商品不一定要用这个分类下全部得属性:
属性分组-规格参数-销售属性-三级关系图如下:
官网地址:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
环境安装:https://blog.csdn.net/weixin_45606067/article/details/109629397
入门检索学习:https://blog.csdn.net/weixin_45606067/article/details/110818401
进阶检索学习:
商城项目应用场景:
1)9300: TCP
2)9200: HTTP
JestClient: 非官方,更新慢;
RestTemplate:模拟HTTP请求,ES很多操作需要自己封装,麻烦;
HttpClient:同上;
Elasticsearch-Rest-Client:官方RestClient,封装了ES操作,API层次分明,上手简单;
最终选择Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client);
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
上架的商品才可以在网站中展示,上架的商品需要可以被检索。
① 上架是将后台的商品放在es中可以提供检索和查询功能:
1) hasStock: 代表是否有库存。 默认上架的商品都有库存。 如果库存无货的时候才需要更新一下 es
2) 库存补上以后, 也需要重新更新一下 es
3) hotScore:代表热度值。我们只模拟使用点击率更新热度。 点击率增加到一定程度才更新热度值。
4) 下架就是从 es 中移除检索项, 以及修改 mysql 状态。
② 商品上架步骤:
1)先在es中按照之前的mapping信息,建立 gulimall_product
索引。
2)点击上架,查询出所有sku的信息,保存到es中;
3)es保存成功后返回,更新数据的上架状态信息。
创建商品在es中索引格式
PUT gulimall_product { "mappings": { "properties": { "skuId": { "type": "long" }, "spuId": { "type": "keyword" }, "skuTitle": { "type": "text", "analyzer": "ik_smart" }, "skuPrice": { "type": "keyword" }, "skuImg": { "type": "keyword" }, "saleCount": { "type": "long" }, "hasStock": { "type": "boolean" }, "hotScore": { "type": "long" }, "brandId": { "type": "long" }, "catalogId": { "type": "long" }, "brandName": { "type": "keyword" }, "brandImg": { "type": "keyword" }, "catalogName": { "type": "keyword" }, "attrs": { "type": "nested", "properties": { "attrId": { "type": "long" }, "attrName": { "type": "keyword" }, "attrValue": { "type": "keyword" } } } } } }
商品上架接口参照:/product/spuinfo/{spuId}/up
。调试通过debug断点测试。
步骤:
1. 查询当前spuid对应的所有sku信息;遍历得到skuId的集合
2. 查询当前sku所有 可以被用来检索的规格属性;遍历得到attrId的集合
3. 在指定的attrId集合中,挑出检索的属性
4. 将被用来检索的规格属性的数据遍历放入检索对象中
5. 发送远程调用,去库存系统根据skuId集合查询是否有库存
6. 封装每个sku的信息
6.1 组装需要的数据
6.2 设置库存信息
6.3 设置热度评分
6.4 查询并设置品牌的名字和图片信息
6.5 查询并设置分类的名字和图片信息
6.6 设置检索属性
7. 将数据发送给es进行保存,gulimall-search服务
8. 修改数据库中当前spu的状态
官网地址:
https://www.thymeleaf.org/documentation.html
使用步骤:
ctrl + F9
重新自动编译页面,代码配置需要重启。渲染一级分类 接口:/index.html
渲染二级和三级分类 接口:/index/catalog.json
由于静态资源放在项目中的static 文件夹下,过于消耗资源,所以我们将项目中所有的静态资源放在服务器中保存,为此引入nginx。
官网地址:http://nginx.org/en/docs/
修改本机 hosts
(C:\Windows\System32\drivers\etc\hosts)文件,配置域名如下:
nginx监听的是虚拟机的80端口,访问gulimall.com此时就会访问到nginx的index页面。
1、搭建域名访问地址进行反向代理(gulimall.conf配置文件)
原理:浏览器访问gulimall.com,windows中的hosts文件中指明了gulimall.com映射的是虚拟机IP,因此gulimall.com就会来到虚拟机,来到虚拟机之后,虚拟机的nginx又监听了80端口,而且域名是gulimall.com的请求,nginx就会帮我们代理到windows本机上的服务地址。
2、负载均衡
gulimall.com会来到虚拟机中的nginx,由nginx再代理给我们的商品服务,但是商品服务可能是一个集群环境,多台服务器,而且有上线和下线,如果我们直接使用nginx代理我们的商品服务,那么就需要nginx负载均衡到商品服务中,而且商品服务的机器上下线也是动态的,那么就需要经常修改配置,因此我们希望nginx将请求交给网关,由网关通过nacos服务注册中心,获取上线的商品服务,由网关负载均衡到商品服务。
3、nginx动静分离(压测优化)
/mydata/nginx/html/static
conf.d/gulimall.conf
文件JMeter的下载和使用:https://blog.csdn.net/weixin_45606067/article/details/121248621
1. jvm内存模型
2. 堆
所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也称为 GC堆
,也就是我们最多考虑的地方。
堆可以细分为:
垃圾回收
从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一 个新的区域—元空间(MetaSpace)
3. 项目如何监听JVM
Jdk 的两个小工具 jconsole
、jvisualvm
(升级版的 jconsole);通过命令行启动,可监控本地和 远程应用。远程应用需要配置。
安装官方gc插件
如果在检验更新版本时 出现503错误,解决方法如下:
打开网址 https://visualvm.github.io/pluginscenters.html
cmd 查看自己的 jdk,复制下面查询出来的链接。并重新设置上即可
4. 项目检测指标
中间件越多,性能损失越大,大多数都损失在网络交互上。
简单优化
① 开启模板引擎缓存
② nginx静态资源和动态资源(thymeleaf)分离
③ 开启 jvisualvm监测 visual gc情况
④ 先使用50个线程来进行压测:
可以看到吞吐量可以达到8左右,仍然很低
同过观察可以发现,老年代和伊甸园区经常爆满,频繁的垃圾回收,垃圾回收太浪费时间了
⑤ 改用200个线程压力测试:
可以看到老年代已满,内存溢出,服务已经崩溃
继续简单优化
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据罗盘工作。
那些数据适合放入缓存?
项目整合redis步骤:
data-redis-starter
StringRedisTemplate
来操作redis以项目中查询二级分类和三级分类为例(/index/catalog.json
接口)的原理:
注意:缓存中存的数据是json字符串,因为json是跨语言,跨平台兼容的。我们拿到json字符串后,还要逆转为能用的对象类型。【序列化与反序列化】
上述代码 通过压力测试 产生的问题:堆外内存溢出:outOfDirectMemoryError
产生原因:
1)SpringBoot2.0 以后默认使用 lettuce
作为操作 redis 的客户端。它使用 netty
进行网络通信。
2)lettuce 的bug导致 netty 堆外内存溢出。VM Option = -Xmx300m
;netty 如果没有指定堆外内存,默认使用 -Xmx300m
解决方案:不能使用 -Dio.netty.maxDirectMemory
只去调大堆外内存。
1)升级 lettuce 客户端
2)切换使用 jedis
说明:lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate。
1. 缓存穿透
是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此纪录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要存储层去查询,失去了缓存的意义。
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃。
解决:null结果缓存,并加入短暂过期时间。
2. 缓存雪崩
是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引引发集体失效的事件。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中。设置热点数据永远不过期。
出现雪崩:降级 熔断。
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
3. 缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发的访问,是一种非常 “热点” 数据。
如果这个key在大量请求同时进来前面正好失效,那么所有对这个key的数据都落到db,我们称为 缓存击穿。
解决:加锁。常用的做法是使用 mutex
。
大量并发只让一个去查,其他人等待,查到后释放锁,其他人获取到锁,先去查缓存,就会有数据,不用去db查询。
为了解决缓存击穿问题。以项目中查询二级分类和三级分类为例。
加锁:只要是同一把锁,就能锁住需要这个锁的所有线程。
synchronized (this)
:springboot所有的组件在容器中都是单例的。
本地锁:synchronized,JUC(Lock)只能锁住当前进程;
在分布式情况下,想要锁住所有,必须使用分布式锁。
锁时序问题:之前的逻辑是查缓存没有,然后去竞争锁查数据库,这样就造成多次查数据库。
解决方法:竞争到锁后,再次确认缓存中没有,再去查数据库,查询后的结果直接放入缓存。
idea 如何复制微服务:
右键点击服务,copy configuration
在program arguments: --server.port=10003
本地锁,只能锁住当前进程,所以我们需要分布式锁。
分布式基本原理:我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。
“占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。
分布式锁演进 - 阶段一
问题:setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁。
解决:设置锁的过期时间,即使没有删除,也会自动删除。
分布式锁演进 - 阶段二
问题:setnx设置好,正好去设置过期时间,宕机。又死锁了。
解决:设置过期时间和占位必须是原子的。redis支持使用setnx ex命令。
问题:删除锁直接删除??
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。
问题:如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁。
解决:删除锁必须保证原子性。使用redis+Lua脚本完成。
官网说明:http://redis.cn/commands/set.html
改造redis锁的最终代码如下:
上面的 lua 脚本写法每次用分布式锁时都比较麻烦,官网推荐我们可以采用 redisson
框架。
https://redis.io/docs/reference/patterns/distributed-locks/
官网:https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0
Redisson是一个在Redis的基础上实现的Java驻内存数据网格。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离),从而让使用者能够将精力更集中地放在处理业务逻辑上。
1、项目整合redisson步骤:
2、锁的说明:【参照官网 8. 分布式锁和同步器】
整体知识点可参照
JUC
,进行学习
(1)可重入锁(Reentrant Lock)
基于Redis的 Redisson 分布式可重入锁 RLock Java 对象实现了java.util.concurrent.locks.Lock
接口。同时还提供了异步(Async
)、反射式(Reactive
)和RxJava2
标准的接口。
锁的续期:大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout
来另行指定。
@ResponseBody @GetMapping("/hello") public String hello() { //获取一把锁,只要锁的名字相同,就是同一把锁 RLock lock = redisson.getLock("my-lock"); //加锁 lock.lock();//阻塞式等待,默认加的锁都是30s的时间 //1)锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删除。 //2)加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s之后自动删除。 // lock.lock(10, TimeUnit.SECONDS);//10s自动解锁,自动解锁时间一定要大于业务执行时间。 //问题:lock.lock(10, TimeUnit.SECONDS);在锁时间到了以后,不会自动续期。 //1. 如果我们传递了超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间 //2. 如果我们未指定超时时间,就使用30 * 1000【lockWatchdogTimeout看门狗的默认时间】 // 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】 // internalLockLeaseTime【看门狗的的时间】 / 3 ,也就是10s。每隔10s都会自动再次续期,续成30s //最佳实战:lock.lock(30, TimeUnit.SECONDS);省掉了整个续期操作,手动操作。将解锁时间设大一些 为30s try { System.out.println("加锁成功,指定业务代码...." + Thread.currentThread().getId()); Thread.sleep(30000); } catch (Exception e) { } finally { //解锁 假设解锁代码没有运行,redisson会不会出现死锁。 结果是不会。 System.out.println("释放锁..." + Thread.currentThread().getId() ); lock.unlock(); } return "hello"; }
(2)读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。其中读锁和写锁都继承了RLock接口。
@ResponseBody @GetMapping("/write") public String writeValue() { RReadWriteLock lock = redisson.getReadWriteLock("wr-lock"); String s = ""; RLock rLock = lock.writeLock();//改数据加写锁 try { rLock.lock(); System.out.println("写锁加锁成功..." + Thread.currentThread().getId()); s = UUID.randomUUID().toString(); Thread.sleep(30000); redisTemplate.opsForValue().set("writeValue",s); } catch (InterruptedException e) { e.printStackTrace(); } finally { rLock.unlock(); System.out.println("写锁释放成功..." + Thread.currentThread().getId()); } return s; } @ResponseBody @GetMapping("/read") public String readValue() { RReadWriteLock lock = redisson.getReadWriteLock("wr-lock"); String s = ""; RLock rLock = lock.readLock();//读数据加读锁 try { rLock.lock(); System.out.println("读锁加锁成功..." + Thread.currentThread().getId()); Thread.sleep(30000); s = redisTemplate.opsForValue().get("writeValue"); } catch (Exception e) { e.printStackTrace(); } finally { rLock.unlock(); System.out.println("读锁释放成功..." + Thread.currentThread().getId()); } return s; }
(3)信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与 java.util.concurrent.Semaphore
相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
/** * 举例:车库停车 3个车位 */ @ResponseBody @GetMapping("/park") public String park() throws InterruptedException { RSemaphore park = redisson.getSemaphore("park"); park.acquire();//获取一个信号 //信号量 可以用于分布式限流。 // boolean b = park.tryAcquire(); // if (b){ // //执行业务 // } else { // return "error"; // } return "停车..."; } @ResponseBody @GetMapping("/go") public String go() { RSemaphore park = redisson.getSemaphore("park"); park.release();//释放一个信号 return "开走..."; }
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用 release()
方法增加数量,也可以调用 acquire()
方法减少数量,但是当调用 release()
之后小于0的话方法就会阻塞,直到数字大于0
(4)闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与 java.util.concurrent.CountDownLatch
相似的接口和用法。
/** * 举例:放假锁门 * 5个班全部走完,我们就可以锁门 */ @ResponseBody @GetMapping("/lockDoor") public String lockDoor() throws InterruptedException { RCountDownLatch latch = redisson.getCountDownLatch("door"); latch.trySetCount(5); latch.await(); return "放假了..."; } @ResponseBody @GetMapping("/gogogo/{id}") public String gogogo(@PathVariable("id") Long id) { RCountDownLatch latch = redisson.getCountDownLatch("door"); latch.countDown();//计数减一 return id + "班的人都走了..."; }
改造redisson锁的最终代码如下:
缓存里面的数据如何和数据库中数据保持一致
解决方案:
总结:
每次都那样写缓存太麻烦了,spring从3.1开始定义了Cache、CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发
Cache接口的实现包括RedisCache
、EhCacheCache
、ConcurrentMapCache
等。
每次调用需要缓存功能的方法时,spring会检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
项目整合cache步骤:
spring.cache.type=redis
# 过期时间。毫秒为单位,设置为1小时
spring.cache.redis.time-to-live=3600000
# key的前缀,如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
# 是否使用前缀
spring.cache.redis.use-key-prefix=true
# 是否缓存空值。防止缓存穿透。
spring.cache.redis.cache-null-values=true
缓存注解的说明:
原理说明:【源码分析】
获取菜单代码进行修改:
/** * 更新本表及关联表,保证冗余字段的数据一致性 * @CacheEvict:失效模式 * 1. 同时进行多种缓存操作:@Caching * 2. 指定删除某个分区下的所有数据:@CacheEvict(value = "category",allEntries = true) * 3. 存储同一个类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀。 */ // @Caching(evict = { // @CacheEvict(value = "category",key = "'getLevel1Categorys'"), // @CacheEvict(value = "category",key = "'getCatalogJson'") // }) @CacheEvict(value = "category",allEntries = true)//失效模式 // @CachePut()//双写模式 @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); if (!StringUtils.isEmpty(category.getName())) { // 同步更新其他关联表中的数据 categoryBrandRelationService.updateCategory(category.getCatId(),category.getName()); // TODO 更新其他关联表 } } /** * 查询所有一级分类 * 1. 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务划分)】 * 2. @Cacheable({"category"}):表示当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法,并将方法的结果放入缓存。 * 3. 默认行为 * 1)如果缓存中有,方法不用调用 * 2)key默认自动生成:缓存的名字::SimpleKey [](自动生成key的值) * 3)缓存的value的值:默认使用jdk序列换机制。将序列化后的数据存到redis。 * 4)默认ttl时间是-1。 * 4. 自定义 * 1)指定生成缓存使用的key: key属性指定,接收一个SpEL表达式 * SpEL语法详细:https://docs.spring.io/spring-framework/docs/5.3.19-SNAPSHOT/reference/html/integration.html#cache * 2)指定缓存数据的存活时间: 配置文件中修改ttl * 3)将数据保存为json格式: * 查看源码,自定义RedisCacheConfiguration配置类进行修改 */ @Cacheable(value = {"category"},key = "#root.method.name") @Override public List<CategoryEntity> getLevel1Categorys() { System.out.println("getLevel1Categorys...."); List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; } @Cacheable(value = {"category"},key = "#root.methodName") @Override public Map<String, List<Catelog2Vo>> getCatalogJson(){ System.out.println("查询了数据库....."); List<CategoryEntity> selectList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L); // 封装数据 Map<String, List<Catelog2Vo>> listMap = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 每一个一级分类,查到这个一级分类的二级分类 List<CategoryEntity> level2Catelog = getParent_cid(selectList,v.getCatId()); // 封装上面的结果集 List<Catelog2Vo> catelog2Vos = null; if (level2Catelog != null) { catelog2Vos = level2Catelog.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName()); // 找到当前二级分类的三级分类封装成vo List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId()); if (level3Catelog != null) { List<Catelog2Vo.catelog3Vo> collect = level3Catelog.stream().map(l3 -> { // 封装成指定格式 Catelog2Vo.catelog3Vo catelog3Vo = new Catelog2Vo.catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()); return catelog3Vo; }).collect(Collectors.toList()); catelog2Vo.setCatalog3List(collect); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); return listMap; }
默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
@EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching//开启缓存 public class MyCacheConfig { /** * 配置文件中的东西没有用上,不生效。 * 1. 原来和配置文件绑定的配置类是这样的: * @ConfigurationProperties(prefix = "spring.cache") * public class CacheProperties { * 2. 要让他生效 * @EnableConfigurationProperties(CacheProperties.class) */ @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); // 将配置文件中的所有配置都生效 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; } }
SpringCache原理与不足:
1)读模式
spring.cache.redis.cache-null-values=true
sync = true
来解决击穿问题spring.cache.redis.time-to-live=3600000
2)写模式:(缓存与数据库一致)
3)总结:
除了在检索页面通过 三级分类catelog3Id
和检索关键字keyword
进行检索商品外,还有其他的检索条件进行检索。
keyword=华为&catalog3Id=225&attrs=1_NOH-AL00/NOH-AL10&attrs=2_2010&sort=saleCount_desc&hasStock=1&brandId=2
接口可以通过postman进行单独测试:
发送请求查看 控制台中 构建的DSL语句 在 kibana
中进行验证。
检索页完成的功能:
① 商品
② 品牌、分类、属性
③ 排序、价格区间、是否显示有货
④ 分页
⑤ 面包屑导航
⑥ 条件筛选联动
1、初始化线程的 4 种方式
public class ThreadTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { //1、继承Thread Thread01 thread01 = new Thread01(); thread01.start(); //2、实现Runnable接口 Runnable01 runnable01 = new Runnable01(); new Thread(runnable01).start(); //3、实现Callable接口 + FutureTask FutureTask<Integer> futureTask = new FutureTask<>(new Callable01()); new Thread(futureTask).start(); Integer integer = futureTask.get();//阻塞等待整个线程执行完成,获取返回结果 //4、线程池 //我们以后业务代码里面,以上三种启动线程的方式都不用。【将所有的多线程异步任务都交给线程池执行】 //当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行 service.execute(new Runnable01()); //原生的线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 200, 10, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } public static class Thread01 extends Thread { @Override public void run() { System.out.println("当前线程:"+Thread.currentThread().getId()); int i = 10 /2; System.out.println("运行结果:" + i); } } public static class Runnable01 implements Runnable { @Override public void run() { System.out.println("当前线程:"+Thread.currentThread().getId()); int i = 10 /2; System.out.println("运行结果:" + i); } } public static class Callable01 implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("当前线程:"+Thread.currentThread().getId()); int i = 10 /2; System.out.println("运行结果:" + i); return i; } } }
总结:方式 1 和方式 2 不能得到返回值,方式3 可以得到返回值。
方式 1 和方式 2 和方式 3 不利于控制服务器中的线程资源。会导致服务器资源耗尽。
方式四 可以控制资源,比较稳定,也可以获取执行结果, 并捕获异常。
2、开发中为什么使用线程池?
降低资源的消耗: 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗。
提高响应速度:因为线程池中的线程数没有超过线程池的最大上限时, 有的线程处于等待分配任务的状态, 当任务来时无需创建新的线程就能执行。
提高线程的可管理性:线程池会根据当前系统特点对池内的线程进行优化处理, 减少创建和销毁线程带来的系统开销。 无限的创建和销毁线程不仅消耗系统资源, 还降低系统的稳定性, 使用线程池进行统一分配 。
3、常见的 4 种线程池
4、线程池的七大参数说明
5、线程池工作顺序
面试: 一个线程池 core 7; max 20 ,queue:50,100 并发进来怎么分配的;
解决:先有 7 个能直接得到执行,接下来 50 个进入队列排队,在多开 13 个继续执行。现在 70 个 被安排上了。剩下 30 个默认拒绝策略。
如果不想抛弃还要执行,可以使用 CallerRunsPolicy 同步方式执行。
业务场景:查询商品详情页的逻辑比较复杂, 有些数据还需要远程调用, 必然需要花费更多的时间。
假如商品详情页的每个查询, 需要如下标注的时间才能完成 ,那么, 用户需要 5.5s 后才能看到商品详情页的内容。 很显然是不能接受的。如果有多个线程同时完成这 6 步操作, 也许只需要 1.5s 即可完成响应。
在 Java 8 中, 新增加了一个包含 50 个方法左右的类: CompletableFuture, 提供了非常强大的Future 的扩展功能, 可以帮助我们简化异步编程的复杂性, 提供了函数式编程的能力, 可以通过回调的方式处理计算结果, 并且提供了转换和组合 CompletableFuture 的方法。CompletableFuture 类实现了 Future 接口, 所以你还是可以像以前一样通过get方法阻塞或者轮询的方式获得结果, 但是这种方式不推荐使用。
CompletableFuture 和 FutureTask 同属于 Future 接口的实现类, 都可以获取线程的执行结果。
1、四个静态方法来创建一个异步操作
runXxxx 都是没有返回结果的, supplyXxx 都是可以获取返回结果的;Executor 可以传入自定义的线程池, 否则就用默认的线程池 。
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
}
}
2、计算完成时的回调方法:方法完成后的感知
public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程:" + Thread.currentThread().getId()); int i = 10 / 0; System.out.println("运行结果:" + i); return i; }, service).whenComplete((res,exception) -> { //虽然能得到异常信息,但是没法修改返回数据。 System.out.println("异步任务成功完成了...结果是:" + res + ";异常是" + exception); }).exceptionally(throwable -> { //可以感知异常,同时返回默认值。 return 10; }); } }
3、handle方法完成后的处理
public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("运行结果:" + i); return i; }, service).handle((res,thr) -> { if (res != null) { return res*2; } if (thr != null) { return 0; } return 1; }); Integer integer = future.get();//得到返回值 } }
4、线程串行化方法
/** * 1.thenRunAsync:不能获取到上一步的执行结果,无返回值 * .thenRunAsync(() -> { * System.out.println("任务2启动了..."); * }, service); * 2.thenAcceptAsync:能接受上一步结果,但没有返回值 * .thenAcceptAsync(res -> { * System.out.println("任务2启动了..." + res); * }, service); * 3.thenApplyAsync:能接受上一步结果,有返回值 * .thenApplyAsync(res -> { * System.out.println("任务2启动了..." + res); * return "hello" + res; * }, service); */ public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { System.out.println("当前线程:" + Thread.currentThread().getId()); int i = 10 / 4; System.out.println("运行结果:" + i); return i; }, service).thenApplyAsync(res -> { System.out.println("任务2启动了..." + res); return "hello" + res; }, service); } }
5、两任务组合 - 都要完成
public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> { System.out.println("任务1线程:" + Thread.currentThread().getId()); int i = 10 / 2; System.out.println("任务1结束:" + i); return i; }, service); CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> { System.out.println("任务2线程:" + Thread.currentThread().getId()); try { Thread.sleep(3000); System.out.println("任务2结束..."); } catch (InterruptedException e) { e.printStackTrace(); } return "hello"; }, service); //不能感知到前两个结果,无返回值 future1.runAfterBothAsync(future2,() -> { System.out.println("任务3开始..."); },service); //能感知到前两个结果,无返回值 future1.thenAcceptBothAsync(future2,(f1,f2) -> { System.out.println("任务3开始..." + f1 + "->" + f2); },service); //能感知到前两个结果,有返回值 CompletableFuture<String> future = future1.thenCombineAsync(future2, (f1, f2) -> { return f1 + ": " + f2 + "-> haha"; }, service); System.out.println("future = " + future.get()); } }
6、两任务组合 - 一个完成
public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> { System.out.println("任务1线程:" + Thread.currentThread().getId()); int i = 10 / 2; System.out.println("任务1结束:" + i); return i; }, service); CompletableFuture<Object> future2 = CompletableFuture.supplyAsync(() -> { System.out.println("任务2线程:" + Thread.currentThread().getId()); try { Thread.sleep(3000); System.out.println("任务2结束..."); } catch (InterruptedException e) { e.printStackTrace(); } return "hello"; }, service); //不感知结果,自己无返回值 future1.runAfterEitherAsync(future2,() -> { System.out.println("任务3开始..."); }, service); //感知结果,自己无返回值 future1.acceptEitherAsync(future2,(res) -> { System.out.println("任务3开始..." + res); },service); //感知结果,自己有返回值 CompletableFuture<String> future = future1.applyToEitherAsync(future2, (res) -> { return res.toString() + " -> haha"; }, service); System.out.println("future = " + future.get()); } }
7、多任务组合
public class CompletableFutureTest { public static ExecutorService service = Executors.newFixedThreadPool(10); public static void main(String[] args) throws ExecutionException, InterruptedException { CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品图片信息"); return "hello.jpg"; },service); CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(3000); System.out.println("查询商品的属性"); } catch (InterruptedException e) { e.printStackTrace(); } return "黑色256g"; },service); CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> { System.out.println("查询商品介绍"); return "华为"; },service); //等待所有任务完成 CompletableFuture<Void> future = CompletableFuture.allOf(futureImg, futureAttr, futureDesc); future.get(); System.out.println(futureImg.get() + "->" + futureAttr.get() + "->" + futureDesc.get()); //只有一个任务完成 CompletableFuture<Object> future = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc); future.get(); System.out.println(future.get()); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。