第33周JavaSpringCloud微服务 分布式综合应用
一、分布式综合应用概述
分布式知识体系内容广泛,主要包括分布式事务、分布式锁、RabbitMQ等消息中间件的应用以及跨域问题的解决。
1.1 课程重点内容介绍
-
分布式事务 :在大型项目中普遍存在,是面试中的重要考点。能够全面考虑分布式事务的复杂情况,可以体现候选人的综合技术实力。本课程将详细介绍分布式事务的原理,并通过实操将项目从原本不支持分布式事务升级为支持分布式事务。
- 原理 :分布式事务是指在分布式系统中,为了保证多个操作要么全部成功要么全部失败而采用的一种机制。其核心目标是保证数据的一致性和完整性。
- 难点 :涉及多个服务或数据库,处理复杂,需要全面考虑各种情况,如网络延迟、服务故障等。例如,在微服务架构中,当一个事务涉及多个服务时,如何协调这些服务的事务提交或回滚是一个关键问题。
-
分布式锁 :当项目涉及微服务或多个服务时,普通锁无法满足需求。本课程将介绍分布式锁的原理,主要包括Redis和ZooKeeper两种实现方案,并在学习原理后,在代码中逐步引入并应用分布式锁,测试验证其特点。
- 原理 :分布式锁的目的是在分布式系统中,保证多个服务对共享资源的互斥访问。Redis通过SET NX命令实现分布式锁,ZooKeeper则利用临时顺序节点和监听机制实现分布式锁。
- 难点 :选择合适的实现方案并理解其原理及适用场景,如Redis适合高并发场景,但需要处理锁的过期问题;ZooKeeper适合需要强一致性的场景,但部署和维护相对复杂。
-
RabbitMQ的应用 :介绍RabbitMQ的设计模式,并在项目中应用。重点介绍在Spring Cloud架构中,如何使用MQ实现通信来处理库存问题(如取消订单时库存返还)。
- 原理 :RabbitMQ采用生产者-消费者模式,生产者将消息发送到队列,消费者从队列中获取消息并进行处理。
- 难点 :掌握RabbitMQ的设计模式及在Spring Cloud架构中的通信方式,特别是在微服务架构中如何通过异步通信实现库存返还等业务场景。
-
跨域问题 :在前后端分离的架构中,跨域问题是常见的问题。本课程将使用Docker和Nginx来完成跨域的配置。
- 原理 :跨域问题是由于浏览器的同源策略限制,导致前端页面无法请求其他域名、协议或端口号的资源。通过配置Nginx的请求头,可以解决跨域问题。
- 难点 :了解Docker和Nginx在跨域配置中的作用及具体使用方法,特别是在容器化部署的场景下如何进行跨域配置。
1.2 考点与易混淆点
- 分布式事务与单体事务的区别及复杂性 :分布式事务涉及多个服务或数据库,处理复杂,需全面考虑各种情况。单体事务通常只涉及一个数据库,而分布式事务需要协调多个服务的事务提交或回滚。
- 分布式锁的实现与选择 :Redis和ZooKeeper是常见的分布式锁实现方案,需理解其原理及适用场景。例如,Redis适合高并发场景,但需要处理锁的过期问题;ZooKeeper适合需要强一致性的场景,但部署和维护相对复杂。
- RabbitMQ在分布式系统中的应用 :需掌握RabbitMQ的设计模式及在Spring Cloud架构中的通信方式,特别是在微服务架构中如何通过异步通信实现业务功能。
- 跨域问题的解决方案 :需了解Docker和Nginx在跨域配置中的作用及使用方法,特别是在容器化部署的场景下如何进行跨域配置。
二、分布式事务
2.1 分布式事务章节重点梳理
复习事物的基本概念,包括ACID特性,介绍分布式事务的产生背景、下单流程分析等。
2.1.1 为什么需要分布式事务
随着系统从单体架构升级为微服务架构,数据源从单数据库升级为多数据源,分布式事务应运而生。在下单流程中,涉及两个关键点:一是与商品相关的库存,二是与订单相关的生成订单。这两个点分别属于不同的微服务,传统事务方式无法解决,需分布式事务保证数据一致性和完整性。
- 背景 :微服务架构中,每个微服务通常有自己的数据库,这导致事务的范围不再局限于单个数据库,而是跨越多个服务和数据库。
- 问题 :如果没有分布式事务,在下单过程中,可能会出现库存扣减成功但订单生成失败的情况,导致数据不一致。例如,用户下单成功但库存未正确扣减,可能会导致商品超卖等问题。
2.1.2 2PC理论
两阶段提交协议是分布式事务的经典实践,应用广泛。其成功与失败的表现如下:
- 成功表现 :事务管理器(协调者)逐一询问各服务(如商品服务、订单服务)是否准备好,所有服务均回答 “OK”,事务管理器通知所有服务提交成功。
- 失败表现 :事务管理器询问各服务时,若有服务回答 “否”,则事务管理器发出终止和回滚的指令,所有服务执行回滚操作,恢复事务前的状态。
尽管2PC可以保证分布式事务的原子性,但也存在一些缺点,如性能瓶颈和阻塞问题。性能瓶颈主要体现在事务管理器需要与所有参与事务的服务进行多次通信,增加了网络开销和事务提交的延迟。阻塞问题则是因为在事务的第二阶段,如果某个服务出现故障或网络问题,会导致整个事务被阻塞,无法及时完成。
为了解决这些问题,产生了对2PC的改进方案,如3PC(三阶段提交)。3PC在2PC的基础上进行了优化,引入了超时机制和额外的准备阶段,以避免因网络问题导致的系统卡住。然而,3PC的应用和实践尚不成熟,了解即可。
2.1.3 分布式事务实操
使用Seata框架实现分布式事务的实操,包括引入依赖、配置Seata、数据库操作、订单服务开发、注解修改、抛出异常操作及Postman操作演示等步骤。
2.2 分布式事务原理
分布式事务源于单体应用中的事物概念,在分布式系统中难度增加。重点探讨其原理,包括Seata框架的介绍、主要原理及实现步骤。
2.2.1 Seata介绍
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。它与微服务场景高度匹配,且满足对简单易用和高性能框架的需求,因此被广泛用于解决分布式事务问题。
- 优势 :Seata具有高性能、易于集成和使用等特点。它可以无缝集成到Spring Cloud等微服务框架中,开发者只需进行简单的配置和注解即可实现分布式事务的功能。
- 应用场景 :适用于需要保证分布式事务一致性的微服务应用,如电商系统中的下单、支付等场景。
2.2.2 Seata主要原理
Seata的内部结构及其原理如下:
-
核心模块 :
- TC(事务协调器) :作为独立服务,维护全局事务状态,为分布式事务奠定基础。它负责协调各个参与事务的服务,确保事务的提交或回滚。
- TM(事务管理器) :位于项目客户端,负责开启整个事务。它向TC请求生成代表全局事务的XID,并将XID传递给各个服务中的RM。
- RM(资源管理器) :管理数据库等资源,如操作订单数据库。它与TC通信并注册,根据TC的指令提交或回滚事务。
-
分布式事务流程 :
- TM开启新事务 :TM启动新事务,并向TC请求生成代表全局事务的XID。
- XID传递 :TM获取XID后,将其逐一传递给各个服务中的RM。
- RM注册至TC :每个RM获取XID后,与TC进行通信并注册。
- 事务决策 :所有服务调用完毕后,TM与TC通信,决定是提交还是回滚事务。
- TC驱动RM操作 :TC根据决策,驱动每个RM提交或回滚其事务。
- 事务完成 :完成上述流程后,分布式事务执行完毕。
2.2.3 Seata实现步骤
要实现对于下单的一个分布式事务的改造,需要经历以下步骤:
- 引入依赖 :引入Spring Cloud的依赖,并根据项目需求调整版本。
- 添加其它配置文件 :在代码中使用properties文件进行配置,或者使用独立服务的默认配置。
- 数据库建表 :在运行时,Seata需要在数据库中创建表来存储事务相关信息。需要手动执行建表语句。
- 启动Seata Server :下载Seata Server的最新版本,解压缩后执行启动脚本。可以使用默认配置,也可以根据需要进行修改。
- 使用Global Transactional :在代码中添加Global Transactional注解,开启分布式事务功能。
三、分布式锁
3.1 什么是分布式锁
分布式锁是在分布式系统环境下的一种锁机制。在单机系统中,锁能够确保在同一服务内资源访问的同步性。然而,在分布式系统中,由于服务部署在不同的节点上,传统的单机锁已无法满足需求,因此引入了分布式锁。
- 主要使用场景 :多个服务需要操作同一共享资源时,且该共享资源具有不允许被同时访问的特性。例如,在高并发场景下,多个服务同时对库存进行扣减操作,需要使用分布式锁保证操作的原子性。
3.2 Redis实现分布式锁
- 获得锁 :使用Redis的SET NX命令。该命令的作用是设置一个值。在Redis中设置值时,若键(key)为空,则设置成功;若键已被设置,则本次SET NX失败,不做任何动作。通过这一原子性操作实现分布式锁的获取。
- 释放锁 :释放锁的操作即删除对应的值。在使用set nx进行标记时,若value设置成功,代表获取锁成功。若要释放锁,直接删除该值即可。
- 过期时间 :在设置分布式锁时,需考虑客户端获取锁后突然故障的风险。若客户端在获取锁后死机,将无法释放锁,导致其他客户端永远无法获取该锁。因此,必须设置锁的超时或过期时间,以避免此类风险。
3.3 ZooKeeper实现分布式锁
分布式锁的实现可以利用ZooKeeper的以下特性:
- 创建临时顺序节点 :这些节点保证有序且自增。
- 监听机制 :当某一节点发生动作时,可以及时监听到。
实现步骤如下:
- 客户端创建顺序节点,创建后判断该节点是否为当前最小节点。
- 若是最小节点,则代表获取锁成功,可执行业务逻辑。执行完业务逻辑后,通过删除该节点来释放锁。
- 若未抢到最小节点,则设置监听机制于上一个节点。当上一个节点被删除时,当前节点被唤醒,尝试获取锁。
四、RabbitMQ的设计模式
4.1 生产者消费者模式
RabbitMQ的设计模式是生产者 - 消费者模式,该模式不仅在面试中常见,而且是RabbitMQ的内在核心逻辑,因此有必要掌握。
4.1.1 代码实现
-
生产者消费者模式 :通过编写生产者、消费者代码来深入理解该模式。
- 主方法 :新建一个BlockingQ实例,它是实现生产者、消费者的核心。BlockingQ实际上是一个具有阻塞功能的队列。当队列满时,再向里面放入元素,会进行等待;如果从队列中取元素,而队列已经为空,取的时候也会进行等待。
- 生产者 :新建一个生产者Producer,它是一个Runnable。生产者通过死循环向队列中放入新的对象,并通过线程启动生产者Runnable。
- 消费者 :新建一个消费者Consumer,它从队列中取数据。消费者也是通过线程启动,并自动从队列中取数据。
-
程序演示 :在代码中,可以看到两种打印输出:生产者已放置和消费者已消费。这两者交替配合,实现了生产者放置数据、消费者消费数据的模式。
4.2 库存返还
库存返还主要在取消订单的时候进行。在单体项目中,可直接调用MA PR实现库存返还。在微服务架构中,无法直接使用MA PR来更新商品库存,因此,考虑使用Fin来实现此功能。通过Fin接口,可以实现与其他模块的通信,从而更新库存。
4.2.1 微服务中MQ方式实现库存返还
实现库存恢复的方式除了传统方法外,还可以通过MQ的方式来实现。本节课将通过MQ异步实现库存更新。
- 取消订单接口分析 :在订单模块中,为订单添加对MQ的依赖。引入spring - boot - starter - amqp的依赖,其group ID是org.springframework.boot。同样,生产者这边也需要引入这个依赖。依赖引入完毕后,进行配置。
- 配置文件配置网络地址 :配置RabbitMQ的地址、用户认证、虚拟机和连接超时设置等。
- 交换机和队列配置 :在项目order中新建一个包,命名为mq。在mq包中新建一个类,命名为MqConfig。在MqConfig类中,指定队列、交换机,并将它们绑定在一起。
- 发送和接收配置 :在MQ的类中,存在发送消息的组件,即message sender。为了发送消息,首先需要引入Amqp Template。定义一个send方法,传入商品ID和库存参数,使用RabbitMQ Template的convertAndSend方法发送消息。在接收端,新建一个包,命名为mq。在该包中,新建一个名为receiver的组件,指定一个listener,并设置rabbit.listener的queue参数。对接收到的消息进行处理,调用product service中的update stock方法更新库存。
五、跨域问题
5.1 跨域概念
跨域的定义包含三个要求:域名要相同、协议要相同、端口号要相同。前端页面在浏览器中可能会请求来自其他网站的内容,这种行为存在安全风险。为了提高安全性,浏览器通常会对跨域进行限制。
- 具体表现 :例如,在im ook网页中请求百度内容,由于域名不同,会被视为跨域;若im ook网页是http协议,而尝试访问https协议的接口,由于协议不同,同样会被视为跨域;若网页端口为80,而尝试访问8080端口,由于端口号不同,也会被判定为跨域。
5.2 实操解决跨域问题
跨域问题的解决实操:需使用docker及Nginx的docker镜像。首先,进入Nginx镜像中进行配置,目的是配置所有请求的返回,并添加请求头以消除跨域问题。
5.2.1 下载nginx镜像
使用docker pull命令下载nginx镜像。若不指定版本,将默认下载最新版本。
5.2.2 启动nginx
使用命令docker run -d -p 8088:80,将本地的8088端口映射到容器内部的80端口。执行后,会生成一个容器ID,表示容器已成功启动。在浏览器中访问localhost:8088,验证是否可以访问到容器内的服务。
5.2.3 进入容器修改配置文件
使用命令docker exec -it 容器ID /bin/bash进入容器内部。
- 装vim :执行APT - get update命令更新包列表,随后使用APT - get install vim命令下载并安装vim编辑器。
- 验证并修改nginx配置文件 :安装完成后,通过执行nginx -t命令测试nginx当前配置文件的正确性。若显示配置文件正确,但不符合预期设置,则需进行修改。使用vim编辑器打开nginx配置文件,定位至文件末尾,发现该配置将特定文件夹下的所有控制文件均加载进来。需进一步进入该文件夹进行配置修改。
5.2.4 修改配置文件
在vim中打开default.conf文件后,定位到location配置区域。针对跨域配置,主要涉及以下三个关键点:
- 添加请求头Access - Control - Allow - Origin :设置为星号(*),表示接受所有来源的请求。
- 配置允许的HTTP方法 :通过Access - Control - Allow - Methods指定,支持的方法包括GET、POST和OPTIONS。
- 设置自定义请求头 :使用Access - Control - Allow - Headers,添加需要允许的请求头,例如jwt token。此外,还可以包含如keep - alive、user - agent等标准请求头。
完成上述配置后,保存配置文件,并使用命令nginx -s reload重新加载配置,以确保配置生效。在重新加载前,需确认当前请求头中未包含刚配置的内容。
5.2.5 重新读取配置文件
按下回车使NGINX重新读取配置文件,并刷新页面,观察是否发生变化。变化后可见,配置中多出了三个请求头:allow headers、allow methods以及allow origin。添加这些请求头后,浏览器会识别并允许跨域,不再进行拦截。