目录
设置一个服务的堆内存上限:VM options:-Xmx100m
RejectedExecutionHandler handler:
RedirectAttributes.addFlashAttribute():
前置小知识
分布式:
是一种工作方式,不同业务分布在不同地方
集群:
只要是一堆机器就是集群
配置中心:
每一个服务可能都有大量配置,每个服务可能部署到多台机器上,我们经常需要修改服务的配置,我们可以让每台机器上的服务自动获取配置中心的配置。
服务熔断、服务降级:
服务于服务之前存在多层调用,如果多层调用的某一步出现问题则会导致整个系统阻塞,其他请求再发送过来就会导致请求挤压然后雪崩。这时就可以服务熔断:当某个服务经常访问失败达到某个阈值,就会开启短路保护机制,让后来的请求不去调用这个服务而是返回默认值。或者可以服务降级:系统高峰期服务紧张时可以让某些服务服务简单处理或者不处理。
使用人人逆向工程生成基本的代码
Common模块每一个微服务公共的依赖,bean,工具类等其他模块代表每一个微服务
使用nacos作为服务注册中心和配置中心
步骤:
1、为微服务配置注册中心的地址和服务名称
2、在每个服务的启动类上面加上注解@EnableDiscoveryClient
就等于将这个服务注册到注册中心了,新版的SpringBoot不需要加这个注解
http://127.0.0.1:8848/nacos进入到nacos可视化界面
想要远程调用其他服务的步骤:
1、引入open-fegin依赖
2、编写一个接口
告诉springcloud这个接口需要调用远程服务,在这个接口上面写上注解@FeginClient(“服务名”)
3、开启远程调用功能:
在启动类上边加上注解@EnableFeginClients(basePackages = “接口所在包的全包名”),这样服务启动就会扫描所有这个包下面的所有标了@FeginClient的接口。
4、远程服务传输对象注意:
如果一个服务在map中存了一个对象,然后把这个map通过json传给另一个服务,那么这个想要被传的对象会被转成map;如r.put(“data”,person);然后将r传给另一个服务,另一个服务get(“data”)得到的不是Person而是一个map。
解决方法:先通过alibaba的fastjson将得到的map转换成json字符串,再将字符串转换成对象。
使用nacos作为配置中心的步骤
1、引入依赖
2、创建一个bootstrap.properties文件
在里面配置spring.application.name=gulimall-coupon
Spring.cloud.nacos.config.server-addr=127.0.0.1:8848
3、打开nacos那个可视化页面,添加一个数据集(Data Id)gumimall-coupon.properties。默认规则,应用名.properties
4、添加想要的配置
5、动态获取配置:使用@RefreshScope标在Controller类上面,使用@Value(${配置项的名})获取配置中的值。
如果配置中心和当前应用的配置文件中有相同的项应该优先使用配置中心的配置。
配置中心的命名空间
目的:在开发、测试、生成等不同场景使用不同的配置实现环境隔离,也可以基于不同的服务进行隔离,不同服务使用不同配置。
默认的命名空间:为public,意思是在bootstrap.properties配置中如果不指明命名空间的话就会使用public中的环境配置。
如何指明命名空间:先要在nacos可视化页面的配置中心创建命名空间,就会得到一串标识命名空间的代码,然后在bootstrap.properties配置中配置上这串代码spring.cloud.nacos.config.namespace=@#$%^&*&^%$#@;
配置集分组:相当于先分成不同命名空间之后,在一个命名空间下又分为一些分组对应不同环境。
同时加载多个配置集:微服务的任何配置信息,任何配置文件都可以在配置中心中,只需要在bootstrap文件中说明加载配置中心的哪些配置文件就可以了。
网关gateway:
使用流程:
请求到达网关网关会进行断言,判断是否符合某个路由规则,如果符合了就将这个请求路由到指定地方,去到这个指定地方的路上会经过一系列的filter进行过滤。
这里的lb表示负载均衡到renren-fast服务,Path表示满足这个路径的请求都会负载均衡。下面这个filter表示路径重写。
这一系列配置之后就可以实现下面的路由:
跨域问题:
跨域是指浏览器默认不能执行其他网站的脚本,是由浏览器同源策略产生的,同源是指协议、域名、端口号都要相等,不相等就是不同源就是跨域了。
前端和后端运行在不同的端口下,所以当前端发送请求给后端的时候就出现了跨域问题。
跨域的流程如下:
解决方法:
在网关中统一配置就行了
文件存储:
因为这个项目是分布式的,一个微服务可能是一个集群,不可能在每一个微服务的每一个服务器下都保存文件,所以我们使用阿里云云存储。
阿里云OSS:将文件存储到阿里云,存储模式如下
前端发送上传请求后先从gulimall-third-party服务中调用/oss/policy请求对应的方法获取签名,然后连带要上传的文件一起发送给阿里云。
存储到阿里云的步骤:
1、引入阿里cloud的starter依赖
2、在阿里云创建用户获取accesskey
3、并给这个用户授权
4、在application中配置accesskey
5、注入OSSClient对象调用putObject方法上传文件到阿里云(mall-gy是bucket(存储空间))
统一的异常处理
使用@ControllerAdvice
1、编写异常处理类,使用@ControllerAdvice标记类。
2、使用@ExceptionHandler标注方法可以处理的异常。
VO和TO:
VO:VO实体使用来返回给前端的对象,有时候只需要返回entity的某些属性或者需要返回entity中没有的属性,所以要创建VO类。
TO:是服务与服务之间传输对象时用的实体类,只声明两边服务需要用到的属性。由于TO两个服务都要使用所以可以声明在common模块里面。
设置一个服务的堆内存上限:VM options:-Xmx100m
ELASTIC SEARCH
基本概念
Mysql用于持久化存储,es用于检索
Index索引:相当于mysql的db
Type类型:相当于mysql的表,每一种类型的数据放在一起
Document文档:保存在某个Index下的某种type当中的一个一个数据document,就像mysql当中的一行数据。
Post和put请求
Es中put请求会指定数据的id,会更新version,post请求可以不指定id,自动生成唯一标识。这两种请求如果指定了id,第一次请求会create,后面的请求如果id已存在就是update并且更新version了。Post请求如果不指定id自动生成的话那么每一次请求就都是新增了。
带有下划线开头的,称为元数据,反映了当前的基本信息。
{ "_index": "customer", 表明该数据在哪个数据库下; "_type": "external", 表明该数据在哪个类型下; "_id": "1", 表明被保存数据的id; "_version": 1, 被保存数据的版本 "result": "created", 这里是创建了一条数据,如果重新put一条数据,则该状态会变为updated,并且版本号也会发生变化。 "_shards": { "total": 2, "successful": 1, "failed": 0 }, "_seq_no": 0, "_primary_term": 1 } |
更新文档_update
POST customer/externel/1/_update
在请求的后面加上_update,这时的更新操作就会判断更新的内容和原来的内容是否相等,如果相等则什么操作都不会执行。而如果不加_update的话即使内容相等也会改变版本号。
注意:如果加上了_update则内容需要这么写,将内容使用doc括起来。
{ "doc":{ "name":"111" } } |
ES的批量操作——bulk
匹配导入数据
POST http://192.168.56.10:9200/customer/external/_bulk
两行为一个整体
{"index":{"_id":"1"}}
{"name":"a"}
{"index":{"_id":"2"}}
{"name":"b"}
注意格式json和text均不可,不能用postman了要去kibana里Dev Tools,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。
Es进阶检索语法
参考:从前慢-谷粒商城篇章4_unique_perfect的博客-CSDN博客_谷粒商城 从前慢
使用java操作ELASTIC SEARCH
1、导入依赖
2、创建配置类,配置es的地址端口创建一个RestHighLevelClient注入容器中
3、参照api操作
使用nginx反向代理:
步骤:
windows设置域名映射:
C:\Windows\System32\drivers\etc下的hosts文件
这样设置之后访问mymall.com就相当于与访问了虚拟机。
然后修改usr/local/nginx里面的配置文件
这里的意思是nginx监听80端口下的mymall.com域名,nginx帮我们代理到本机的10002端口,也就是访问商品服务。
Nginx+网关:
而我们最终使用的方案是nginx反向代理到网关再由网关路由到指定服务。
修改nginx/conf/nginx.conf,将upstream映射到我们的网关服务
upstream mymall{
server 192.168.42.30:88;
}
修改nginx/conf/conf.d/mymall.conf,接收到mymall.com的访问后,如果是/,转交给指定的upstream,由于nginx的转发会丢失host头,造成网关不知道原host,所以我们添加头信息
location / {
proxy_pass http://mymall;
proxy_set_header Host $host;
}
网关配置:
Nginx动静分离:
由于动态资源和静态资源目前都处于服务端,所以为了减轻服务器压力,我们将js、css、img等静态资源放置在Nginx端,以减轻服务器压力。(将静态资源存放在usr/local/nginx/static下) 我们可以在服务端所有访问静态资源的请求前加上一个static,然后再nginx中配置拦截static开口的请求让它不再反向代理到网关而是直接访问nginx本地的资源。
使用JMeter做压力测试:
运行bin目录下的jmeter.bar
创建线程组:
200个用户在一秒内每个用户发100个请求:
给线程组添加取样器和监听器:
Redis作缓存:
引入依赖,配置,spring.redis.host = xxxxx
使用springboot自动配置的SpringRedisTemplate操作redis:
SpringRedisTemplate.opsvalue.set(“gy”,”niubi”);
在redis当中最好存储json字符串,这样可以跨平台跨语言使用了。我们可以使用阿里的fastjson实现json字符串和java对象之间的转换。
缓存问题:
缓存穿透:
用户发送大量查询不存在的商品的请求,这时缓存里面没有这件商品的信息,就会都去查数据库造成数据库巨大的压力,解决方法就是把null值缓存起来,并设置一个较短的过期时间。
缓存雪崩:
缓存中的大批数据在同一时刻都过期了,这时如果来了大批查询这些数据的请求就会雪崩。解决方法是给数据创建缓存的时候可以加上一个随机的时间避免他们同时过期。
缓存击穿:
有一个热门的商品,在他的缓存刚刚过期的时候就有大量的请求过来,这时这些请求都去查数据库了。解决方法:加锁,第一个请求查完数据库并缓存,后面得请求就可以从缓存中获取信息了。
Redis分布式锁:
使用setIfAbsent();api来占锁,占到锁再执行相应的代码,没抢到锁就进行自旋重试。执行完之后要把锁删除,占锁的同时要设置过期时间,防止某一个线程获取到锁了断电了,这时就会一直占着锁不放。
删锁的时候有可能业务执行的时间比较长,在删锁之前锁就已经过期了,其他线程就进来了,此时你再删锁删的就是其他线程的锁了,所以在set锁的时候要把值设为一个uuid,然后删的时候判断这个锁是不是自己的再删。但是获取锁的值的过程也需要时间,所以可能redis返回值的时候刚好锁过期了其他线程又进来了,这时redis当中的锁已经是其他线程的了,然而返回得到的结果还是自己线程的uuid所以判断uuid的时候为true于是执行删锁操作就把别人的锁给删了。解决方法:使用lua脚本实现原子删锁。
Redisson:
使用redisson来实现分布式锁
加的锁有默认30秒得过期时间,如果业务执行时间过长看门狗会自动续期,每10s续一次。
使用步骤:
- 导入依赖
- 注入RedissonClient
- 获取锁,Rlock lock = redissonClient.getLock(“lock”);只要锁名一样就是同一把锁。
- 加锁lock.lock();解锁lock.unLock();
- 加锁时如果锁已被其他线程获取则会不断尝试获取锁,相当于自旋。也可以在加锁的时候指定过期时间,但是加了指定过期时间之后锁就不会自动续期了。
读写锁:
读数据没必要加锁,可以有多个线程同时读数据,又为了保证写操作没有完成的时候不让读到数据,所以使用读写锁。
写完后才能写,写完后才能读,
读完后才能写,读没完可以读。
RReadWriteLock lock = redissonClient.getReadWriteLock();
RLock wLock = lock.writeLock();
RLock rLock = lock.readLock();
然后使用rLock.lock();给读数据加锁,wLock.lock();给写数据加锁
信号量:
相当于在redis有一些信号,然后可以消费减少信号,也可以生成增加信号。
RSemaphore s = redissonClient.getSemaphore(“a”);//获取名为a的信号量。
s.acquire();消费掉s中的一个信号量,如果没有信号量则等待。
s.release();增加s中一个信号量。
s.tryAcquire();返回值为boolean,有信号量则消费,没有就算了
闭锁:
RCountDownLatch cd= redissonClient.getCountDownLatch(“a”);
cd.trySetCount(5);//使这个五减为0后接着执行代码
cd.await();//等待减为0再接着执行后面的代码
cd.countDown();//使那个5减1
场景:匹配到10个人了再开游戏,屋里人走完了再锁门。。。。
SpringCache简化缓存开发:
大部分加锁的场景可以用springcache来实现
使用步骤:
1、引入依赖spring-boot-starter-cache、
Spring-boot-starter-cache-data-redis
2、在启动类上标上注解@EnableCaching
3、配置
下面四条含义:
缓存存活时间(ms)
是否开启缓存前缀(默认为分区名字)
设置缓存前缀(设置后替换默认值)
是否存储null值(开启可防止缓存穿透)
4、在需要缓存的方法上标上注解@CacheAble(“存的分区的名字”),默认行为:执行标了注解的方法之前它会先到缓存中查看是否有数据,如果有就不用调用这个方法了,没有继续执行方法并在缓存中存入数据。
5、自定义
创建配置类完成以下操作
- 指定生成的缓存的key的名称
- 指定缓存的数据的存活时间
- 将数据保存为json格式
@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;
}
@Cacheable:加缓存
缓存名为category,key使用的是SpEL表达式,这里为方法名,sync表示该方法的缓存被读取时会加本地锁,value等同于cacheNames,key如果是字符串写法为"'name'"
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
@CacheEvict:失效模式保证缓存一致性
标了这个注解的方法表示执行完这个方法会删除category分区下的所有缓存,如果要删除某个具体,用key="''"
@CacheEvict(value = {"category"},allEntries = true)
@Caching:可以组合多个@CacheEvict
推荐使用这个而不是allEntries = true
@Caching(evict={@CacheEvict(value="a",key = “’xxx’”),
@CacheEvict(value="a",key = “’yyy’”)})
表示执行这个方法会删除a分区中key为xxx和yyy的两个缓存
Spring Cache总结:
读模式的三个问题:基本能解决
缓存穿透:配置文件中配置允许存null值解决
缓存雪崩: 加随机时间
缓存击穿:sync = true加个本地锁基本上能解决了
写模式缓存与数据库一致性的问题:
常规数据:(读多写少,即时性,一致性要求不是那么高的)完全可以使用springcache。
特殊数据:特殊处理使用redisson分布式锁
线程池:
ThreadPoolExecutor executor = new ThreadPoolExecutor();
七大参数:
corePoolSize:
核心线程数;线程池创建好以后就等待执行任务的线程数量,相当于new了几个Thread。
maximumPoolSize:
最大线程数量;控制资源
keepAliveTime:
非核心线程的存活时间,如果当前线程数量大于核心线程数,空闲时间超时的非核心线程就会死亡。
unit:
时间单位。
BlockingQueue<Runnable> workQueue:
阻塞队列;如果任务有很多就会把目前多的任务存到队列里面,只要有线程空闲就会从队列中取出任务执行。(可使用LinkedBlockingDeque<>作为阻塞队列,默认大小为Integer的最大值)
threadFactory:
线程创建的工厂。(Executors.defaultThreadFactory())
RejectedExecutionHandler handler:
拒绝策略;如果队列满了并且达到最大线程数了,按照我们指定的拒绝策略拒绝任务。(new ThreadPoolExecutor.AbortPolicy())
执行过程:
先执行核心,再放入队列,之后开线程,最后执行拒绝策略
为什么使用线程池:
异步
复习mybatis编写复杂对象的resultmap
RedirectAttributes.addFlashAttribute():
模拟的是HttpSession存储数据,可以在不同页面之间传递数据,但是数据只能取一次。而RedirectAttributes.addAttribute()是在url后面拼接要传递的数据。addAttribute()的value貌似不能是对象。
复习session:
一个客户端第一次发送请求给服务端,服务端创建一个session并响应给客户端,客户端以cookie的形式把sessionID保存下来。这个客户端接下来发送的请求会把sessionID一并发给服务端,服务端获取到请求就能根据sessionID判断出这个请求是以前发过请求的一个客户端发送的,从而就能区分不同客户端的请求。
客户端关闭则清除会话cookie,一个客户端一个session,比如两个不同的浏览器发送请求用的是两个不同的session。默认30分钟没有发送请求,session就会销毁。
Session共享问题:
不同服务如何都获取用户的登录信息?
Session是存在当前服务器的,但是这是一个分布式的项目,且一个服务可能有多个服务器,不同服务之间不能共享session。
解决方法:用户在第一次访问一个服务的时候生成的session将它保存在redis当中,生成的jsessionid返回给客户端的时候要设置作用域不能仅仅只是当前服务,而是放大作用域,使访问该项目任何服务都会带上jsessionid,然后从redis当中取到想要的内容。我们使用springsession来解决这个问题。
SpringSession
使用过程:
1、依赖:
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
2、配置文件:
spring.session.store-type=redis
server.servlet.session.timeout=30m
3、加注解:
启动类加上@EnableRedisHttpSession
4、配置类:
修改作用域cookie名称,序列化为json
SpringSession基本原理:
@EnableRedisHttpSession给容器中注入了一个组件,RedisOperationsSessionRepository,这个组件是用来操作session的增删改查的,只不过操作的session都是存在redis中的。
还注入了一个过滤器SessionRepositoryFilter,它将原生的request和response使用装饰者模式包装成了wrappedRequest和wrappedResponse,这两个包装后的类重写了getSession方法从RedisOperationsSessionRepository中获取session,所以以后用到session都是通过重写后的getSession获取到的redis中的session。
MD5加密:
这个api巧妙的将随机盐值加到了暗文里面,且同一个明文加密后的暗文不一样。
明文与暗文比较的api:
@RequestBody标注的参数表示来接收json数据的
Redis中存储对象需要将对象序列化(实现接口Serializable),或者存json数据。
临时购物车的实现
在执行业务之前不管用户有没有登录都搞一个临时的购物车给用户(cookie),在此期间用户的任何对购物车的操作(加购,减购)都会保存到cookie并且存到浏览器(过期时间半小时)。可以实现用户在没有登录的情况下可以看见之前加入到购物车的商品,并且在用户登录之后可合并之前在未登录状态下加购的商品。
实现方法:拦截器
声明拦截器
注册拦截器以及设置拦截路径
ThreadLocal
可以存储一个线程独立的数据,其他线程读取不到。一次请求代表一个线程,重定向之后就是不同线程了。
维护了一个ThreadLocalMap,键是ThreadLocal值是存储的对象。
商城中的运用:
在拦截器中声明一个静态的ThreadLocal,每个请求都会经过拦截器,然后把用户id和购物车的cookieId封装到UserInfoTo再用ThreadLocal存起来。以后无论在哪个地方只要还是这个请求就能直接获取到这个userInfoTo。
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
threadLocal.set(userInfoTo);
UserInfoTo userInfoTo = threadLocal.get();
在redis中用hash来存储购物车中的商品
key表示是哪一个购物车,里面存有一个个hash,这些hash的key是skuId,值是商品。
指定操作哪一个key里面的hash,也就是操作哪一个购物车里面的商品。
绑定完之后就可以用这个operations来操作指定的key的hash(操作指定购物车里的商品)
RabbitMQ
Feign远程调用问题
丢失请求头(丢失cookie)
feign转发请求不是浏览器发的,不含请求头,feign在转发请求的时候会经过一系列的拦截器来包装这个请求,我们可以创建拦截器在拦截器中把要转发的请求的请求头信息带上。
容器中注入一个请求的拦截器 ,每个线程的RequestContextHolder中的内容是不一样的。
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
异步处理时丢失ThreadLocal中的信息
使用openfeign远程调用经过拦截器带上cookie信息时只有主线程能带上,异步处理都是从线程池中新拿的线程,而ThreadLocal又是线程独立的,所以使用异步处理的时候回出现丢失请求中信息的问题。
解决方法:
在进入到新线程异步处理之前,先把主线程中的请求信息取出来,进入到异步线程之后将取出来的值赋给新线程,然后就可以带着请求信息远程调用了。RequestContextHolder.getRequestAttributes();
接口幂等性
防止用户重复提交订单:
1、在用户查看这个订单的时候,设置一个token给返回给客户端的数据(这里是orderConfirmVo),并且将这个token令牌保存到redis中。
//防重令牌,防止这个订单重复提交
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberEntity.getId(),token,30, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(token);
2、用户真正提交这个订单时,从客户端获取token到redis中去验证是否存在这个token存在则提交成功并且删除这个token,如果多次提交的话后面的请求就到redis中找不到这个token了即验证失败。token令牌的验证和删除必须保证原子性,所以我们使用lua脚本进行对redis的操作。
//1、验证令牌【令牌的对比和删除必须保证原子性】
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//获取这个订单请求的token
String orderToken = vo.getOrderToken();
//获取当前用户
MemberEntity memberEntity = LoginUserInterceptor.loginUser.get();
//通过lure脚本原子验证令牌和删除令牌,返回1就是验证通过
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberEntity.getId()),
orderToken);
分布式事务
不使用分布式事务存在的问题
在提交订单这个服务中如有以下流程:保存订单->锁库存->扣减积分
其中锁库存操作是需要远程调用库存服务的,如果锁库存操作成功但是因网络原因没有返回结果,订单服务就误以为锁库存操作失败于是进行回滚,这样一来库存已经锁了但是保存的订单却回滚了导致了数据不一致的情况。又比如扣减积分操作失败进行回滚时无法使远程调用的锁库存操作也回滚。
使用seata控制分布式事务
缺点:不适用于最求高并发的接口
tc相当于seata服务,协调不同服务的事务
步骤:
1、每一个微服务的数据库中必须创建一个undo_log表
2、安装事务协调器(seata服务器),修改seata配置文件。
指定注册中心为nacos并配置地址
3、导入依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.3.0</version>
</dependency>
4、启动seata服务
5、在使用上了分布式事务的方法上加上注解@GlobalTransactional,远程调用的其他小事务加上@Transactional就可以了
使用延时队列实现分布式事务(适用于高并发)
设置一个延时队列给这个队列指定死信交换机、路由键、存活时间,这个队列接受到的消息会在队列中存活一段时间,如果存活时间内没被消费就是死信了,然后被死信交换机给接受发送给死信队列进行指定操作(订单没有被支付,删除订单)。
/**
* 延迟队列
* @return
*/
@Bean
public Queue stockDelay() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
延时队列在分布式商城中的应用--锁定库存(orderLockStock)
用户提交订单之后需要锁定库存,库存锁定成功之后一定往延时队列中发一个消息,延时队列的时间到了之后发送消息到解锁库存的队列,然后调用解锁库存的方法。
解锁方法是否解锁参考如下:
* 1、查询数据库关于这个订单锁定库存信息 * 有:证明库存锁定成功了 * 解锁:订单状况 * 1、没有这个订单,必须解锁库存 * 2、有这个订单,不一定解锁库存 * 订单状态:已取消:解锁库存 * 已支付:不能解锁库存
延时队列在分布式商城中的应用--关闭订单(closeOrder)
用户可能在三十分钟内没有支付订单就要关闭这个订单,在创建订单时候发送一个消息给一个延时队列,三十分钟之后这个队列的消息路由到关闭订单的队列进行关闭订单业务,执行这个业务要判断这个订单的状态是新创建的还是已被支付的再决定是否关闭。
在创建订单的时候会锁库存,然后有一个解锁库存的延时消息,这个延时消息是比关闭订单的消息慢的,所以可以等延时关闭订单的消息消费完看订单是否关闭再判断是否解锁库存。但是如果关闭订单的服务出现网络问题,本来要关闭的订单迟迟没有关闭这时已经到了解锁库存延时消息的时间了,这时就会错误的判断订单没有关闭不进行解锁操作。所以在订单关闭之后要再手动地发一条解锁库存的消息。
支付宝沙箱
加密加签逻辑
商户根据要传输的信息生成一个私钥发给支付宝作为公钥,支付宝收到商户发来的消息后用公钥来进行验证防止中途被人拦截修改数据,同样,支付宝响应商户时也要生成私钥加密信息,然后把私钥发给商户作为公钥来验证发过来的信息。
沙箱的使用
1、进入支付宝开放平台
2、进入沙箱
3、使用公钥
4、导入依赖
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.9.28.ALL</version>
</dependency>
5、工具类
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
public String app_id;
// 商户私钥,您的PKCS8格式RSA2私钥
public String merchant_private_key;
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
public String alipay_public_key;
// 服务器[异步通知]页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
// 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
public String notify_url;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
//同步通知,支付成功,一般跳转到成功页
public String return_url;
// 签名方式
private String sign_type;
// 字符编码格式
private String charset;
//订单超时时间
private String timeout = "1m";
// 支付宝网关; https://openapi.alipaydev.com/gateway.do
public String gatewayUrl;
public String pay(PayVo vo) throws AlipayApiException {
//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
//1、根据支付宝的配置生成一个支付客户端
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id, merchant_private_key, "json",
charset, alipay_public_key, sign_type);
//2、创建一个支付请求 //设置请求参数
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(return_url);
alipayRequest.setNotifyUrl(notify_url);
//商户订单号,商户网站订单系统中唯一订单号,必填
String out_trade_no = vo.getOut_trade_no();
//付款金额,必填
String total_amount = vo.getTotal_amount();
//订单名称,必填
String subject = vo.getSubject();
//商品描述,可空
String body = vo.getBody();
alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
+ "\"total_amount\":\""+ total_amount +"\","
+ "\"subject\":\""+ subject +"\","
+ "\"body\":\""+ body +"\","
+ "\"timeout_express\":\""+timeout+"\","
+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
String result = alipayClient.pageExecute(alipayRequest).getBody();
//会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
System.out.println("支付宝的响应:"+result);
return result;
}
}
6、配置沙箱中的公钥私钥
cron定时任务
在Spring中表达式是6位组成,不允许第七位的年份(秒 分 时 日 月 周) 在周几的的位置,1-7代表周一到周日 定时任务不该阻塞(比如一个任务要每五秒执行一次,但时其中一个任务执行了6了,那么下一个任务就得不到按时的执行)。默认是阻塞的。 解决方法三种: 1)、可以让业务以异步的方式,自己提交到线程池 CompletableFuture.runAsync(() -> { },execute); 2)、支持定时任务线程池;设置 TaskSchedulingProperties spring.task.scheduling.pool.size: 5 3)、让定时任务异步执行 异步任务 解决:使用异步任务 + 定时任务来完成定时任务不阻塞的功能
定时任务 1、@EnableScheduling 开启定时任务 2、@Scheduled开启一个定时任务 异步任务 1、@EnableAsync:开启异步任务 2、@Async:给希望异步执行的方法标注
应用--秒杀商品定时上架
每天晚上3点,上架最近三天需要秒杀的商品,将参与秒杀的活动场次以及每个场次的商品放入redis中
当天00:00:00 - 23:59:59
明天00:00:00 - 23:59:59
后天00:00:00 - 23:59:59
防止同一个用户重复秒杀:
用户秒杀成功后在redis中使用用户的id和这个秒杀场次的id作为key占位并设置过期时间,下一次再参与秒杀的时候就会去redis中判断是否已经秒杀过了。
Sentinel
使用步骤
1、引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2、下载sentinel控制台
在sentinel的jar包所在文件目录输入cmd
输入指令使用8333端口启动控制台
java -jar sentinel-dashboard-1.8.6.jar --server.port=8333
3、配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8333 #客户端地址
port: 8719
#暴露所有端点
management:
endpoints:
web:
exposure:
include: '*'
自定义流控响应
在控制台进行流控,默认能流控某些接口,也可自定义往下看。
@Configuration
public class GulimallSeckillSentinelConfig {
public GulimallSeckillSentinelConfig() {
WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMessage());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().write(JSON.toJSONString(error));
}
});
}
}
使用sentinel来保护feign远程调用:熔断
#使用sentinel做熔断保护
feign:
sentinel:
enabled: true
1、调用方熔断保护,指定回调方法
2、在控制台手动给提供方降级
熔断和降级是两个概念,熔断主要是在调用方控制,降级是在提供方控制。熔断主要是防止提供方宕机,降级则是提供方为了解压,给调用方提供了一些简单的数据。
自定义限流资源
1)使用try catch
//seckillSkus是自己取的资源名称
try (Entry entry = SphU.entry("seckillSkus")) {
//执行的代码。。。。。
} catch (BlockException e) {
log.error("资源被限流{}",e.getMessage());
}
取完名后到控制台给seckillSkus限流
2)使用注解
@SentinelResource(value = "getCurrentSeckillSkusResource",blockHandler = "blockHandler")
@Override
public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
//被限流的方法体
}
//被限流调用的方法
public List<SeckillSkuRedisTo> blockHandler(BlockException e) {
log.error("getCurrentSeckillSkusResource被限流了,{}",e.getMessage());
return null;
}
在网关层进行限流熔断等操作
导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
然后就可以在控制台进行限流操作
定制网关返回
@Configuration
public class SentinelGatewayConfig {
public SentinelGatewayConfig() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
//网关限流了请求,就会调用此回调
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMessage());
String errorJson = JSON.toJSONString(error);
Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errorJson), String.class);
return body;
}
});
}
}