谷粒商城笔记

文章目录

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的配置内容优先于项目本地的配置内容。

#gulimall-coupon.properties6 但是修改肿么办?实际生产中不能重启应用。在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

  1. 环境搭建

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

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、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值