文章目录
- 1、什么是vagrant?
- 2、vagrant常用命令
- 3、docker常用命令
- 4、springboot 2.2之前的测试类使用
- 5、注册微服务到nacos
- 6、openFegin的使用
- 7、 配置中心nacos
- 8、**@RefreshScope** 动态更新配置中心的配置
- 9、网关gateway
- 10、@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})的作用:让DataSourceAutoConfiguration相关配置不生效
- 11、配置网关路由示例
- 12、跨域问题
- 13、mybvatis-plus 逻辑删除
- 14、阿里云上传
- 15、统一异常处理
- 16、@JsonInclude注解:去空字段
- 17、将集合反转
- 18、集合转数组
- 19、String.join(" ",strings)以指定字符串拼接集合
- 20、@RequestBody传输和接收时只要据模型是兼容的。双方服务无需使用同一个to
- 21、设置mysql隔离级别为读未提交
- 22、设置主键id为输入
- 23、feign调用的两种路径方案
- 24、不让事务回滚的方法
- 25、List转set的方法
- 26、List转Map的方法
- 27、lambda 表达式中使用的变量应该是最终的或有效的最终的
- 28、Nginx相关笔记
- 29、JMeter性能压测
- 30、缓存和分布式缓存
- 31、redis缓存
- 32、缓存穿透、雪崩、击穿
- 33、分布式锁--Jedis/lettuce+lua 原子性 来实现分布式锁
- 34、分布式锁--Redisson
- 35、缓存和数据库一致性
- 36、SpringCache
- 37、线程池
- 38、CompletableFuture异步编排
- 39、mybatis自定义结果集resultMap封装嵌套属性
- 40、自定义线程池
- 41、异步编程使用
- 42、配置方式试图解析
- 43、MD5
- 44、社交登录
- 45、分布式session
- 46、SpringSession整合redis
- 47、分布式登录总结
- 48、单点登录
- 49、登录拦截器
- 50、[后续笔记](https://blog.csdn.net/qq_24654501/article/details/119796732)
- 51、接口防刷
- 52、Spring Boot 整合 定时任务
- 53、
- 54、
- 55、
- 56、
- 57、
- 58、
- 59、
- 60、
- 61、
- 62、
- 63、
- 64、
- 65、
- 66、
- 67、
- 68、
- 69、
- 70、
- 71、
- 72、
- 73、
- 74、
- 75、
- 76、
- 77、
- 78、
- 79、
- 80、
- 81、
- 82、
- 83、
- 84、
- 85、
- 86、
- 87、
- 88、
- 89、
- 90、
- 91、
- 92、
- 93、
- 94、
- 95、
- 96、
- 97、
- 98、
- 99、
- 100、
1、什么是vagrant?
简单理解,就是可以通过Vagrant这个工具管理虚拟机,比如说想创建一个centos环境的虚拟机,不需要安装系统这么麻烦,通过vagrant可以快速创建
2、vagrant常用命令
vagrant up 启动虚拟机
vagrant reload 重启虚拟机
3、docker常用命令
##列出本地images
docker images
##下载Redis官方最新镜像,相当于:docker pull redis:latest
docker pull redis
##单个镜像删除,相当于:docker rmi redis:latest
docker rmi redis
##新建并启动容器,参数:-i 以交互模式运行容器;-t 为容器重新分配一个伪输入终端;--name 为容器指定一个名称
docker run -i -t --name mycentos
##启动一个或多个已经被停止的容器
docker start redis
##重启容器
docker restart redis
##关闭容器并退出
exit
##查看正在运行的容器
docker ps
##查看正在运行的容器的ID
docker ps -q
##停止一个运行中的容器
docker stop redis
4、springboot 2.2之前的测试类使用
5、注册微服务到nacos
首先,修改 common中的pom.xml 文件,引入 Nacos Discovery Starter。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
1、配置nacos地址
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.11.1:8848
2、启动类加@EnableDiscoveryClient 注解开启服务注册与发现功能
6、openFegin的使用
feign是一个声明式的HTTP客户端,他的目的就是让远程调用更加简单。
给远程服务发的是HTTP请求。
1、pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、启动类上加注解@EnableFeignClients(basePackages=“com.yxj.gulimall.member.feign”),
告诉spring这里面是一个远程调用客户端,member要调用的接口
如果不写包名默认找启动类包下面的feignClient
package com.yxj.gulimall.member;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 如果不写包名默认找启动类包下面的feignClient
*/
@SpringBootApplication
@MapperScan("com.yxj.gulimall.member.dao")
@EnableDiscoveryClient
@EnableFeignClients(basePackages="com.yxj.gulimall.member.feign")
public class GulimallMemberApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallMemberApplication.class, args);
}
}
3、 那么要调用什么东西呢?就是我们刚才写的优惠券的功能,
复制函数部分,在member的com.yxj.gulimall.member.feign包下新建类:
package com.yxj.gulimall.member.feign;
import com.yxj.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("gulimall-coupon") //告诉spring cloud这个接口是一个远程客户端,要调用coupon服务,再去调用coupon服务/coupon/coupon/member/list对应的方法
public interface CouponFeignService {
@RequestMapping("/coupon/coupon/member/list")
public R membercoupons();//得到一个R对象
}
7、 配置中心nacos
我们还可以用nacos作为配置中心。配置中心的意思是不在application.properties
等文件中配置了,而是放到nacos配置中心公用,这样无需每台机器都改。
1 引入配置中心依赖,放到common中
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2 在coupons项目中创建/src/main/resources/bootstrap.properties ,这个文件是
springboot里规定的,他优先级别application.properties高,nacos的配置内容优先于项目本地的配置内容。
6 但是修改肿么办?实际生产中不能重启应用。在coupon的控制层上加**@RefreshScope**
8、@RefreshScope 动态更新配置中心的配置
9、网关gateway
发送请求需要知道商品服务的地址,如果商品服务器有100服务器,1号掉线后,
还得改,所以需要网关动态地管理,他能从注册中心中实时地感知某个服务上
线还是下线。
请求也要加上询问权限,看用户有没有权限访问这个请求,也需要网关。
所以我们使用spring cloud的gateway组件做网关功能。
网关是请求浏览的入口,常用功能包括路由转发,权限校验,限流控制等。springcloud gateway取到了zuul网关。
三大核心概念:
Route:
发一个请求给网关,网关要将请求路由到指定的服务。路由有id,目的地uri,断言的集合,匹配了断言就能到达指定位置,
Predicate断言: 就是java里的断言函数,匹配请求里的任何信息,包括请求头等
Filter:过滤器请求和响应都可以被修改。
客户端发请求给服务端。中间有网关。先交给映射器,如果能处理就交给handler
处理,然后交给一系列filer,然后给指定的服务,再返回回来给客户端。
10、@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})的作用:让DataSourceAutoConfiguration相关配置不生效
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
public class GulimallGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallGatewayApplication.class, args);
}
}
11、配置网关路由示例
12、跨域问题
从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求
跨域
问题描述:已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。
(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
问题分析:这是一种跨域问题。访问的域名和端口和原来的请求不同,请求就会被限制
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。(ajax可以)
同源策略:是指协议,域名,端囗都要相同,其中有一个不同都会产生跨域;
跨域流程:
这个跨域请求的实现是通过预检请求实现的,先发送一个OPSTIONS探路,收到响应允许跨域后再发送真实请求
解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。
package com.yxj.gulimall.gateway.config;
@Configuration // gateway
public class GulimallCorsConfiguration {
@Bean // 添加过滤器
public CorsWebFilter corsWebFilter(){
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
13、mybvatis-plus 逻辑删除
修改product.entity.CategoryEntity实体类,添加上@TableLogic,表明使用逻辑删除:
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
14、阿里云上传
如果上传任务都交给gulimall-product来完
成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。
创建第三方模块
改进
15、统一异常处理
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
1 抽取一个异常处理类
@Slf4j
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
public R handleValidException(MethodArgumentNotValidException exception){
Map<String,String> map=new HashMap<>();
// 获取数据校验的错误结果
BindingResult bindingResult = exception.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> {
String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message);
});
log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
return R.error(400,"数据校验出现问题").put("data",map);
}
}
3 默认异常处理
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
return R.error(400,"数据校验出现问题");
}
16、@JsonInclude注解:去空字段
//如果放在类上边,那对这个类的全部属性起作用
@JsonInclude(JsonInclude.Include.NON_EMPTY) // 不为空时包含该注解
@TableField(exist = false)
private List<CategoryEntity> Children;
17、将集合反转
Collections.reverse(parentPath);
18、集合转数组
集合.toArray(new数组类型[0])
parentPath.toArray(new Long[0]);
public Long[] findCategoryPath(Long catelogId) {
List<Long> paths = new ArrayList<>();
List<Long> parentPath = findParentPath(catelogId, paths);
Collections.reverse(parentPath);
return parentPath.toArray(new Long[0]);
}
19、String.join(" ",strings)以指定字符串拼接集合
List<String> strings = new LinkedList<>();
strings.add("Java");strings.add("is");
strings.add("cool");
String message = String.join(" ", strings);
//message returned is: "Java is cool"
Set<String> strings = new LinkedHashSet<>();
strings.add("Java"); strings.add("is");
strings.add("very"); strings.add("cool");
String message = String.join("-", strings);
//message returned is: "Java-is-very-cool"
20、@RequestBody传输和接收时只要据模型是兼容的。双方服务无需使用同一个to
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
/**
* 1、CouponFeignService.saveSpuBounds(spuBoundTo);
* 1)、@RequestBody将这个对象转为json。
* 2)、找到gulimall-coupon服务,给/coupon/spubounds/save发送请求。
* 将上一步转的json放在请求体位置,发送请求;
* 3)、对方服务收到请求。请求体里有json数据。
* (@RequestBody SpuBoundsEntity spuBounds);将请求体的json转为SpuBoundsEntity;
* 只要json数据模型是兼容的。双方服务无需使用同一个to
* @param spuBoundTo
* @return
*/
@PostMapping("/coupon/spubounds/save")
R saveSpuBounds(@RequestBody SpuBoundTo spuBoundTo);
}
与上面接口中数据格式一致
/**
* 保存
*/
@PostMapping("/save")
//@RequiresPermissions("coupon:spubounds:save")
public R save(@RequestBody SpuBoundsEntity spuBounds){
spuBoundsService.save(spuBounds);
return R.ok();
}
21、设置mysql隔离级别为读未提交
– 设置隔离级别为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
– 查询隔离级别
SELECT @@SESSION.tx_isolation
22、设置主键id为输入
/**
* 商品id
*/
@TableId(type = IdType.INPUT)
private Long spuId;
未设置之前无法插入id,因为是自增的。会报错如下图:
23、feign调用的两种路径方案
1、直接请求原路径
2、请求gateway网关,让网关转发
24、不让事务回滚的方法
1、catch异常 不让异常抛出
2、todo
25、List转set的方法
HashSet<Long> idSet = new HashSet<>(list);
也可以用stream流的方式转换
26、List转Map的方法
27、lambda 表达式中使用的变量应该是最终的或有效的最终的
Variable used in lambda expression should be final or effectively final
28、Nginx相关笔记
nginx简介
nginx动静分离
动:反向代理,处理请求;负载均衡
静:静态文件服务器
28.1 静态HTTP服务器
首先,Nginx是一个HTTP服务器,可以将服务器上的静态文件(如HTML、图片)通过HTTP协议展现给客户端。
配置:
server {
listen 80; # 端口号
location / {
root /usr/share/nginx/html; # 静态文件路径
}
}
28.2、反向代理服务器
什么是反向代理?
客户端本来可以直接通过HTTP协议访问某网站应用服务器,如果网站管理员在中间加上一个Nginx,客户端请求Nginx,Nginx请求应用服务器,然后将结果返回给客户端,此时Nginx就是反向代理服务器。
配置:
server {
listen 80;
location / {
proxy_pass http://192.168.0.112:8080; # 应用服务器HTTP地址
}
}
既然服务器可以直接HTTP访问,为什么要在中间加上一个反向代理,不是多此一举吗?反向代理有什么作用?继续往下看,下面的负载均衡、虚拟主机,都基于反向代理实现,当然反向代理的功能也不仅仅是这些。
28.33、负载均衡
当网站访问量非常大,也摊上事儿了。因为网站越来越慢,一台服务器已经不够用了。于是将相同的应用部署在多台服务器上,将大量用户的请求分配给多台机器处理。同时带来的好处是,其中一台服务器万一挂了,只要还有其他服务器正常运行,就不会影响用户使用。
Nginx可以通过反向代理来实现负载均衡。
配置:
upstream myweb {
server 192.168.0.111:8080; # 应用服务器1
server 192.168.0.112:8080; # 应用服务器2
}
server {
listen 80;
location / {
proxy_pass http://myweb;
}
}
28.4、虚拟主机
的网站访问量大,需要负载均衡。然而并不是所有网站都如此出色,有的网站,由于访问量太小,需要节省成本,将多个网站部署在同一台服务器上。
例如将www.aaa.com和www.bbb.com两个网站部署在同一台服务器上,两个域名解析到同一个IP地址,但是用户通过两个域名却可以打开两个完全不同的网站,互相不影响,就像访问两个服务器一样,所以叫两个虚拟主机。
配置:
server {
listen 80 default_server;
server_name _;
return 444; # 过滤其他域名的请求,返回444状态码
}
server {
listen 80;
server_name www.aaa.com; # www.aaa.com域名
location / {
proxy_pass http://localhost:8080; # 对应端口号8080
}
}
server {
listen 80;
server_name www.bbb.com; # www.bbb.com域名
location / {
proxy_pass http://localhost:8081; # 对应端口号8081
}
}
在服务器8080和8081分别开了一个应用,客户端通过不同的域名访问,根据server_name可以反向代理到对应的应用服务器。
虚拟主机的原理是通过HTTP请求头中的Host是否匹配server_name来实现的,有兴趣的同学可以研究一下HTTP协议。
另外,server_name配置还可以过滤有人恶意将某些域名指向你的主机服务器。
28.5、FastCGI
Nginx本身不支持PHP等语言,但是它可以通过FastCGI来将请求扔给某些语言或框架处理(例如PHP、Python、Perl)。
29、JMeter性能压测
影响性能的考虑要素
数据库,应用程序,中间件(tomcat,nginx) 、网络和操作系统等方面
IO密集型:
网络请求流量大,磁盘读写,数据库读写数据,redis读写
解决:增加内存,增加磁盘,使用缓存,提高网卡传输效率
CPU密集型:
程序中 对大量数据的排序,遍历,以及线程的切换,都会占用cpu
对此我们可以增加cpu,优化程序
性能压测-性能监控-jvisualvm使用
我的链接:jvisualvm的基本使用
结论:中间件越多,性能损失越大,大多都损失到网络交互了;
业务代码:尽量减少对数据库的请求查询次数,然后做数据处理尽量用代码处理,在内存中处理更快
数据库: 读写分离,设置索引等,mysql优化一整套
页面展示:模板的渲染速度(cpu 内存,最重要缓存),需要用缓存来提高效率
静态资源:(tomcat还要分一些线程来处理静态资源,吞吐量下降很多,大多请求静态资源占用了后台好多线程)
我们就用nginx做静态文件的管理,这样就把压力分担到nginx上了
设置各个的服务的堆内存:尽量设置大 -Xmx1024 -Xms1024 -Xmn512
堆满了会报oom
30、缓存和分布式缓存
缓存分析:
本地缓存,我们可以定义一个map来缓存本地数据,但是这只符合单机情况,如果多台机器服务 负载的情况下,那么map缓存不能做到同步多服务,
所以在我们分布式服务中,我们需要分布式缓存,来解决这个问题。那么我们就可以应用第三方组件redis,来专门做分布式的缓存
31、redis缓存
想看redis在springboot中的实现源码,去看RedisAutoConfiguration.class 和RedisTemplate 类
引入依赖
<!-- 引入redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置application.xml
redis:
host: 192.168.56.10
port: 6379
测试代码
自动注入RedisTemplate
String vue = redisTemplate.opsForValue().get("key");
if (!StringUtils.isEmpty(vue)){
redisTemplate.opsForValue().set("key", "123");
return "redis中的值:"+"123";
}
32、缓存穿透、雪崩、击穿
总结:
所以对这些高频数据我们加入缓存时,一定要考虑并解决 缓存穿透,雪崩,击穿的问题,否则,大并发下我们数据库时刻可能会崩溃,
并且,在解决缓存击穿的问题上,我们分布式的项目,我们需要引入分布式锁来解决,缓存击穿的问题,本地锁已经不能在分布式中使用了,使用分布式锁,那么就往下看。。。
33、分布式锁–Jedis/lettuce+lua 原子性 来实现分布式锁
34、分布式锁–Redisson
由于用jedis/lettuce+lua 实现分布式锁,还是比较麻烦,因为锁的自动加时的问题也不太好解决,所以我们直接使用Redisson,
Redisson分布式锁其实是锁的一个集合,类似我们的本地锁 有lock锁,sych锁,等等,分布锁,其实也是分布式下,这些锁的统称,其用法和我们juc下的本地锁用法类似
(1) 环境搭建
导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
这个用作连续,后面可以使用redisson-spring-boot-starter
开启配置
@Configuration
public class MyRedisConfig {
@Value("${ipAddr}")
private String ipAddr;
// redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() {
Config config = new Config();
// 创建单例模式的配置
config.useSingleServer().setAddress("redis://" + ipAddr + ":6379");
return Redisson.create(config);
}
}
(2) 可重入锁(Reentrant Lock)
分布式锁:github.com/redisson/redisson/wiki/8.-分布式锁和同步器
A调用B。AB都需要同一锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁
// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
锁的续期:大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
// 加锁以后10秒钟自动解锁,看门狗不续命
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间/3是定时任务周期;
最佳实战:自己指定锁时间,时间长点即可
(3) 读写锁(ReadWriteLock)
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
(4) 信号量(Semaphore)
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
@GetMapping("/park")
@ResponseBody
public String park() {
RSemaphore park = redissonClient.getSemaphore("park");
try {
park.acquire(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "停进2";
}
@GetMapping("/go")
@ResponseBody
public String go() {
RSemaphore park = redissonClient.getSemaphore("park");
park.release(2);
return "开走2";
}
(5) 闭锁(CountDownLatch)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
以下代码只有offLatch()被调用5次后 setLatch()才能继续执行
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
35、缓存和数据库一致性
双写模式:写数据库后,写缓存
问题:并发时,2写进入,写完DB后都写缓存。有暂时的脏数据
失效模式:写完数据库后,删缓存
问题:还没存入数据库呢,线程2又读到旧的DB了
解决:缓存设置过期时间,定期更新
解决:写数据写时,加分布式的读写锁。
解决方案:
如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
总结:
我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
我们不应该过度设计,增加系统的复杂性
遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
36、SpringCache
每次都那样写缓存太麻烦了,spring从3.1开始定义了Cache、CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发
Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等
每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
1、确定方法需要缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
1) 配置
依赖
<dependency>
<groupId>org.springframework.b oot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
指定缓存类型并在主配置类上加上注解@EnableCaching
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
@Configuration
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration( CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//设置配置文件中的各项配置,如过期时间
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;
}
}
2) 缓存自动配置
// 缓存自动配置源码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })
@Import({ CacheConfigurationImportSelector.class, // 看导入什么CacheConfiguration
CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })
public class CacheAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CacheManagerCustomizers cacheManagerCustomizers(ObjectProvider<CacheManagerCustomizer<?>> customizers) {
return new CacheManagerCustomizers(customizers.orderedStream().collect(Collectors.toList()));
}
@Bean
public CacheManagerValidator cacheAutoConfigurationValidator(CacheProperties cacheProperties,
ObjectProvider<CacheManager> cacheManager) {
return new CacheManagerValidator(cacheProperties, cacheManager);
}
@ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
@ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
static class CacheManagerEntityManagerFactoryDependsOnPostProcessor
extends EntityManagerFactoryDependsOnPostProcessor {
CacheManagerEntityManagerFactoryDependsOnPostProcessor() {
super("cacheManager");
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
@Bean // 放入缓存管理器
RedisCacheManager cacheManager(CacheProperties cacheProperties,
CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}
3) 缓存使用@Cacheable@CacheEvict
第一个方法存放缓存,第二个方法清空缓存
// 调用该方法时会将结果缓存,缓存名为category,key为方法名
// sync表示该方法的缓存被读取时会加锁 // value等同于cacheNames // key如果是字符串"''"
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
return getCategoriesDb();
}
//调用该方法会删除缓存category下的所有cache,如果要删除某个具体,用key="''"
@Override
@CacheEvict(value = {"category"},allEntries = true)
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())) {
categoryBrandRelationService.updateCategory(category);
}
}
如果要清空多个缓存,用@Caching(evict={@CacheEvict(value="")})
4) SpringCache原理与不足
37、线程池
1 线程池的创建
使用 Executors 工具类(Executors.newFixedThreadPool(10))
直接创建(new ThreadPoolExecutor)
spring下使用ThreadPoolTaskExecutor类:
2 线程池参数
corePoolSize:核心线程数。线程池创建好就准备就绪的线程数量,即使空闲也不会释放,除非自主设置
maximumPoolSize:最大线程数量。并发情况下控制资源
keepAliveTime:存活时间。如果线程的空闲时间大于该时间,则释放 maximumPoolSize - corePoolSize 数量的线程
unit:时间单位
BlockingQueue < Runnable > workQueue:阻塞队列。如果任务有很多,就会将目前多的任务放在队列里,
线程空闲则会去队列里取新的任务
threadFactory:线程的创建工厂
RejectedExecutionHandler handler:拒绝策略。如果队列满了,则按照配置的拒绝策略进行执行
3 线程池工作流程
线程池创建,准备好 core 数量的核心线程,准备接受任务
core 满了,就会将再进来的任务放在阻塞队列里。空闲的core 会自己去阻塞队列里获取任务执行
阻塞队列满了,就会直接开新线程执行,最大只能有max指定数量的线程
max满了就会用设置的RejectedExecutionHandler拒绝任务
max都执行完成后,有空闲线程,在指定的keepAliveTime时间后,释放maximumPoolSize - corePoolSize 数量的线程
例子:
一个线程池,core = 7,max = 20,queue = 50,假设进入100并发,是怎样分配?
7个请求会立即执行
50个请求会进入队列
线程池再开13个线程处理请求
剩下的30个请求根据配置的拒绝策略进行执行
4 线程池种类
Executors.newFixedThreadPool(n):固定大小,当core = max时,所有线程都不可回收
Executors.newCachedThreadPool():core是0,所有线程都可以进行回收
Executors.newSingleThreadScheduledExecutor():单线程的线程池,线程从队列里获取任务,挨个执行
Executors.newScheduledThreadPool(int corePoolSize):定时任务的线程池
38、CompletableFuture异步编排
创建异步对象
CompletableFuture 提供了四个静态方法来创建一个异步操作。
runAsync方法不支持返回值。
supplyAsync可以支持返回值。
static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。
计算完成时回调方法:当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action);
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor);
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn);
whenComplete可以处理正常和异常的计算结果,exceptionally处理异常情况。BiConsumer<? super T,? super Throwable>可以定义处理业务
whenComplete 和 whenCompleteAsync 的区别:
whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
handle 方法
handle 是执行任务完成时对结果的处理。
handle 是在任务完成后再执行,还可以处理异常的任务。
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
线程串行化方法
thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
thenAccept方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
thenRun方法:只要上面的任务执行完成,就开始执行thenRun,只是处理完任务后,执行 thenRun的后续操作
带有Async默认是异步执行的。这里所谓的异步指的是不在当前线程内执行。
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);
两任务组合 - 都要完成
详解
public <U,V> CompletableFuture thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor);
public CompletableFuture thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action);
public CompletableFuture thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action);
public CompletableFuture thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor);
public CompletableFuture runAfterBoth(CompletionStage<?> other, Runnable action);
public CompletableFuture runAfterBothAsync(CompletionStage<?> other, Runnable action);
public CompletableFuture runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor);
thenCombine:组合两个future,获取前两个future的返回结果,并返回当前任务的返回值
thenAcceptBoth:组合两个future,获取前两个future任务的返回结果,然后处理任务,没有返回值。
runAfterBoth:组合两个future,不需要获取之前任务future的结果,只需两个future处理完任务后,处理该任务。
两个任务 - 一个完成
applyToEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务并有新的返回值。
acceptEither: 两个任务有一个执行完成, 获取它的返回值, 处理任务, 没有新的返回值。
runAfterEither: 两个任务有一个执行完成, 不需要获取 future 的结果, 处理任务, 也没有返回值。
多任务组合
//allOf: 等待所有任务完成
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
return andTree(cfs, 0, cfs.length - 1);
}
//anyOf: 只要有一个任务完成
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {
return orTree(cfs, 0, cfs.length - 1);
}
39、mybatis自定义结果集resultMap封装嵌套属性
只要有嵌套属性一定要封装自定义结果集
40、自定义线程池
41、异步编程使用
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity info = this.getById(skuId);
skuItemVo.setInfo(info);
return info;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取spu的销售属性组合
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesc(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//5、获取spu的规格参数信息
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
//2、sku图片信息 pms_sku_images
List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(imagesEntities);
}, executor);
//等待所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
return skuItemVo;
}
42、配置方式试图解析
43、MD5
@Test
public void contextLoads() {
String s = DigestUtils.md5Hex("123456");
System.out.println(s);
// 盐值加密
System.out.println(Md5Crypt.md5Crypt("123456".getBytes()));
System.out.println(Md5Crypt.md5Crypt("123456".getBytes(), "$1$qqqqqqqq"));
// Spring 盐值加密
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
//$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S
//$2a$10$cR3lis5HQQsQSSh8/c3L3ujIILXkVYmlw28vLA39xz4mHDN/NBVUi
String encode = bCryptPasswordEncoder.encode("123456");
boolean matches = bCryptPasswordEncoder.matches("123456", "$2a$10$GT0TjB5YK5Vx77Y.2N7hkuYZtYAjZjMlE6NWGE2Aar/7pk/Rmhf8S");
System.out.println(encode + "==>" + matches);
}
44、社交登录
45、分布式session
(1) session 原理
session存储在服务端,jsessionId存在客户端,每次通过jsessionid取出保存的数据
问题:但是正常情况下session不可跨域,它有自己的作用范围
(2) 分布式session解决方案
session要能在不同服务和同服务的集群的共享
redis统一存储
最终的选择方案,把session放到redis中
46、SpringSession整合redis
https://spring.io/projects/spring-session-data-redis
https://docs.spring.io/spring-session/docs/2.4.2/reference/html5/#modules
通过SpringSession修改session的作用域
会员服务、订单服务、商品服务,都是去redis里存储session
- 环境搭建
Oauth导入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改配置
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=192.168.56.10
添加注解 @EnableRedisHttpSession
创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
public class GulimallAuthServerApplication {
但是现在还有一些问题:
序列化的问题
cookie的domain的问题
2) 扩大session作用域
由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化
并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com
@Configuration
public class GulimallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("GULISESSIONID"); // cookie的键
serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
47、分布式登录总结
登录url:http://auth.gulimall.com/login.html
(注意是url,不是页面。)
判断session中是否有user对象
没有user对象,渲染login.html页面
用户输入账号密码后发送给 url:auth.gulimall.com/login
根据表单传过来的VO对象,远程调用memberFeignService验证密码
如果验证失败,取出远程调用返回的错误信息,放到新的请求域,重定向到登录url
如果验证成功,远程服务就返回了对应的MemberRespVo对象,
然后放到分布式redis-session中,key为"loginUser",重定向到首页gulimall.com,
同时也会带着的GULISESSIONID
重定向到非auth项目后,先经过拦截器看session里有没有loginUser对象
有,放到静态threadLocal中,这样就可以操作本地内存,无需远程调用session
没有,重定向到登录页
有user对象,代表登录过了,重定向到首页,session数据还依靠sessionID持有着
额外说明:
问题1:我们有sessionId不就可以了吗?为什么还要在session中放到User对象?
为了其他服务可以根据这个user查数据库,只有session的话不能再次找到登录session的用户
问题2:threadlocal的作用?
他是为了放到当前session的线程里,threadlocal就是这个作用,随着线程创建和消亡。把threadlocal定义为static的,这样当前会话的线程中任何代码地方都可以获取到。如果只是在session中的话,一是每次还得去redis查询,二是去调用service还得传入session参数,多麻烦啊
问题3:cookie怎么回事?不是在config中定义了cookie的key和序列化器?
序列化器没什么好讲的,就是为了易读和来回转换。而cookie的key其实是无所谓的,只要两个项目里的key相同,然后访问同一个域名都带着该cookie即可。
48、单点登录
上面解决了同域名的session问题,但如果taobao.com和tianmao.com这种不同的域名也想共享session呢?
去百度了解下:https://www.jianshu.com/p/75edcc05acfd
最终解决方案:都去中央认证器
spring session已经解决不了不同域名的问题了。无法扩大域名
sso思路
记住一个核心思想:建议一个公共的登陆点server,他登录了代表这个集团的产品就登录过了
49、登录拦截器
通用登录拦截器
因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截
先注入拦截器HandlerInterceptor组件
在config中实现WebMvcConfigurer接口.addInterceptor()方法
拦截器和认证器的关系我在前面认证模块讲过,可以翻看,这里不赘述了
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();
// 这个请求直接放行
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if(match){
return true;
}
// 获取session
HttpSession session = request.getSession();
// 获取登录用户
MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(memberRespVo != null){
threadLocal.set(memberRespVo);
return true;
}else{
// 没登陆就去登录
session.setAttribute("msg", AuthServerConstant.NOT_LOGIN);
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**");
}
}
加上ThreadLocal共享数据,是为了登录后把用户放到本地内存,而不是每次都去远程session里查
在auth-server中登录成功后会把会话设置到session中
MemberRespVo data = login.getData("data",new TypeReference<MemberRespVo>);
session.setAttribute(AuthServerConstant.LOGIN_USER,data);
50、后续笔记
51、接口防刷
/**
* 发送验证码,接口防刷
* @param phone
* @return
*/
@GetMapping("/sendCode")
public R sendCode(@RequestParam("phone") String phone){
//1.接口防刷
String key = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;
String code = redisTemplate.opsForValue().get(key);
if(StringUtils.isNotEmpty(code)){
//code的格式为 5位随机数+当前时间戳
String time = code.split("-")[1];
Long l = System.currentTimeMillis()-Long.parseLong(time);
if(l < 6*1000){
throw new RuntimeException("验证码发送过于频繁,请稍后再发");
}
}
//验证码的再次校验 redis key:phone value:code
String uuid = UUID.randomUUID().toString().substring(0,5);
String value = uuid + "_" +System.currentTimeMillis();
//验证码存缓存中
redisTemplate.opsForValue().set(key,value,10, TimeUnit.MINUTES);
//调用短信发送服务 发送验证码
thirdPartFeignService.sendCode(phone,code);
return R.ok();
}
52、Spring Boot 整合 定时任务
/**
* 1:定时任务
* 1:@EnableScheduling
* 2:@Scheduled(cron = "* /5 * * ? * 1")
*
* 2:异步任务:
* 1:@EnableAsync
* 2:@Async
*
* 3:使用 定时任务 + 异步任务 ,保证定时任务不阻塞。
*/
@Slf4j
@Component
// 开启异步任务
//@EnableAsync
// 开启定时任务
//@EnableScheduling
public class HelloSchedule {
/**
* 在Spring中 只允许6位 [* * * ? * 1] : 每周一 每秒执行一次
* [* /5 * * ? * 1] : 每周一 每5秒执行一次
* 1.定时任务不应阻塞 [默认是阻塞的]
* 2.定时任务线程池 spring.task.scheduling.pool.size=5
* 3.让定时任务异步执行
* 1:SpringBoot + Schedule 线程池。(不太好使))
* 2:启动异步任务:( CompletableFuture.runAsync(() -> {} )
* 3:开启异步任务:@Async。
*/
@Async
@Scheduled(cron = "*/5 * * ? * 1")
public void hello() {
log.info("i love you...");
CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
});
}
}
spring:
task:
# Async
execution:
pool:
core-size: 5
max-size: 50
# Schedule 线程池(不太好使)
# scheduling:
# pool:
# size: 5