微服务架构的思想
微服务是一种架构风格,旨在将应用程序拆分为一组小型、独立的服务,每个服务都专注于执行特定的业务功能。每个服务都可以使用自己的技术栈、数据库和开发团队,通过轻量级通信机制进行交互。
微服务架构的优点:
-
独立开发和部署:故障隔离。
-
可伸缩性好:便于业务的扩展。
-
维护性高:每个服务关注一个特定的业务功能,修改和维护起来更容易。
微服务架构的缺点:
-
复杂性增强:服务间的通信,数据的一致性,以及分布式事务等问题
-
运维难度大,成本高;对团队的协作有更高的要求
Nacos
官网: What is Nacos
由于存在服务与服务之间的调用问题;我们需要将服务注册到nacos中。
Nacos是什么?
Nacos致力于帮助服务注册、发现、配置和管理服务。Nacos提供了一组简易的特性集,帮助快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos基本结构
-
nacos-server:Nacos服务端,在页面中管理的进程
-
nacos-client:连接nacos-server 根据提供的接口功能进行调用的进程。这个进程一般需要整合到微服务中运行,使用微服务信息和nacos-server进行交互。
Nacos的功能及使用
<!--nacos-注册抓取-client--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--nacos-discovery-->
注册发现
-
作用:
将服务注册到nacos中,用于被其他服务发现并使用。 注册发现接口功能: nacos-server nacos-client之间交互逻辑 众多接口,可以分为两大类,写操作(注册注销), 读操作(读取服务,监听服务)
-
注册:将自己应用实例信息 放到nacos. 目的是让别人可以发现.
-
发现: 读取别人在nacos注册信息. 目的是可以调用别人.
在注册时可以选择为永久实例还是临时实例:
-
永久实例:保证日常使用,永远不会删除,nacos会主动去探测心跳是否正常
-
临时实例:多用于高峰并发时,结束后会直接停止;它要主动向nacos提供心跳。
在注册时还可以将服务进行分组,存放在不同的命名空间中;从而实现隔离的开发,灰度开发等。
-
-
如何使用:
第一步在.pom文件添加依赖
第二步在yml配置文件中添加nacos相关属性
#微服务配置 #在微服务架构中,表示微服务名字 #不同的命令表示功能不一样,相同的名字,功能接口都相同 spring: application: name: csmall-business #微服务cloud配置 cloud: #微服务cloud中组件nacos nacos: #nacos中注册发现的功能 discovery: #填写nacos的服务端地址 server-addr: localhost:8848 #当前启动进程是永久实例还是临时实力 true 表示临时实力 false表示永久实例 #true是默认值 ephemeral: true #nacos实例的ip地址 ip: 127.0.0.1 #命名空间 namespace: f033ea8e-15ca-4f37-b112-127edc03de9e #分组 group: 1.0 #心跳检测的间隔时间 heart-beat-interval: 5 #超时心跳删除时间 ip-delete-timeout: 20
配置中心
-
作用:
一般在项目中,我们会用到很多公用的配置信息;例如:数据库连接信息、mybatis的配置信息等。可将公用的配置信息放在nacos的配置中心。
-
如何使用:
在微服务项目中的resources文件夹中添加bootstrap.yml配置文件;当项目启动时优先读取此配置文件。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--springboot版本大于2.3.X,需要引入这个依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
spring: profiles: #开启不同环境配置 active: dev #bootstrap定义不同环境,在同一个文件bootstrap.yaml #---区分,每个环境中定义名称 --- spring: config: activate: on-profile: dev #告诉bootstrap 远程配置中心nacos地址 和我们要读取的配置文件(先读默认的) application: #所有的application.name都是服务名称 name: csmall-stock cloud: nacos: config: #必须配置的值 server-addr: localhost:8848 #文件类型 后缀 默认是properties txt json yaml xml html file-extension: yaml #前缀,默认是服务名称,不想使用默认值,可以自定义 #prefix: sdlaslkdfjsdlj #多环境运行的 namespace 命名空间的id #namespace: f033ea8e-15ca-4f37-b112-127edc03de9e #持续发布中,保证灰度发布 分组 #group: 1.0 #是否支持刷新.远程配置一旦修改,本地进程内存数据是否刷新,如果刷新,true,不需要重启 #false 重启,内存才变动 默认也是true refresh-enabled: true #读取指定的文件 shared-configs: - data-id: redis.yaml group: 1.0 refresh-enable: true - data-id: datasource.yaml group: 1.0 refresh-enable: true - data-id: es.yaml group: 1.0 refresh-enable: true --- spring: config: activate: on-profile: test
Dubbo远程调用
-
作用:
Dubbo是一个RPC框架,序列化底层通信协议。
当我们需要服务与服务之间内部的远程调用时,我们可以借助Nacos和Dubbo结合使用。
-
实现:
方法一(使用apache):
引入依赖
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.8</version> </dependency>
provider的applicationContext.xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <!--定义应用 dubbo概念中也有应用--> <dubbo:application name="${spring.application.name}"> <!--qos关闭--> <dubbo:parameter key="qos" value="false"/> </dubbo:application> <!--注册中心配置--> <dubbo:registry id ="nacos" protocol="nacos" address="localhost:8848" use-as-config-center="false" use-as-metadata-center="false"/> <!--dubbo配置协议 设置ip port -1 同一个服务器端口 会从20880开始向后顺延 协议名称--> <dubbo:protocol name="dubbo" port="-1"/> <!--dubbo配置spring中的 暴露接口 和接口实现 创建过程交给spring管理,使用dubbo在用--> <!--provider 对应service consumer 对应 referrence--> <!--springxml 相当于bean标签--> <dubbo:service interface="com.tarena.luban.demo.cart.api.DubboTestApi" ref="dubboTestRpcApi" registry="nacos"/> </beans>
consumer的applicationContext.xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://dubbo.apache.org/schema/dubbo" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"> <!--dubbo应用配置--> <dubbo:application name="${spring.applicaiton.name}"> <dubbo:parameter key="qos" value="false"/> </dubbo:application> <!--注册中心--> <dubbo:registry id="nacos" protocol="nacos" address="localhost:8848" use-as-metadata-center="false" use-as-config-center="false"/> <!--通信协议--> <dubbo:protocol name="dubbo" port="-1"/> <!--consumer角色 reference 底层是个代理对象 注入使用--> <!--check = false 表示消费者启动时候会不会检查抓取的provider存在还是不存在--> <dubbo:reference id="dubboTestApi" interface="com.tarena.luban.demo.cart.api.DubboTestApi" registry="naocs" check="false"/> </beans>
由于使用applicationContext.xml 对Dubbo应用进行了配置,需要在启动类中添加@ImportResource(“applicationContext.xml”)
方法二(使用阿里巴巴):
引入依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
yaml配置:
spring: application: name: csmall-stock dubbo: application: #dubbo客户端 在nacos注册,携带的服务名称 name: ${spring.application.name} protocol: #port表示dubbo底层通信协议使用的端口 -1 自增的端口号 #20880开始,查看当前服务,哪个没有被占用 port: -1 name: dubbo registry: #告诉dubbo注册中心类型和地址 address: nacos://localhost:8848 #禁止dubbo在nacos中自动创建一些无效的配置文件 use-as-config-center: false use-as-metadata-center: false cloud: #明确当前服务,订阅的其他服务,如果需要调用其他服务,用多个服务拼接, #不配置这个属性,会导致当前的dubbo客户端默认订阅nacos所有服务 # 不给值,订阅空 subscribed-services: consumer: #如果当前程序 有consumer的角色,不会在启动时检查provider是否存在 #如果当前程序不是consumer,这个配置没有任何作用 check: false
配置类+注解:
配置类添加 @EnableDubbo 注解 》》 provider业务层添加 @DubboService 注解 》》 consumer调用处使用 @DubboReference 注解引入。
-
负载均衡
负载均衡的计算策略:加权随机,加权轮训,最小连接值,一致性hash。
优先级:provider>consumer;局部>全局。
<dubbo:server interface="inter" ref="bean" loadbalance="random" weight="6"/>
dubbo: provider: loadbalance: roundrobin
注解 @Reference(version = "${product.service.version}",loadbalance="roundrobin")
-
重试机制
可能存在网络抖动的情况,造成调用的失败,可以设置重试机制,进行多次调用。
<dubbo:reference id="stockApi" interface="com.tarena.luban.demo.stock.api.StockApi" registry="nacos" check="false" retries="5" timeout="2000000"/>
Sentinel熔断/限流/降级
<dependencies> <!--sentinel alibaba cloud 组件--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> </dependencies>
核心概念
-
资源
资源是Sentinel的关键概念。它可以是Java应用程序的任何内容,例如:由应用程序提供的服务,一段代码,一个方法,一个类等等。只要通过Sentinel API 定义的代码,就是资源,能够被Sentinel保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
如何定义资源:
方法一(硬编码):
//准备一个sentinel资源入口,进入sentinel统计责任链中 Entry entry=null; try{ //对entry赋值,赋值过程,会真正进入sentinel计算统计责任链中 entry= SphU.entry("sayHello"); String result=helloService.sayHello(name); return result; }catch (BlockException e){ log.info("当前资源sayHello受到了限制",e); }finally { //释放entry资源 if(entry!=null){ entry.exit(); } }
方法二(注解定义):
@SentinelResource(value="资源名")
@SentinelResource( value = "serviceHi", blockHandler = "aaa", blockHandlerClass = 类名.class fallback = "bbb", fallbackClass = 类名.class)
注解的底层原理:就是使用AOP切面技术将SphU.entry("资源名")放到切面编程中。
blockHandler: 用于编写处理降级后的逻辑代码。
fallback: 用于编写出现其他异常的逻辑代码
-
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
我们可以将规则存放在Nacos配置中心内。使用时直接读取。
熔断
-
作用:
在多个服务调用过程中,由于某一个服务的实例故障,导致调用失败、延迟、等待;挨个相传导致整个大系统瘫痪。此时我们就需要对出现故障的服务实例进行熔断策略:就好比一个断路器,牺牲局部来保全大局。
断路器的三种状态:闭合,断开,半开.
-
规则
{ "resource": "sayHello", "count": 100, "grade": 0, "timeWindow": 10, "minRequestAmount": 10, "slowRatioThreshold": 0.5, "statIntervalMs": 10000 }
resource: 绑定已经定义好的资源名称
count: 熔断触发的阈值(不再调用这个资源,而是访问降级策略)。如果grade=0 count表示慢调用临界RT(响应时间单位毫秒),超过这个数字,就记录一次慢调用。grade=1,count值应该是>0小于1的小数,表示异常比例;grade=2 count配置整数,表示异常出现的次数。
grade: 熔断类型 0 默认值 慢调用比例 1 异常比例 2异常数
timeWindow: 如果触发熔断,持续时间,单位秒。
minRequestAmount: 最少统计请求数量,如果没达到,即使超过count的阈值,也不熔断
slowRatioThreshold: 慢调用比例,只有在grade=0的时候才有用.
statIntervalMs: 统计时长,计算判断熔断规则是否违反的逻辑中,有很多都需要统计时间段 单位是毫秒数
限流
-
相关概念:
QPS: 每秒查询的数量
RT: 一次请求的响应时间
PV: 一次请求计算一次pv
QPS=并发量/RT
-
作用: 当访问量剧增时,可以对请求进行限流控制;当达到流量压测值时,超出的请求禁止访问,直接返回失败
-
规则
{ "resource": "app", "count": 1, "grade": 1, "strategy": 1, "limitApp": "limitApp", "controlBehavior": 0 }
resource: 绑定已经定义好的资源名称
count: 限流阈值,至于阈值表示的何种含义 取决于grade grade=1 qps数量 grade=0 并发数量(web应用并发表示同时存在请求个数)
grade: 1 qps 流控类型; 0 并发 流控类型
strategry: 限流的模式
0 直接限流::针对resource的资源做限流
1 关联限流:limitApp 属性值也表示一个资源,如果app的资源访问超过阈值了,限制limitApp。
例如: 双11,所有资源都需要给下单让路.
2 链路 limitApp 指定的资源,查看当前资源的链路是否是通过limitApp进入的,如果不是,不限制流量。如果是则限制流量.(限制的是来源)
limitApp: 当前资源关联的其它的某个资源,是否生效取决于strategy的值 1 2有用,而且作用不同.
controlBehavior: 限流效果,当访问已经违反了限流规则时,表现状态
0: 直接拒绝抛异常
1: Warm up 慢慢的拒绝抛异常
2: 排队 后续请求不拒绝 排队到一个queue队列(有上限)
降级
-
作用:
退而求其次的原则;在保证核心业务的前提下,提供可用数据。当发生了熔断和限流的情况时,我们可以将其请求降级处理,返回一个可以接受的结果。
Getaway网关
1.概念
微服务网关是一个用于管理和监控微服务的入口,用于转发和路由来自客户端的请求。微服务网关可以将来自客户端的请求转发给后端的多个微服务,同时也可以处理跨域、身份验证、限流、缓存、流量控制等一系列与微服务相关的功能,从而简化了微服务架构的服务开发的复杂度。
网关的架构图:
当客户端发出请求后,请求进入网关,经过GatewayHandlerMapping来匹配传入的请求路径和谓词(即根据预定义的路由规则进行映射),经过GatewayWebHandler进行实际的处理和转发请求;期间经过过滤器可以做一些自定义的处理(例如:解析JWT,检查权限等);从nacos中拿到所有服务注册的路由信息;由负载均衡进行计算代理向后调用项目,从而返回数据。
2. 如何使用:
依赖:
<!--网关发现服务后,进行负载均衡的转发调用--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!--网关核心依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
application.yml配置:
server: #端口 port: 10000 spring: application: # spring应用名称 name: luban-demo-gateway main: # springcloud gateway 底层框架webflux 和tomcat里的servlet有冲突 # spring-boot-starter-web 需要这个值避免冲突 web-application-type: reactive cloud: gateway: # 为入门案例转发,做一个路由配置 # routes 是一个list类型属性 # 表示可以在网关中配置的路由规则 routes: # id 全局唯一 - id: cart-test # 指定访问一个uri: http://127.0.0.1:9011 uri: lb://luban-demo-cart predicates: # Path 路径匹配请求 /**匹配所有进入网关的请求. - Path=/cart/** filters: # 可以将断言匹配的路径path 去处[n]级前缀路径,然后拼接uri - StripPrefix=1 - id: stock-test #uri: http://127.0.0.1:9002 uri: lb://luban-demo-stock predicates: - Path=/stock/** filters: - StripPrefix=1
3. Path路由匹配规则
规则 | 说明 | 案例 |
---|---|---|
/? | 匹配/开始的任意一个字符 | /a, /b, /c |
/* | 匹配/开始的任意一个字符串 | /abc, /bc |
/** | 匹配/开始的任意多个字符串 | /a/b/c, /a/c |
4.跨越处理
spring: application: # spring应用名称 name: luban-demo-gateway main: # springcloud gateway 底层框架webflux 和tomcat里的servlet有冲突 # spring-boot-starter-web 需要这个值避免冲突 web-application-type: reactive cloud: gateway: # 可以配置跨域 globalcors: cors-configurations: "[/**]": #允许 origin来源的配置 allowed-origin-patterns: "*" #- "order.luban.com" #- "cart.luban.com" #- "**.luban.com" #允许跨域cors时的头 allowed-headers: "*" #- "Accept" #- "Authorization" #允许跨域的方法 allowed-methods: "*" #- "GET" #- "POST"
5.网关过滤器
每个web应用都可以存在过滤器和拦截器,SpringMVC中也有过滤器和拦截器;逻辑基本相似,只是不同组件中的使用习惯和编码api不同。
常用于用户认证授权(spring security框架)
-
过滤器特点:
1.过滤器各司其职
2.顺序执行需要符合业务逻辑(越简单、越快速的过滤器越在前面执行)
3.特殊的情况,过滤器可以实现拦截的效果(请求和响应对象在过滤器整条链路中)
-
使用案例:
@Component public class FirstFilter implements GlobalFilter, Ordered { /** 网关中 编写过滤逻辑的方法 * @param exchange webflux 底层给我们封装的一个对象 包装了请求和响应数据 * @param chain 过滤链,贯穿整个web容器的一个过滤器连接对象 * @return Mono SpringWebFlux 返回结果 SpringMVC Model ModelAndView */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { /** 在此处写需要在过滤器执行的逻辑*/ //request对象 包含请求url地址 path 请求query参数 请求头数据 //从exchange拿到请求对象 ServerHttpRequest request = exchange.getRequest(); //请求路径 RequestPath path = request.getPath(); URI uri = request.getURI(); System.out.println("获取uri:"+uri.toString()); System.out.println("获取path:"+path.toString()); //请求参数 localhost:10000/abc?name=haha&age=18&girlFriends[0]=王翠花 &girlFriends[1]=刘翠花 MultiValueMap<String, String> queryParams = request.getQueryParams(); //key 是string value List<String> //name=haha name=List<String>{"haha"} //name[0]=haha&name[1]=gaga 不会像springmvc一样整理成list数据,只会单独封装 //query在当前方法的value值永远都是一个元素的list对象. List<String> name = queryParams.get("name"); System.out.println("name值元素个数:"+name.size()); System.out.println("数据分别是:"+name.toString()); //jwt解析,获取的headers Authorization //获取头数据,打印展示 HttpHeaders headers = request.getHeaders(); //从所有头中,获取一个名称的头 List<String> headerValues = headers.get("Accept"); System.out.println("当前头的元素个数:"+headerValues.size()); System.out.println(headerValues); //request其它api request.getId(); //能解析客户端ip request.getRemoteAddress(); return chain.filter(exchange); } /** 此方法用于设置过滤器执行的顺序;返回值越小越靠前。 */ @Override public int getOrder() { return 0; } }
RocketMQ消息队列
-
rocketmq-dashboard(辅助观察仪表盘)
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <!--传递进来一个rocket-client 4.9.3 和当前软件版本没有冲突--> <!--如果编写测试案例的时候,出现了匪夷所思的异常 可以尝试将版本更新4.9.4--> <version>2.2.2</version> <!--<exclusions> <exclusion> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> </exclusion> <exclusion> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-acl</artifactId> </exclusion> </exclusions>--> </dependency> <!--<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.9.4</version> </dependency> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-acl</artifactId> <version>4.9.4</version> </dependency>-->
rockermq :适用于高吞吐、强一致性的消息通信场景。它主要用于大规模互联网应用的消息中间件,支持延迟消息、事务消息、顺序消息等特性。
rabbitmq :适用于灵活性较高、消息处理逻辑较为复杂的场景。它支持多种消息协议,包括AMQP、STOMP、MQTT等,并提供了丰富的特性和插件机制。
kafka :适用于处理大规模的实时数据流。它是一种高吞吐、高可靠、低延迟的分布式消息系统,广泛应用于日志收集、实时流处理、事件驱动架构等场景。
消息队列可以实现哪些功能?
异步处理、削峰填谷、解耦合、可扩展、顺序性等等。
RocketMQ核心概念和运行原理
核心概念
1.nameserver :注册中心,支持Topic、broker的动态注册和发现。
2.broker :主要负责消息的存储,投递和查询以及服务高可用的保证。(同时配置多个主从结构,形成一个分布式消息队列的集群)
3.Topic主题 :一类消息的集合,由broker管理。一个rocketMq的集群中可以有多个主题。
4.queue队列 :存储消息的物理实体(最小单位),一个Topic中可以包含多个Queue(分布式体现的关键),每个Queue中存放的就是该Topic的消息。一个Topic的Queue也被称为一个Topic中消息的分区(partition)。
5.生产者组 :
-
从技术角度讲:一个生产者组可以向多个主题发送消息,同一个主题也可以接收多个不同生产者组发送的消息。
-
从业务角度:只要能保证发送给不同主题的消息,是同一种类型的消息,就可以组合。否则会出现消费失败,消费故障.。(一般情况下 一个分组的生产者 只给一个主题发送消息,如果需要发送给多个主题,多创建一个生产者分组)。
6.消费者组 :消费者进程通过连接nameserver获取注册信息,根据主题中的队列个数,根据主题中记录的消费者组的信息,确定直接绑定的队列。消费者最大数量就是队列数量。
7.消费点位 :同一个消费组 共享一份消费位点(consumerOffset)记录的,所以无论多少个消费者,哪个消费者都不会对同一个主题的消息在绑定时 重0计算消费位点。
8.keys和tags :
-
keys:绑定到消息的一个业务数据,用来查询统计使用的。
-
tags:给消息绑定标签,消费端消费时,可以根据标签进行过滤消费。
运行原理
RockerMQ的使用案例
1.添加相应依赖
2.配置yaml文件
server: port: 8999 # 满足rocketmq的自动配置逻辑的属性 rocketmq: # nameserver 连接的nameserver name-server: localhost:9876 # defualMQProducer 的属性条件 必须指定producer.group producer: group: test-producer-group # 不是必须的 consumer: group: test-consumer-group
3.发送消息
package com.tarena.demo.luban.all.main.controller; @RestController @RequestMapping("/base/order") @Api(tags = "订单") public class OrderController { @Autowired private RocketMQTemplate rocketMQTemplate; @PostMapping("/add") @ApiOperation("新增订单的功能") public JsonResult addOrder(OrderAddParam orderAddParam){ //发送消息 Message是rocketMQTemplate支持发送的消息参数 //和底层api方法的Message不是同一个类,相当于将底层Message包装了一次. //orderService.addOrder(orderAddParam); //发送一条消息 目的实现业务层代码的调用 //新增订单 orderAddParam作为payLoad发送 需要确定这个类型是否实现了序列化接口 //准备一个消息对象 Message<OrderAddParam> message= MessageBuilder.withPayload(orderAddParam).build(); //发送消息 同步发送 //***:** 这种格式的desitination 底层翻译的时候 解析成 topic:tag rocketMQTemplate.syncSend("luban-demo-order-topic:addOrder",message); return JsonResult.ok("新增订单完成!"); } }
4.消费消息
/** topic:消费端绑定主题 consumerGroup:消费者分组 selectorExpression: 顾虑的标签 */ @Component @RocketMQMessageListener( topic = "rocket-topic-a", consumerGroup = "${rocketmq.consumer.group}", selectorExpression = "*") public class MyConsumerListener implements RocketMQListener<String> { /** * 每个listener自定义的对象,底层都会开启一个消费进程 绑定这个listerner * 在底层消费者中,监听consumerMessage方法里,调用这个类的onMessage; * 调用之前,已经实现了对象消息数据的转化 * 接口有泛型,底层方法逻辑,会根据泛型,将消息message进行反序列化和数据封装 * @param name 根据泛型反序列化的body对象 * 对于消费成功还是失败,spring整合rocketmq: 只要抛异常,就返回失败,不抛异常就是正常 */ @Override public void onMessage(String name) { System.out.println("消费端接收到消息:"+name); } }
分布式事务消息
在一个消费平台,当用户选购完商品后,下单付款;此时即需要生成订单信息和减少库存量(本地事务)、并且清除购物车。(既有本地事务、又需要异步处理时)此时需要保证整个流程为一个整体,要么全部成功,要么全部失败。
此时需要实现分布式事务,生产者先发送一个半消息,当事务协调返回接收成功后,开始执行本地事务,成功则commit,执行异步消息,消费者消费消息。
发送半消息
/** 发送半消息测试 */ @GetMapping("/half") public String sendHalf(){ Message message=MessageBuilder.withPayload("半消息").build(); rocketMQTemplate .sendMessageInTransaction( "trans-test-topic", message,"业务数据"); return "success"; }
本地事务监听器
/** 和消费端监听,类似的 1.spring bean对象 2.实现一个接口 事务监听接口 3.使用注解 当前这个类,就是本地代码,不能和半消息发送的客户端代码分开,到两个应用程序 */ @Component @RocketMQTransactionListener @Slf4j public class MyTransactionConsumerListener implements RocketMQLocalTransactionListener { /** * @param message 半消息中的消息对象 * @param o 业务数据 * @return */ @Override public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { log.info("当前执行本地事务的逻辑,time:{}"); log.info("本次本地事务,执行结束后返回UNKNOWN"); return RocketMQLocalTransactionState.UNKNOWN; } /** * 回调方法 只有上面的本地事务方法返回UNKONNW或者超时,才会调用. * @param message * @return */ @Override public RocketMQLocalTransactionState checkLocalTransaction(Message message) { log.info("进入到回调方法,说明本地事务方法执行返回UNKNOWN或者抛异常,或者超时"); return RocketMQLocalTransactionState.UNKNOWN; } }
executeLocalTransaction
进程中一旦发送了半消息,说明开始消息事务流程,就会进入这个方法,执行本地业务,通过本地事务,控制返回结果.
checkLocalTransaction
在本地事务方法中,返回结果是unknown和超时和异常,会调用.
Redis
官网:Redis
中文网址:Redis中文网
命令参考网址:https://www.cnblogs.com/antLaddie/p/15362191.html
基本数据类型:String、List、Hash、Set、Zset。
<!-- Spring Boot支持Redis编程的依赖项 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency>
配置类
@Configuration public class RedisConfiguration { @Bean public RedisTemplate<String, Serializable> redisTemplate( RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.json()); return redisTemplate; } }
使用案例:
@SpringBootTest public class RedisTests { // 如果操作与值相关,需要获取XxxOperations才能调用对应的API // -- 例如存入或取出字符串、对象类型的值时,需要先获取ValueOperations对象 // 如果操作与值无关,直接调用RedisTemplate的API即可 // -- 例如执行keys或delete时,直接调用RedisTemplate的API,并不需要事先获取XxxOperations对象 @Autowired RedisTemplate<String, Serializable> redisTemplate; // 存入字符串类型的值 @Test void setValue() { ValueOperations<String, Serializable> ops = redisTemplate.opsForValue(); ops.set("username1", "张三"); System.out.println("向Redis中存入数据,完成!"); } @SpringBootTest public class RedisTests { // 如果操作与值相关,需要获取XxxOperations才能调用对应的API // -- 例如存入或取出字符串、对象类型的值时,需要先获取ValueOperations对象 // 如果操作与值无关,直接调用RedisTemplate的API即可 // -- 例如执行keys或delete时,直接调用RedisTemplate的API,并不需要事先获取XxxOperations对象 @Autowired RedisTemplate<String, Serializable> redisTemplate; // 存入字符串类型的值 @Test void setValue() { ValueOperations<String, Serializable> ops = redisTemplate.opsForValue(); ops.set("username1", "张三"); System.out.println("向Redis中存入数据,完成!"); } }
计时任务:
计划任务指的是:在满足一定条件下,会周期性执行的任务,通常,可能是每过多长时间就执行一次,或到了某个特定的时间点就执行一次。
计划任务可能是比较耗时的,在Spring Boot项目中,默认不允许执行计划任务,需要在配置类上添加@EnableScheduling
注解,以启用计划任务!
则在项目中的根包下创建config.ScheduleConfiguration
类,在类上添加@Configuration
注解,并添加@EnableScheduling
注解:
@Configuration @EnableScheduling public class ScheduleConfiguration { }
然后,在任何组件类中,自定义方法(公有的、void
返回值类型、无参数列表),并在方法上添加@Scheduled
注解,则此方法就是一个计划任务方法,然后,配置注解参数,以决定什么时候执行计划任务,例如:
@Slf4j @Component public class CacheSchedule { // fixedRate:执行频率,以毫秒为单位 @Scheduled(fixedRate = 5 * 1000) public void xxx() { log.debug("CacheSchedule.xxx()"); } }
Redis分布式锁
案例代码
引入依赖、配置yaml
/** * @author java@tedu.cn * @version 1.0 */ @Component @RocketMQMessageListener( topic = "business-order-topic", consumerGroup = "${rocketmq.consumer.group}", selectorExpression = "orderAdd") @Slf4j public class OrderAddConsumerListener implements RocketMQListener<MessageExt> { @Autowired private IOrderService orderService; @Autowired private StringRedisTemplate redisTemplate; @Override public void onMessage(MessageExt msg) { //拿到底层消息对象的body byte[] body = msg.getBody(); //尝试先解析成string String orderJson=new String(body, StandardCharsets.UTF_8); System.out.println(orderJson); OrderAddDTO orderAddDTO= JSON.toJavaObject(JSON.parseObject(orderJson),OrderAddDTO.class); System.out.println(orderAddDTO); //1.生成锁的key值,生成当前这把锁的随机数 //准备锁key String msgKeyLock="msg:order:add:"+msg.getMsgId(); //准备随机数 4 6 8位 String randCode=new Random().nextInt(9000)+1000+""; ValueOperations<String, String> stringOps = redisTemplate.opsForValue(); try{ //补充消息消费的抢锁机制 //2.抢锁 setnx msgKeyLock randCode expire 10s Boolean tryLockSuccess = stringOps .setIfAbsent(msgKeyLock, randCode, 10, TimeUnit.SECONDS); //3.判断 抢锁成功还是失败 if(!tryLockSuccess){ //3.2 失败了 可以等待5秒重新抢锁,也可以直接结束 //尝试这里使用while编写等待5秒重新抢的逻辑 log.info("有别人抢锁了,msgKey:{},value:{}",msgKeyLock,randCode); return; } //3.1 成功了 执行orderAdd orderService.orderAdd(orderAddDTO); }catch (CoolSharkServiceException e){ //业务异常,说明订单新增业务性失败,比如库存没了 log.error("库存减少失败,库存触底了:{},异常信息:{}",orderAddDTO,e.getMessage()); }finally { //释放锁 读以下锁的value值,等于当前生成value才释放 String s = stringOps.get(msgKeyLock); if (s!=null && s.equals(randCode)){ //del msgKeyLock redisTemplate.delete(msgKeyLock); } } } }