持续集成

1.项目部署

  • 项目打成jar包
  • Docker部署(项目打成jar包 —>docker镜像文件—>docker容器)
  • K8S

2.持续集成CI

持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,就意味着每天有多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早发现集成错误。

2.1 优点

  • 1.自动构建,发布,测试
  • 2.降低风险

2.2 分类

  • Jenkins[tk旧项目使用]
  • 坎特[tk新项目使用]

3.Jenkins(老头)

Jenkins是一个开源的实现持续集成的软件工具:Jenkins

==原理图:每当我们push代码时候就触发项目完成自动编译和打包==

image-20240812101022218

4.项目部署

微服务部署比较麻烦,所以企业中都会采用持续集成的方式,快捷实现开发、部署一条龙服务。

为了模拟真实环境,我们在虚拟机中已经提供了一套持续集成的开发环境,代码一旦自测完成,push到Git私服后即可自动编译部署。

==原理图:每当我们push代码时候就触发项目完成自动编译和打包==

image-20240812101022218

而开发我们负责的微服务时,则需要在本地启动运行部分微服务。

4.1 虚拟机部署

4.2 本地部署

如果需要运行某个微服务时,只需要以下两步:

  • 第一步:访问Jenkins控制台
  • 第二步:点击对应微服务后面的绿色运行按钮

image-20240812211957065

构建过程中,可以在页面左侧看到构建进度,如果没有说明构建已经结束了(你的机器速度太快了!):

image-20240812212051332

Mysql分表分库-Sharding-Sphere

title: Mysql分表分库-Sharding-Sphere
date: 2024-07-22 09:39:47
tags: Mysql

1.分表分库概念

1.1 分表

开发者自己对表的处理,与数据库无关

  • 从物理上来看,一张表的数据被拆到多个表文件存储了【多张表

  • 从逻辑上来看,【多张表】 — CRUD会变化,需要考虑取哪张表做数据处理

在开发中我们很多情况下业务需求复杂,更看重分表的灵活性。因此,我们大多数情况下都会选择分表方案。

  • 分表的好处:

    • 1.拆分方式更加灵活【可以水平也可以垂直】

    • 2.可以解决单表字段过多问题【垂直分表,分在多个表】

  • 分表的坏处:

    • 1.CRUD需要自己判断访问哪张表
    • 2.垂直拆分还会导致事务问题及数据关联问题:【原本一张表的操作,变为多张表操作,要考虑长事务情况】

而分表有两种分法:

  • 水平分表[根据一些维度横向]

水平分表是将表中的数据行拆分到多个不同的表/数据库,通常是根据某种键值来拆分

  • 垂直分表[部分字段拆分到别的表]

垂直分表是将表中的列拆分到多个不同的表,通常是根据列的使用频率或者业务逻辑来拆分

  • 两者区别
垂直分表 水平分表
拆分依据 列的使用频率/业务逻辑 某种键值[用户ID,时间戳]
目的 优化表结构,提高访问速度 解决单表数据量过大导致性能问题,分散数据到多个表提高查询和更新性能
优点 1.可以减少页面加载时间,只查询必要的列2.降低数据冗余,提高存储效率 1.减少单个表数据量,提高单表管理能力和访问速度2.易于实现,大多数据库中间件都支持水平分表
缺点 1.增加了数据库复杂性,需要更多join连接合并数据2.分表策略要根据业务仔细设计 1.例如联表查询就需要额外的逻辑处理[有的符合行在A,有的符合行在B]2.键值需要谨慎设计
应用场景 表中有大量列,访问只有少数 几个列情况 处理具有明显数据划分界限情况
注意事项 1.分表操作可能会影响数据库的事务管理,需要考虑跨表操作的一致性2.分表策略应该基于实际的业务需求和数据访问模式来设计3.分表后的数据迁移和数据完整性保持是实施过程中最难的挑战

1.1.1 水平分表

例如,对于赛季榜单,我们可以按照赛季拆分为多张表,每一个赛季一张新的表。如图:

image-20240917203826700

这种方式就是水平分表,表结构不变,仅仅是每张表数据不同。查询赛季1,就找第一张表。查询赛季2,就找第二张表。

1.1.2 垂直分表

如果一张表的字段非常多(比如达到30个以上,这样的表我们称为宽表)。宽表由于字段太多,单行数据体积就会非常大,虽然数据不多,但可能表体积也会非常大!从而影响查询效率。

例如一个用户信息表,除了用户基本信息,还包含很多其它功能信息:

image-20240917203845323

1.2 分库[垂直分库]

image-20240722094437153

  • 1.考虑分库的目标:提高读写性能,写入性能还是两者兼顾
  • 2.考虑分片键的选择:通常是表中某个字段
  • 3.考虑分片策略:①范围分片(基于数值范围)②哈希分片(基于哈希算法)③列表分片(基于枚举值)

无论是分区,还是分表,我们刚才的分析都是建立在单个数据库的基础上。但是单个数据库也存在一些问题:

  • 单点故障问题:数据库发生故障,整个系统就会瘫痪【鸡蛋都在一个篮子里】
  • 单库的性能瓶颈问题:单库受服务器限制,其网络带宽、CPU、连接数都有瓶颈【性能有限制】
  • 单库的存储瓶颈问题:单库的磁盘空间有上限,如果磁盘过大,数据检索的速度又会变慢【存储有限制】

综上,在大型系统中,我们除了要做①分表、还需要对数据做②分库—>建立综合集群。

  • 优点:【解决了单个数据库的三大问题】

    • 1.解决了海量数据存储问题,突破了单机存储瓶颈

    • 2.提高了并发能力,突破了单机性能瓶颈

    • 3.避免了单点故障

  • 缺点:

    • 1.成本非常高【要多个服务器,多个数据库】

    • 2.数据聚合统计比较麻烦【因为牵扯多个数据库,有些语句会很麻烦】

    • 3.主从同步的一致性问题【主数据库往从数据库更新,会有不可取消的延误时间,只能通过提高主从数据库网络带宽,机器性能等操作(↓)延误时间】

    • 4.分布式事务问题【因为涉及多个数据库多个表,使用seata分布式事务可以解决】

微服务项目中,我们会按照项目模块,每个微服务使用独立的数据库,因此每个库的表是不同的

image-20240917203647097

2.Sharding-Sphere概念

2.1 Sharding-JDBC框架简介[配置麻烦]

Sharding-JDBC的定位是一款轻量级Java框架,它会以POM依赖的形式嵌入程序,运行期间会和Java应用共享资源,这款框架的本质可以理解成是JDBC的增强版,只不过Java原生的JDBC仅支持单数据源的连接,而Sharding-JDBC则支持多数据源的管理,部署形态如下:

image-20240722094656183

Java-ORM框架在执行SQL语句时,Sharding-JDBC会以切面的形式拦截发往数据库的语句,接着根据配置好的数据源、分片规则和路由键,为SQL选择一个目标数据源,然后再发往对应的数据库节点处理。

Sharding-JDBC在整个业务系统中对性能损耗极低,但为何后面又会推出Sharding-Proxy呢?因为Sharding-JDBC配置较为麻烦,比如在分布式系统中,任何使用分库分表的服务都需要单独配置多数据源地址、路由键、分片策略….等信息,同时它也仅支持Java语言,当一个系统是用多语言异构的,此时其他语言开发的子服务,则无法使用分库分表策略。

2.2 Sharding-Proxy中间件简介[成本过大]

也正是由于配置无法统一管理、不支持异构系统的原因,后面又引入Sharding-Proxy来解决这两个问题,Sharding-Proxy可以将其理解成一个伪数据库,对于应用程序而言是完全透明的,它会以中间件的形式独立部署在系统中,部署形态如下:

image-20240722095156667

使用Sharding-Proxy的子服务都会以连接数据库的形式,与其先建立数据库连接,然后将SQL发给它执行,Sharding-Proxy会根据分片规则和路由键,将SQL语句发给具体的数据库节点处理,数据库节点处理完成后,又会将结果集返回给Sharding-Proxy,最终再由它将结果集返回给具体的子服务。

Sharding-Proxy虽然可以实现分库分表配置的统一管理,以及支持异构的系统,但因为需要使用独立的机器部署,同时还会依赖Zookeeper作为注册中心,所以硬件成本会直线增高,至少需要多出3~4台服务器来部署。

同时SQL执行时,需要先发给Proxy,再由Proxy发给数据库节点,执行完成后又会从数据库返回到Proxy,再由Proxy返回给具体的应用,这个过程会经过四次网络传输的动作,因此相较于原本的Sharding-JDBC来说,性能、资源开销更大,响应速度也会变慢。

2.3 JDBC、Proxy混合部署模式[取长补短]

如果用驱动式分库分表,虽然能够让Java程序的性能最好,但无法支持多语言异构的系统,但如果纯用代理式分库分表,这显然会损害Java程序的性能,因此在Sharding-Sphere中也支持JDBC、Proxy做混合式部署,也就是Java程序用JDBC做分库分表,其他语言的子服务用Proxy做分库分表,部署形态如下:

image-20240722095231817

这种混合式的部署方案,所有的数据分片策略都会放到Zookeeper中统一管理,然后所有的子服务都去Zookeeper中拉取配置文件,这样就能很方便的根据业务情况,来灵活的搭建适用于各种场景的应用系统,这样也能够让数据源、分片策略、路由键….等配置信息灵活,可以在线上动态修改配置信息,修改后能够在线上环境中动态感知。

Sharding-Sphere还提供了一种单机模式,即直接将数据分片配置放在Proxy中,但这种方式仅适用于开发环境,因为无法将分片配置同步给多个实例使用,也就意味着会导致其他实例由于感知不到配置变化,从而造成配置信息不一致的错误。

3.Sharding-Sphere核心概念—路由键/分片算法

  • 路由键/分片键:作为数据分片的基准字段[可以是一个/多个字段组成]

  • 分片算法:基于路由键做一定逻辑处理,从而计算出一个最终节点位置的算法

举例:好比按user_id将用户表数据分片,每八百万条数据划分一张表。user_id就是路由键,而按user_id做范围判断则属于分片算法,一张表中的所有数据都会依据这两个基础,后续对所有的读写SQL进行改写,从而定位到具体的库、表位置。

4.Sharding-Sphere分表分库的工作流程

image-20240722101404319

  • 逻辑表:提供给应用程序操作的表名,程序可以像操作原本的单表一样,灵活的操作逻辑表(逻辑表并不是一种真实存在的表结构,而是提供给Sharding-Sphere使用的)

  • 真实表:在各个数据库节点上真实存在的物理表,但表名一般都会和逻辑表存在偏差。

  • 数据节点:主要是用于定位具体真实表的库表名称,如DB1.tb_user1、DB2.tb_user2.....

    • 均匀分布:指一张表的数量在每个数据源中都是一致的。
    • 自定义分布:指一张表在每个数据源中,具体的数量由自己来定义,上图就是一种自定义分布。

Java为例:

编写业务代码的SQL语句直接基于逻辑表操作;当Sharding-Sphere接收到一条操作某张逻辑表的SQL语句—–已配置好的路由键和分片算法—–>对相应的SQL语句进行解析,然后计算出SQL要落入的数据节点(是哪个真实表),最后再将语句发给具体的真实表上处理即可

JDBC和Proxy的主要区别就在于:解析SQL语句计算数据节点的时机不同

  • JDBC是在Java程序中就完成相应计算,从Java程序中发出SQL语句就已经是操作真实表的SQL
  • Proxy是在Java程序外做解析工作,它会接收程序操作逻辑表的SQL语句。然后再做解析得到具体要操作的真实表,然后再执行,同时Proxy还要作为应用程序和数据库之间,传输数据的中间人

5.Sharding-Sphere概念—表

5.1 绑定表[解决主外键数据落不同库产生跨库查询]

  • 现有问题:

多张表之间存在物理或逻辑上的主外键关系,如果无法保障同一主键值的外键数据落入同一节点,显然在查询时就会发生跨库查询,这无疑对性能影响是极大的。

image-20240722110038729

  • 解决方案:

比如:前面案例中的order_id、order_info_id可以配置一组绑定表关系,这样就能够让订单详情数据随着订单数据一同落库,简单的说就是:配置绑定表的关系后,外键的表数据会随着主键的表数据落入同一个库中,这样在做主外键关联查询时,就能有效避免跨库查询的情景出现。

5.2 广播表[解决跨库join问题]

  • 现有问题:

当有些表需要经常被用来做连表查询时,这种频繁关联查询的表,如果每次都走跨库Join,这显然又会造成一个令人头疼的性能问题。

image-20240722110446940

  • 解决方案:

对于一些经常用来做关联查询的表,就可以将其配置为广播表

image-20240722112901134

广播表是一种会在所有库中都创建的表,以系统字典表为例,将其配置为广播表之后,向其增、删、改一条或多条数据时,所有的写操作都会发给全部库执行,从而确保每个库中的表数据都一致,后续在需要做连表查询时,只需要关联自身库中的字典表即可,从而避免了跨库Join的问题出现。

5.3 单表[不分表分库]

单表的含义比较简单,并非所有的表都需要做分库分表操作,所以当一张表的数据无需分片到多个数据源中时,就可将其配置为单表,这样所有的读写操作最终都会落入这一张单表中处理。

5.4 动态表

动态表是指表会随着数据增长、或随着时间推移,不断的去创建新表,如下:

image-20240722134127808

Sharding-Sphere中可以直接支持配置,无需自己去从头搭建,因此实现起来尤为简单,配置好之后会按照时间或数据量动态创建表。

6.Sharding-Sphere数据分片策略

分库分表之后读写操作具体会落入哪个库中,这是根据路由键和分片算法来决定的

Sharding-Sphere中的数据分片策略又分为:

  • 1.内置的自动化分片算法:[取模分片、哈希分片、范围分片、时间分片等这积累常规算法]

  • 2.用户自定义的分片算法:[标准分片、复合分片、强制分片]

  • 2.1 标准分片算法:适合基于单一路由键进行=、in、between、>、<、>=、<=...进行查询的场景。

  • 2.2 复合分片算法:适用于多个字段组成路由键的场景,但路由算法需要自己继承接口重写实现。

  • 2.3 强制分片算法:适用于一些特殊SQL的强制执行,在这种模式中可以强制指定处理语句的节点。

综上所述,在Sharding-Sphere内部将这四种分片策略称为:Inline、Standard、Complex、Hint,分别与上述四种策略一一对应,但这四种仅代表四种策略,具体的数据分片算法,可以由使用者自身来定义。

7.Sharding-Sphere分库方式

Sharding-Sphere生态中,支持传统的主从集群分库,[如搭建出读写分离架构、双主双写架构],同时也支持按业务进行垂直分库,也支持对单个库进行横向拓展,做到水平分库。

但通常都是用它来实现水平分库和读写分离,因为分布式架构的系统默认都有独享库的概念,也就是分布式系统默认就会做垂直分库,因此无需引入Sharding-Sphere来做垂直分库。

==Sharding-Sphere实际操作==

之前提到过,Sharding-Sphere的所有产品对业务代码都是零侵入的,无论是Sharding-JDBC也好,Sharding-Proxy也罢,都不需要更改业务代码,这也就意味着大家在分库分表环境下做业务开发时,可以像传统的单库开发一样轻松。

  • Sharding-Sphere中最主要的是对配置文件的更改
  • Sharding-JDBC主要修改application.properties/yml文件
  • Sharding-Proxy主要修改自身的配置文件

1.配置yml文件[业务代码零侵入]

1
//后期补充

==Sharding-Sphere工作原理==

1.核心工作步骤

其核心工作步骤会分为如下几步:

  • • 配置加载:在程序启动时,会读取用户的配置好的数据源、数据节点、分片规则等信息。
  • SQL解析:SQL执行时,会先根据配置的数据源来调用对应的解析器,然后对语句进行拆解。
  • SQL路由:拆解SQL后会从中得到路由键的值,接着会根据分片算法选择单或多个数据节点。
  • SQL改写:选择了目标数据节点后,接着会改写、优化用户的逻辑SQL,指向真实的库、表。
  • SQL执行:对于要在多个数据节点上执行的语句,内部开启多线程执行器异步执行每条SQL
  • • 结果归并:持续收集每条线程执行完成后返回的结果集,最终将所有线程的结果集合并。
  • • 结果处理:如果SQL中使用了order by、max()、count()...等操作,对结果处理后再返回。

整个Sharding-Sphere大致工作步骤如上,这个过程相对来说也比较简单,但具体的实现会比较复杂,针对于不同的数据库,内部都会实现不同的解析器,如MySQLMySQL的解析器,PgSQL也会有对应的解析器,同时还会做SQL语句做优化。而SQL路由时,除开要考虑最基本的数据分片算法外,还需要考虑绑定表、广播表等配置,来对具体的SQL进行路由。

2.分库分表产品对比

对比项 Sharding-JDBC Sharding-Proxy MyCat
性能开销 较低 较高
异构支持 不支持 支持 支持
网络次数 最少一次 最少两次 最少两次
异构语言 仅支持Java 支持异构 支持异构
数据库支持 任意数据库 MySQL、PgSQL 任意数据库
配置管理 去中心化 中心化 中心化
部署方式 依赖工程 中间件 中间件
业务侵入性 较低
连接开销
事务支持 XA、Base、Local事务 同前者 XA事务
功能丰富度 一般
社区活跃性 活跃 活跃 一言难尽
版本迭代性 极低
多路由键支持 2 2 1
集群部署 支持 支持 支持
分布式序列 雪花算法 雪花算法 自增序列

Mysql排查

1.前言

在程序开发与运行过程中,出现Bug问题的几率无可避免,数据库出现问题一般会发生在下述几方面:

  • ①撰写的SQL语句执行出错,俗称为业务代码Bug

  • ②开发环境执行一切正常,线上偶发SQL执行缓慢的情况。

  • ③线上部署MySQL的机器故障,如磁盘、内存、CPU100%MySQL自身故障等。

1.1 线上排查和解决问题思路

相对而言,解决故障问题也好,处理性能瓶颈也罢,通常思路大致都是相同的,步骤如下:

  • ①分析问题:根据理论知识+经验分析问题,判断问题可能出现的位置或可能引起问题的原因,将目标缩小到一定范围。
  • ②排查问题:基于上一步的结果,从引发问题的“可疑性”角度出发,从高到低依次进行排查,进一步排除一些选项,将目标范围进一步缩小。
  • ③定位问题:通过相关的监控数据的辅助,以更“细粒度”的手段,将引发问题的原因定位到精准位置。
  • ④解决问题:判断到问题出现的具体位置以及引发的原因后,采取相关措施对问题加以解决。
  • ⑤尝试最优解(非必须):将原有的问题解决后,在能力范围内,且环境允许的情况下,应该适当考虑问题的最优解(可以从性能、拓展性、并发等角度出发)。

我的解决方案:

当然,上述过程是针对特殊问题以及经验老道的开发者而言的,作为“新时代的程序构建者”,那当然得学会合理使用工具来帮助我们快速解决问题:

  • ①摘取或复制问题的关键片段。
  • ②打开百度或谷歌后粘贴搜索。
  • ③观察返回结果中,选择标题与描述与自己问题较匹配的资料进入。
  • ④多看几个后,根据其解决方案尝试解决问题。
  • ⑤成功解决后皆大欢喜,尝试无果后“找人/问群”。
  • ⑥“外力”无法解决问题时自己动手,根据之前的步骤依次排查解决。

1.2 线上排查方向

==①发生问题的大体定位,②逐步推导出具体问题的位置==

  • 1.应用程序本身导致的问题

    • 程序内部频繁触发GC,造成系统出现长时间停顿,导致客户端堆积大量请求。
    • JVM参数配置不合理,导致线上运行失控,如堆内存、各内存区域太小等。【遇到启动项目OOM,在idea创建设置堆空间大小700到10000解决】
    • Java程序代码存在缺陷,导致线上运行出现Bug,如死锁/内存泄漏、溢出等。
    • 程序内部资源使用不合理,导致出现问题,如线程/DB连接/网络连接/堆外内存等。
  • 2.上下游内部系统导致的问题

    • 上游服务出现并发情况,导致当前程序请求量急剧增加,从而引发问题拖垮系统。
    • 下游服务出现问题,导致当前程序堆积大量请求拖垮系统,如Redis宕机/DB阻塞等。
  • 3.程序所部署的机器本身导致的问题

    • 服务器机房网络出现问题,导致网络出现阻塞、当前程序假死等故障。
    • 服务器中因其他程序原因、硬件问题、环境因素(如断电)等原因导致系统不可用。
    • 服务器因遭到入侵导致Java程序受到影响,如木马病毒/矿机、劫持脚本等。
  • 4.第三方的RPC远程调用导致的问题

    • 作为被调用者提供给第三方调用,第三方流量突增,导致当前程序负载过重出现问题。
    • 作为调用者调用第三方,但因第三方出现问题,引发雪崩问题而造成当前程序崩溃。

==——三大类错误排查——==

2.Sql语句执行出错—排查

作为一个程序员,对MySQL数据库而言,接触最多的就是SQL语句的撰写,和写业务代码时一样,写代码时会碰到异常、错误,而写SQL时同样如此,比如:

1
2
ERROR 1064 (42000):
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xxxxxxx' at line 1

Mysql的错误信息会由三部分组成:

  • ErrorCode:错误码【1064这种】

  • SQLState:Sql状态【42000这种】

  • ErrorInfo:错误详情【在;之后跟一长串描述具体错误详情】

Mysql的错误类型:

  • 根据ErrorInfo位置根据错误类型定位,认真对准之后百度搜索
  • 没有定位,只能通过SQLState和网上办法解决

3.Mysql线上慢查询语句—排查

有些SQL可能在开发环境没有任何问题,但放到线上时就会出现偶发式执行耗时较长的情况,所以这类情况就只能真正在线上环境才能测出来,尤其是一些不支持灰度发布的中小企业,也只能放到线上测才能发现问题。

3.1 打开Mysql慢查询日志

一般在上线前,Mysql手动打开慢查询日志:

1
2
3
4
5
6
7
8
开启慢查询日志需要配置两个关键参数:
• slow_query_log:取值为on/off[默认]-----项目上线前需要手动开启。
• long_query_time:指定记录慢查询日志的阈值,[单位是秒,指定更细粒度用小数表示]-----阈值根据不同的业务系统取值也不同

①设置一个大概值,灰度发布时走正式运营场景效果更好
②开启查询日志,压测所有业务,紧接着分析查询日志中sql的平均耗时,再根据正常的sql执行时间,设置一个偏大的慢查询阈值即可

---公司内设置的是3s

3.2 查看Mysql慢查询日志

查看慢查询日志的方式:

  • 拥有完善的监控系统:【自动】读取磁盘中的慢查询日志,然后可以通过监控系统大屏观察
  • 未拥有完善的监控系统:linux系统通过cat类指令查看本地日志文件/windows记事本打开

image-20240710150212444

从上面日志中记录的查询信息来看,可以得知几个信息:

  • • 执行慢查询SQL的用户:root,登录IP为:localhost[127.0.0.1]
  • • 慢查询执行的具体耗时为:0.014960s,锁等待时间为0s
  • • 本次SQL执行后的结果集为4行数据,累计扫描6行数据。
  • • 本次慢查询发生在db_zhuzi这个库中,发生时间为1667466932(2022-11-03 17:15:32)
  • • 最后一行为具体的慢查询SQL语句。

3.3 排查sql执行缓慢问题

通过3.2步骤我们读取慢查询日志后,能够精准定位到发生慢查询Sql的用户、客户端机器、执行耗时、锁阻塞耗时、结果集行数、扫描行数、发生的库和事件、具体的慢查询sql语句。

得到这些信息之后,其实排查引起慢查询的原因就通过以下步骤就可以:

  • ①根据本地慢查询日志文件中的记录,得到具体慢查询sql执行的相关信息
  • ②查看lock_time的耗时,判断本次执行缓慢是否由于并发事务导致的长时间阻塞【多半原因】
    • 2.1 如果是,是由于并发事务导致的长时间阻塞【并发事务抢占锁,造成当前事务长时间无法获取锁资源】,看到大量由于锁阻塞导致执行超过阈值,那就执行查看mysql锁状态,如果值都比较大意味着当前这个mysql节点承担的并发压力过大,急需mysql架构优化
    • 2.2 如果不是,通过①explain索引分析工具,先判断索引使用情况,找到那些执行计划中扫描行数过多、type=index/allSQL语句,尝试优化掉即可;②人肉排查法解决

一般来说在开发环境中没有问题的SQL语句,放到线上环境出现执行缓慢的情况,多半原因是由于并发事务抢占锁,造成当前事务长时间无法获取锁资源,因此导致当前事务执行的SQL出现超时,这种情况下需要去定位操作相同行数据的大事务,一般长时间的阻塞是由于大事务持有锁导致的,找出对应的大事务并拆解或优化掉即可。【基本就是操作相同行数据的大事务持有锁

3.3.1 长时间锁阻塞的排查方法[查看lock_time时间]

通过show status like 'innodb_row_lock_%';命令可以查询MySQL整体的锁状态,如下:

image-20240710171033106

  • Innodb_row_lock_current_waits:当前正在阻塞等待锁的事务数量。
  • Innodb_row_lock_time:MySQL启动到现在,所有事务总共阻塞等待的总时长。
  • Innodb_row_lock_time_avg:平均每次事务阻塞等待锁时,其平均阻塞时长。
  • Innodb_row_lock_time_maxMySQL启动至今,最长的一次阻塞时间。
  • Innodb_row_lock_waitsMySQL启动到现在,所有事务总共阻塞等待的总次数。

3.3.2 非锁阻塞的排查方法[explain/拆分语句]

  • 方法一:explain解释方法:

找到那些执行计划中扫描行数过多、type=index/allSQL语句,尝试优化掉即可

image-20240710171706075

select_type字段:展示查询的类型(简单查询,联合查询,子查询等)—可以往join连接,避免子查询

partitions字段:展示查询涉及的分区(mysql5.1引出的,解决单表问题)—-可以往分区上优化

type字段:展示链接类型,反映mysql如何查找表的行—可以往system(系统表查询)/const(主键/唯一索引)

possible_keys和key字段:mysql认为可以使用的索引和实际使用的索引—可以往一致优化

filtered字段:mysql认为where符合(返回结果的行数/总行数)的百分比—可以往100%优化[where字段优化]

extra字段:一些额外操作—可以往①Using index索引覆盖;②Using where使用where子句过滤行

  • 方法二:人肉排查法:

【对于一些较为复杂或庞大的业务需求,可以采取拆分法去逐步实现,最后组装所有的子语句,最终推导出符合业务需求的SQL语句】

一条复杂的查询语句,拆解成一条条子语句,对每条子语句使用explain工具分析,精准定位到:复杂语句中导致耗时较长的具体子语句,最后将这条子语句优化后重新组装即可。

【拆解排除法有一个最大的好处是:有时组成复杂SQL的每条子语句都不存在问题,也就是每条子语句的执行效率都挺不错的,但是拼到一起之后就会出现执行缓慢的现象,这时拆解后就可以一步步的将每条子语句组装回去,每组装一条子语句都可以用explain工具分析一次,这样也能够精准定位到是由于那条子语句组合之后导致执行缓慢的,然后进行对应优化即可。】

4.Mysql线上机器故障排查

MySQL数据库线上的机器故障主要分为两方面,①是由于MySQL自身引起的问题,比如连接异常、死锁问题等,②是部署MySQL的服务器硬件文件,如磁盘、CPU100%等现象,对于不同的故障问题排查手段也不同,下面将展开聊一聊常见的线上故障及解决方案。

4.1 客户端连接异常

当数据库出现连接异常时,基本上就是因为四种原因导致:

【①②比较简单,设置两者参数就行】

  • ①数据库总体的现有连接数,超出了MySQL中的最大连接数,此时再出现新连接时会出异常。【遇到过,直接更新参数,加大核心线程数即可】
  • ②客户端数据库连接池与MySQL版本不匹配,或超时时间过小,也可能导致出现连接中断。

【③④比较特殊】

  • MySQL、Java程序所部署的机器不位于同一个网段,两台机器之间网络存在通信故障。
  • ④部署MySQL的机器资源被耗尽,如CPU、硬盘过高,导致MySQL没有资源分配给新连接。

其中,介绍一下③④情况:

MySQL、Java程序所部署的机器不位于同一个网段,两台机器之间网络存在通信故障

这种情况,问题一般都出在交换机上面,由于Java程序和数据库两者不在同一个网段,所以相互之间通信需要利用交换机来完成,但默认情况下,交换机和防火墙一般会认为时间超过3~5分钟的连接是不正常的,因此就会中断相应的连接,而有些低版本的数据库连接池,如Druid只会在获取连接时检测连接是否有效,此时就会出现一个问题:

交换机把两个网段之间的长连接嘎了,但是Druid因为只在最开始检测了一次,后续不会继续检测连接是否有效,所以会认为获取连接后是一直有效的,最终就导致了数据库连接出现异常(后续高版本的Druid修复了该问题,可以配置间隔一段时间检测一次连接

一般如果是由于网络导致出现连接异常,通常排查方向如下:

  • • 检测防火墙与安全组的端口是否开放,或与外网机器是否做了端口映射。
  • • 检查部署MySQL的服务器白名单,以及登录的用户IP限制,可能是IP不在白名单范围内。
  • • 如果整个系统各节点部署的网段不同,检查各网段之间交换机的连接超时时间是多少。
  • • 检查不同网段之间的网络带宽大小,以及具体的带宽使用情况,有时因带宽占满也会出现问题。
  • • 如果用了MyCat、MySQL-Proxy这类代理中间件,记得检查中间件的白名单、超时时间配置。

一般来说上述各方面都不存在问题,基本上连接异常应该不是由于网络导致的问题,要做更为细致的排查,可以在请求链路的各节点上,使用网络抓包工具,抓取对应的网络包,看看网络包是否能够抵达每个节点,如果每个节点的出入站都正常,此时就可以排除掉网络方面的原因。

④部署MySQL的机器资源被耗尽,如CPU、硬盘过高,导致MySQL没有资源分配给新连接。

这种情况更为特殊,网络正常、连接数未满、连接未超时、数据库和客户端连接池配置正常….,在一切正常的情况下,有时候照样出现连接不上MySQL的情况咋整呢?在这种情况下基本上会陷入僵局,这时你可以去查一下部署MySQL服务的机器,其硬件的使用情况,如CPU、内存、磁盘等,如果其中一项达到了100%,这时就能够确定问题了!

1
因为数据库连接的本质,在MySQL内部是一条条的工作线程,要牢记的一点是:操作系统在创建一条线程时,都需要为其分配相关的资源,如果一个客户端尝试与数据库建立新的连接时,此刻正好有一个数据库连接在执行某个操作,导致CPU被打满,这时就会由于没有资源来创建新的线程,因此会向客户端直接返回连接异常的信息。

先找到导致资源耗尽的连接/线程,然后找到它当时正在执行的SQL语句,最后需要优化相应的SQL语句后才能彻底根治问题。

4.2 Mysql死锁频发[查看innodb存储引擎运行状态日志]

MySQL内部其实会【默认】开启死锁检测算法,当运行期间出现死锁问题时,会主动介入并解除死锁,但要记住:虽然数据库能够主动介入解除死锁问题,但这种方法治标不治本因为死锁现象是由于业务不合理造成的,能出现一次死锁问题,自然后续也可能会多次出现,因此优化业务才是最好的选择,这样才能根治死锁问题。

从业务上解决死锁问题:①先定准定位到产生死锁的SQL语句,根据查看innodb存储引擎的运行状态日志【找到内部latest detected deadlock区域日志】

例如:
image-20240710183226640

在上面的日志中,基本上已经写的很清楚了,在2022-11-04 23:04:34这个时间点上,检测到了一个死锁出现,该死锁主要由两个事务产生,SQL如下:

  • (1):UPDATEzz_accountSET balance = balance + 888 WHERE user_name = "熊猫";
  • (2):UPDATEzz_accountSET balance = balance + 666 WHERE user_name = "竹子";

在事务信息除开列出了导致死锁的SQL语句外,还给出了两个事务对应的线程ID、登录的用户和IP、事务的存活时间与系统线程ID、持有的锁信息与等待的锁信息….

除开两个发生死锁的事务信息外,倒数第二段落还给出了两个事务在哪个锁上产生了冲突,以上述日志为例,发生死锁冲突的地点位于db_zhuzi库中zz_account表的主键上,两个事务都在尝试获取对方持有的X排他锁,后面还给出了具体的页位置、内存地址….。

最后一条信息中,给出了MySQL介入解除死锁的方案,也就是回滚了事务(2)的操作,强制结束了事务(2)并释放了其持有的锁资源,从而能够让事务(1)继续运行。

经过查看上述日志后,其实MySQL已经为我们记录了产生死锁的事务、线程、SQL、时间、地点等各类信息,因此想要彻底解决死锁问题的方案也很简单了,根据日志中给出的信息,去找到执行相应SQL的业务和库表,优化SQL语句的执行顺序,或SQL的执行逻辑,从而避免死锁产生即可。

最后要注意:如果是一些偶发类的死锁问题,也就是很少出现的死锁现象,其实不解决也行,毕竟只有在一些特殊场景下才有可能触发,重点是要关注死锁日志中那些频繁出现的死锁问题,也就是多次死锁时,每次死锁出现的库、表、字段都相同,这种情况时需要额外重视并着手解决。

4.3 服务器CPU100%[两种思路]

可能出现两种情况:

  • ①业务活动:突然大量流量进来活动后cpu占用率就会下降

  • ②cpu长期占用率过高:程序有那种循环次数超级多,甚至出现死循环

排查思路:

  • ①先找到CPU过高的服务器

  • ②然后在其中定位到具体的进程。【top指令】

  • ③再定位到进程中具体的线程。【top -Hp xxxx】 xxxx就是②查出来的PID进程号

  • ④再查看线程正在执行的代码逻辑–会显示线程是属于Java/Mysql

    • 4.1 Java层面:该线程的PID转换为16进制,然后进一步排查日志grep 查询接口信息
    • 4.2 Mysql层面:mysql5.7以下查找innodb运行状态日志的某个部分/mysql5.7以上通过threads表信息查找】
  • ⑤最后从代码层面着手优化掉即可。

②先使用top指令查看系统后台的进程状态:

image-20240710183927468

从如上结果中不难发现,PID76661MySQL进程对CPU的占用率达到99.9%,此时就可以确定,机器的CPU利用率飙升是由于该进程引起的。

③根据top -Hp [PID]指令查看进程中cpu占用率最高的线程:

image-20240710184121817

top -Hp 76661命令的执行结果中可以看出:其他线程均为休眠状态,并未持有CPU资源,而PID为77935的线程对CPU资源的占用率却高达99.9%

到此时,导致CPU利用率飙升的“罪魁祸首”已经浮现水面,但此时问题来了!在如果这里是Java程序,此时可以先将该线程的PID转换为16进制的值,然后进一步排查日志信息来确定具体线程执行的业务方法。但此时这里是MySQL程序,咱们得到了操作系统层面的线程ID后,如何根据这个IDMySQL中找到对应的线程呢?

④分为Mysql5.7以上和Mysql5.7以下两种情况:

  • MySQL5.7及以上的版本中,MySQL会自带一个名为performance_schema的库,在其中有一张名为threads的表,其中表中有一个thread_os_id字段,其中会保存每个连接/工作线程与操作系统线程之间的关系(在5.7以下的版本是隐式的,存在于MySQL内部无法查看)。

image-20240710185143259

可以通过查询threads表,输出所有已经创建的线程:【select查询–对应processlist_info就是对应的sql语句】

image-20240710185225772

从上述中可以明显看出MySQL线程和OS线程之间的关系,当通过前面的top指令拿到CPU利用率最高的线程ID后,在再这里找到与之对应的MySQL线程,同时也能够看到此线程正在执行的SQL语句,最后优化对应SQL语句的逻辑即可。

  • MySQL5.7以下的版本中,我们只能通过Innodb存储引擎状态表的transactions板块查看,

统计着所有存活事务的信息,此时也可以从中得到相应的OS线程、MySQL线程的映射关系

image-20240710185424434

是这种方式仅能够获取到OS线程、MySQL线程之间的映射关系,无法获取到对应线程/连接正在执行的SQL语句,此时如果线程还在运行,则可以通过show processlist;查询,如下:

image-20240710185448934

但这种方式只能看到正在执行的SQL语句,无法查询到最近执行过的语句,所以这种方式仅适用于:==线上SQL还在继续跑的情况==。

4.4 Mysql刷盘100%

指磁盘IO达到100%利用率,这种情况下一般会导致其他读写操作都被阻塞,因为操作系统中的IO总线会被占满,无法让给其他线程来读写数据,先来总结一下出现磁盘IO占用过高的原因:

  • • ①突然大批量变更库中数据,需要执行大量写入操作,如主从数据同步时就会出现这个问题。
  • • ②MySQL处理的整体并发过高,磁盘I/O频率跟不上,比如是机械硬盘材质,读写速率过慢。
  • • ③内存中的BufferPool缓冲池过小,大量读写操作需要落入磁盘处理,导致磁盘利用率过高。
  • • ④频繁创建和销毁临时表,导致内存无法存储临时表数据,因而转到磁盘存储,导致磁盘飙升。
  • • ⑤执行某些SQL时从磁盘加载海量数据,如超12张表的联查,并每张表数据较大,最终导致IO打满。
  • • ⑥日志刷盘频率过高,其实这条是①、②的附带情况,毕竟日志的刷盘频率,跟整体并发直接挂钩。

一般情况下,磁盘IO利用率居高不下,甚至超过100%,基本上是由于上述几个原因造成的,当需要排查磁盘IO占用率过高的问题时,可以先通过iotop工具找到磁盘IO开销最大的线程,然后利用pstack工具查看其堆栈信息,从堆栈信息来判断具体是啥原因导致的,如果是并发过高,则需要优化整体架构。如果是执行SQL加载数据过大,需要优化SQL语句……

磁盘利用率过高的问题其实也比较好解决,方案如下:

  • • ①如果磁盘不是SSD材质,请先将磁盘升级成固态硬盘,MySQLSSD硬盘做了特殊优化。
  • • ②在项目中记得引入Redis降低读压力,引入MQ对写操作做流量削峰。
  • • ③调大内存中BufferPool缓冲池的大小,最好设置成机器内存的70~75%左右。
  • • ④撰写SQL语句时尽量减少多张大表联查,不要频繁的使用和销毁临时表。

基本上把上述工作都做好后,线上也不会出现磁盘IO占用过高的问题,对于前面说到的:利用iotop、pstack工具排查的过程,就不再做实际演示了,其过程与前面排查CPU占用率过高的步骤类似,大家学习iotop、pstack两个工具的用法后,其实实操起来也十分简单。

Elasticsearch-黑马商城为例

1.启动ES

1.1 安装elasticsearch

通过下面的Docker命令即可安装单机版本的elasticsearch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#先在tar所在目录下打开cmd
docker load -i es.tar

#创建一个网络【不然kibana不能连接es,踩坑了!!】
docker network create elastic

#黑马安装:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ #配置jvm的内存
-e "discovery.type=single-node" \ #配置运行模式【单点模式/集群模式】
-v es-data:/usr/share/elasticsearch/data \ #挂载
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hm-net \
-p 9200:9200 \ #访问http端口
-p 9300:9300 \ #集群使用
elasticsearch:7.12.1

#csdn安装:
docker run -d --name es -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" --privileged --network elastic -p 9200:9200 -p 9300:9300 elasticsearch:7.12.1

启动之后访问http://localhost:9200/就可以看到elasticsearch信息:

image-20240507204417602

1.2 安装Kibana

通过下面的Docker命令,即可部署Kibana:

1
2
3
4
5
6
7
8
9
10
11
12
13
#先在tar所在目录下打开cmd
docker load -i kibana.tar

#黑马安装:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \ #es的地址,这里的es要和es配置docker的时候--name一致
--network=hm-net \ #网络和es一个网络
-p 5601:5601 \
kibana:7.12.1 #要保证和es版本一致!!!

#csdn安装:
docker run -d --name kibana -e ELASTICSEARCH_HOSTS=http://es:9200 --network elastic -p 5601:5601 kibana:7.12.1

启动之后访问http://localhost:5601/就可以通过kibana数据化访问elasticsearch:

image-20240507204635028

可以点击右上角Dev tools,进入开发工具页面:

image-20240507204914788

点击之后:

image-20240507205135009

2.改造操作步骤

在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。

2.1 初始化RestClient

2.1.1 引入RestHighLevelClient依赖

在微服务模块中引入esRestHighLevelClient依赖:

1
2
3
4
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2.1.2 覆盖ES版本

因为SpringBoot默认的ES版本是7.17.10,所以我们需要覆盖默认的ES版本【黑马商城是在pom.xml中修改】:

1
2
3
4
5
6
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<!--覆盖成7.12.1-->
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>

2.1.3 初始化RestHighLevelClient

1
2
3
4
5
6
RestHighLevelClient client = new RestHighLevelClient(
//使用RestClient的builder方法创建
RestClient.builder(
HttpHost.create("http://192.168.xxx.xxx:9200")
)
);

2.2 分析Mysql设计ES实现

我们针对购物车数据库进行分析:

image-20240520172813812

我们可以对购物车的所有字段进行分析,判断哪些字段必须添加到ElasticSearch中,判断哪些字段必须添加搜索功能。从而进行新建索引库和映射:

image-20240520171754450

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
PUT /items
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"name":{
"type": "text",
"analyzer": "ik_max_word"
},
"price":{
"type": "integer"
},
"stock":{
"type": "integer"
},
"image":{
"type": "keyword",
"index": false
},
"category":{
"type": "keyword"
},
"brand":{
"type": "keyword"
},
"sold":{
"type": "integer"
},
"commentCount":{
"type": "integer",
"index": false
},
"isAD":{
"type": "boolean"
},
"updateTime":{
"type": "date"
}
}
}
}

2.3 索引库操作(client.indices.xxx)

JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤:

  • 1.初始化RestHighLevelClient类对象client【创建客户端】
  • 2.创建XxxIndexRequest对象request【XXX是CreateGetDelete
  • 3.准备请求参数request.source()方法【只有新增Create需要参数,其他情况不需要】
  • 4.发送请求client.indices().xxx()方法【xxx是createexistsdelete

2.3.1 创建索引库

image-20240520173351287

2.3.2 删除索引库

image-20240521135115905

2.3.2 查询索引库

2.4 文档操作(client.xxx)

文档操作的基本步骤:

  • 1.初始化RestHighLevelClient类对象client【创建客户端】
  • 2.创建XxxRequest对象request【Xxx是IndexUpdateDeleteBulk
  • 3.准备请求参数request.source()方法(IndexUpdateBulk时需要)
  • 4.发送请求client.Xxx()方法【Xxx是indexgetupdatedeletebulk
  • 5.解析结果(Get查询时需要,数据在_source内部)

2.4.1 新增文档

  • 1.创建Request对象,这里是IndexRequest,因为添加文档就是创建倒排索引的过程
  • 2.准备请求参数,本例中就是Json文档
  • 3.发送请求【client.index()方法就好了】

image-20240521142712455

2.4.2 查询文档

与之前的流程类似,代码大概分2步:

  • 创建Request对象
  • 准备请求参数,这里是无参,【直接省略】
  • 发送请求
  • 解析结果【因为结果在_source部分内】

image-20240521142844007

可以看到,响应结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可

2.4.3 删除文档

与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是2步走:

  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id
  • 2)准备参数,无参,直接省略
  • 3)发送请求。因为是删除,所以是client.delete()方法

image-20240521143043972

2.4.4 修改文档

修改我们讲过两种方式:

  • 全量修改:本质是先根据id删除,再新增【与新增文档】
  • 局部修改:修改文档中的指定字段值

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改
  • 如果新增时,ID不存在,则新增

这里不再赘述,我们主要关注局部修改的API即可

image-20240521143147541

2.4.5 批量导入文档

因此BulkRequest中提供了add方法,用以添加其它CRUD的请求:

image-20240521144140401

具体代码:

image-20240521143955532

2.5 高级查询

文档搜索的基本步骤是:

  1. 创建SearchRequest对象实例request
  2. 准备request.source(),也就是DSL语句【这个位置可以创建查询,分页,排序,聚合,高亮等操作】
    1. QueryBuilders来构建查询条件
    2. 传入request.source()query()方法
  3. 发送请求,得到结果
  4. 解析结果(参考DSL查询得到的JSON结果,从外到内,逐层解析)

2.5.1 查询数据

我们可以分三步拼凑DSL语句和发起请求获取相应结果:

image-20240522172046658

其中2.组织DSL参数的步骤中source()方法下面对应的查询/高亮/分页/排序/聚合:
image-20240522172832347

在查询方面我们直接可以通过QueryBuilders类调用对应的叶子查询/复杂查询

image-20240522172921305

2.5.2 解析数据

我们可以通过响应结果和Elasticsearch页面返回结果获取具体细节: 【可以扩展很多,但其实就是对照DSL查询结果写

image-20240522173851593

黑马的图:

image-20240522173920457

3.代码实现思路

==基础操作==

  • 1.引入RestHighLevelClient依赖

  • 2.初始化RestHighLevelClient

1
2
3
4
5
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
HttpHost.create("http://192.168.xxx.xxx:9200") //使用RestClient的builder方法创建
)
);
  • 3.针对索引库(数据库表)操作【创建,查询,修改,删除】
1
2
3
4
5
索引库操作的基本步骤:
- 1.初始化RestHighLevelClient类对象client【创建客户端】
- 2.创建XxxIndexRequest对象request【XXX是`Create`、`Get`、`Delete`】
- 3.准备请求参数request.source()方法【只有新增`Create`需要参数,其他情况不需要】
- 4.发送请求client.indices().xxx()方法【xxx是`create`、`exists`、`delete`】
  • 4.针对文档(每一行数据)操作【创建,查询,修改,删除】
1
2
3
4
5
6
文档操作的基本步骤:
- 1.初始化RestHighLevelClient类对象client【创建客户端】
- 2.创建XxxRequest对象request【Xxx是`Index`、`Update`、`Delete`、`Bulk`】
- 3.准备请求参数request.source()方法(`Index`、`Update`、`Bulk`时需要)
- 4.发送请求client.Xxx()方法【Xxx是`index`、`get`、`update`、`delete`、`bulk`】
- 5.解析结果(`Get`查询时需要,数据在_source内部)

==高级操作(复杂的DSL查询)==

5.在具体位置就可以进行复杂的DSL查询【可以进行查询,分页,排序,高亮,聚合等操作】

1
2
3
4
5
6
7
文档搜索的基本步骤是:
1. 创建`SearchRequest`对象实例request
2. 准备`request.source()`,也就是DSL语句【这个位置可以创建查询,分页,排序,聚合,高亮等操作】
1. `QueryBuilders`来构建查询条件
2. 传入`request.source()` 的` query() `方法
3. 发送请求,得到结果
4. 解析结果(参考DSL查询得到的JSON结果,从外到内,逐层解析)

RabbitMQ-黑马商城为例

title: RabbitMQ-黑马商城为例
date: 2024-06-22 20:37:17
tags: RabbitMQ

1.启动RabbitMQ

基于Docker来安装RabbitMQ,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
docker run 
-e RABBITMQ_DEFAULT_USER=itheima #设置默认用户名
-e RABBITMQ_DEFAULT_PASS=123456 #设置默认密码
-v mq-plugins:/plugins #将本地主机上的mq-plugins目录挂载到容器内部的/plugins目录,可以存放插件
--name mq #指定容器名
--hostname mq #指定容器的主机名
-p 15672:15672 #RabbitMQ管理页面登录的端口号 [浏览器输入http://localhost:15672/即可进入]
-p 5672:5672 #RabbitMQ用于AMQP协议通信 [SpringAMQP配置时候用]
--network heima #将容器连接到名字为heima的网络中 [如果没有就使用命令创建hmall网络 docker network create heima]
-d #在后台运行容器
rabbitmq:3.8-management #使用RabbitMQ 3.8版本带有管理界面的镜像来创建容器


精简版 --直接在虚拟机上启动docker然后docker run
docker run \
-e RABBITMQ_DEFAULT_USER=itheima \
-e RABBITMQ_DEFAULT_PASS=123456 \
-v mq-plugins:/plugins \
--name mq \
--hostname mq \
-p 15672:15672 \
-p 5672:5672 \
--network heima\
-d \
rabbitmq:3.8-management

可以看到在安装命令中有两个映射的端口:

  • 15672:RabbitMQ提供的管理控制台的端口
  • 5672:RabbitMQ的消息发送处理接口

通过访问 http://localhost:15672或者http://192.168.92.129:15672即可看到本地/服务器上的管理控制台。首次访问登录,需要配置文件中设定的用户名和密码

image-20240319192803935

创建hmall用户,并且配置一个hmall2虚拟空间

image-20240623002616496

2.操作步骤

  • 1.pom.xml中引入AMQP依赖:消费者和生产者项目

  • 2.yml文件中配置RabbitMQ信息:

    • 2.1消费者项目【基础配置,消费者重试机制,消费者确认机制】
    • 2.2生产者项目【基础配置,生产者重试机制,生产者确认机制】
  • 3.发送消息:生产者利用RabbitTemplate.convertAndSend(exchange交换机, routingKey路由key,message消息【传递的字段】(.setDelay设置延迟时间),confirm消息确认机制信息);

    • 3.1 message默认是JDK序列化有一堆问题 –>引入Jackson序列化【①引入依赖,②生产者和消费者的启动类添加@Bean注入】
  • 4.接收消息:消费者在方法上添加@RabbitListener注解

    具体就是@RabbitListener(bindings=@QueueBinding(

    ​ value=@Queue(name=队列名,durable=true持久化,惰性队列arguments = @Argument(name=”x-queue-mode”,value = “lazy”)),

    ​ exchange=@Exchange(name=交换机名,type = ExchangeTypes.TOPIC,delayed=”true”延迟属性),

    ​ key={“绑定条件1”,”绑定条件2”}

    ​ ))

    方法(原来传递的字段){

    ​ //里面写的就是之前直接调用的那个方法(serviceimpl层代码)

    }

3.更改余额支付需求

改造余额支付功能,将支付成功后基于OpenFeign的交易服务的更新订单状态接口的同步调用—–>基于RabbitMQ的异步通知

image-20240622222844704

说明:目前没有通知服务和积分服务,因此我们只关注交易服务,步骤如下:

  • 定义direct类型交换机,命名为pay.direct
  • 定义消息队列,命名为trade.pay.success.queue
  • trade.pay.success.queuepay.direct绑定,BindingKeypay.success
  • 支付成功时不再调用交易服务更新订单状态的接口,而是发送一条消息到pay.direct,发送消息的RoutingKeypay.success,消息内容是订单id
  • 交易服务监听trade.pay.success.queue队列,接收到消息后更新订单状态为已支付

分析:

  • 生产者:支付服务pay-service

  • 消费者:交易服务trade-service

3.1 pom.xml导入依赖

在生产者和消费者的pom.xml文件中配置:

1
2
3
4
5
<!--消息发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

3.2 yml配置RabbitMQ信息

3.2.1 简单配置

在生产者和消费者的application.yml文件中配置:

1
2
3
4
5
6
7
8
9
spring:
rabbitmq:
host: 192.168.92.129 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall2 # 虚拟主机
username: hmall # 用户名
password: 123456 # 密码

//消费者和生产者会在对应位置添加配置 【例如:生产者消费者的确认机制,重试机制等】

3.2.2 nacos统一配置管理

  • 将rabbitmq配置放在nacos平台:【如果使用统一配置管理,记得导入对应nacos统一配置的config依赖和读取bootstrap.yml文件依赖】

image-20240623002711143

  • bootstrap.yml添加读取nacos配置
image-20240622224211688

3.3 支付服务–发送消息

3.3.1 修改原来业务

image-20240622230216876

3.3.2 配置Jackson消息转换器

  • 导入依赖:

image-20240622232241693

  • 直接配置到hm-common微服务下:
image-20240622231633000
  • 因为要考虑trade-service和pay-service调用时候springboot扫描问题:
image-20240622232005648
  • 然后在生产者和消费者启动类添加bean注入:
image-20240623124959823

3.4 交易服务–接受消息

在trade-service服务中定义一个消息监听类,方法外用注解标注队列,交换机和路由key,方法内写之前调用的方法:

image-20240622233402122

3.5 测试

3.5.1 重启两个服务

可以通过hmall用户的hmall虚拟主机看到队列:

image-20240623003731861

可以通过hmall用户的hmall虚拟主机看到交换机:

image-20240623003857226

3.5.2 前端下单

前端下单然后支付成功之后,查看数据库信息变化了,并且有一条消息进入到mq之中。

image-20240623004203477

4.更改清除购物车需求

==这个需求参考3步骤做的,以下只介绍生产者和消费者部分代码修改==

4.1 订单服务–发送消息

image-20240623135513712

4.2 购物车服务–接收消息

image-20240623135542378

5.改造代码总结

原来的设计:我在方法位置直接调用tradeClient的方法
现在的设计:①生产者只需要传递原来的参数和声明交换机名和key路由;②消费者需要声明交换机名,key路由和队列名,在方法里面直接调用底层方法(serviceimpl层方法),就不用像openFeign方式。

image-20240623125707127

Jmeter

1.安装Jmeter

Jmeter依赖于JDK,所以必须确保当前计算机上已经安装了JDK,并且配置了环境变量。

1.1.下载

可以Apache Jmeter官网下载,地址:http://jmeter.apache.org/download_jmeter.cgi

image-20240620133703234

1.2.解压

因为下载的是zip包,解压缩即可使用,目录结构如下:

image-20240620133725523

其中的bin目录就是执行的脚本,其中包含启动脚本:

image-20240620133802462

1.3.运行

双击即可运行,但是有两点注意:

  • 启动速度比较慢,要耐心等待
  • 启动后黑窗口不能关闭,否则Jmeter也跟着关闭了

image-20240620133825276

2.快速入门

2.1.设置中文语言

默认Jmeter的语言是英文,需要设置:

  • ==设置本地运行中文==

image-20240620133838529

效果:

image-20240620133844456

注意:上面的配置只能保证本次运行是中文,如果要永久中文,需要修改Jmeter的配置文件

  • ==设置永久中文==

打开jmeter文件夹,在bin目录中找到 jmeter.properties,添加下面配置:

1
language=zh_CN

image-20240620133857758

注意:前面不要出现#,#代表注释,另外这里是下划线,不是中划线

2.2.基本用法

在测试计划上点鼠标右键,选择添加 > 线程(用户) > 线程组:

image-20240620134023118

在新增的线程组中,填写线程信息:

image-20240620134032791

给线程组点鼠标右键,添加http取样器:

image-20240620134051379

编写取样器内容:

image-20240620134057894

添加监听报告:

image-20240620134103715

添加监听结果树:

image-20240620134118963

汇总报告结果:

image-20240620134130039

结果树:

image-20240620134154569

微服务-黑马商城为例

前提:我们以单体架构的黑马商城为例

image-20240528142451641

代码结构如下:

image-20240528142611395

==服务拆分–各个模块各司其职==

1.微服务拆分

拆分工程结构有两种:

  • 1.独立project:总黑马商城设置一个空项目(各个模块都在这个目录下) –不怎么美观和使用
  • 2.Maven聚合:总黑马商城设置一个空项目(各个模块成为一个module模块,根据maven管理) –只是代码放一起但是各自可以打包开发编译

我们以第二种Maven聚合方式进行拆分

1.1 新建项目

image-20240528165608489

1.2 导入依赖

直接从hm-service中导入,然后删除一些不需要的依赖

1.3 编写启动类

一定记得和其他包是同一级,不然他妈的扫描不到报bean冲突!!!!!

image-20240528165703436

1.4 编写yml配置文件

直接从hm-service中导入,然后删除和修改一些配置

1.5 挪动代码

挪动步骤:

①domain实体,

②mapper数据库打交道的,

③service和serviceimpl,

④controller

==在这一步拆分多个子项目之后,我们可能会发现cart购物车服务会调用查询item商品服务,之前我们可以在一个模块中直接调用mapper,但是分开之后只能发送请求访问==

2.远程调用-RestTemplate

之前通过调用item的mapper层方法即可,现在需要通过RestTemplate发送http请求给item服务获取数据。【但是有个致命问题是,exchange方法的url是写死的就很麻烦】

使用方法:

image-20240529110754747

具体操作:

image-20240529110418958

==服务治理–更高效管理调用者和被调用者==

1.注册中心(+调用中间商)

为了解决RestTemplate发送http请求时会写死url问题【如果被调用服务有多台负载均衡,就会报错更改也很麻烦】。==其实注册中心就相当于docker中的数据卷一样,我们可以当做中间商然后把调用者(服务调用者)和被调用者(服务注册者)联系起来。==

1.1 注册中心原理

流程如下:

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心 –让注册中心知道我可以被调用
  • 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署) –让调用者知道有哪些可以调用
  • 调用者自己对实例列表负载均衡,挑选一个实例 –让调用者选一个被调用者
  • 调用者向该实例发起远程调用 –远程调用

image-20240529171431457

  • 服务治理中的三个角色分别是什么?

​ 服务提供者:暴露服务接口,供其它服务调用

​ 服务消费者:调用其它服务提供的接口

​ 注册中心:记录并监控微服务各实例状态,推送服务变更信息

  • 消费者如何知道提供者的地址?

​ 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息

  • 消费者如何得知服务状态变更?【Nacos会15s检测一次,30s删除一次

​ 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者

  • 当提供者有多个实例时,消费者该选择哪一个?

​ 消费者通过负载均衡算法,从多个实例中选择一个【==以前SpringMVC默认是Ribbon负载均衡,后来默认是loadbalancer负载均衡==】

1.2 注册中心方式

1.1.1 Eureka(之前使用)

具体使用可以去SpringCloud篇笔记查找

1.1.2 Nacos(目前使用)

1.角色1-注册中心

  • 1.准备配置文件和tar包

    image-20240531172545922
  • 2.linux服务器docker容器启动

    image-20240530095352569

  • 3.可以在windows系统下访问

image-20240530095505769

2.角色2-服务注册

主要用于对服务提供者进行信息注册,注册到nacos中。

  • 1.在pom.xml中导入依赖和在application.yml文件中配置nacos地址

image-20240530095103394

  • 2.我们添加完成之后可以刷新nacos地址,就可以在网页中看到

image-20240530095604593

3.角色3-服务发现

  • 1.在pom.xml中导入依赖和在application.yml文件中配置nacos地址

    image-20240601161204592

    Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。

  • 2.我们添加完成之后可以刷新nacos地址,就可以在网页中看到

image-20240531173131580

  • 3.进行远程调用

==服务调用–更高效发送http请求==

1.OpenFeign(优化发送http请求)

之前使用的RestTemplate发起远程调用的代码:

image-20240423202621703

存在下面的问题:

• 代码可读性差,编程体验不统一

• 参数复杂URL难以维护

==Feign==是一个声明式的http客户端。其作用是帮助我们优雅地实现http请求发送,解决了上述的问题

1.1 使用步骤

  • 1.导入依赖

image-20240601164647179

  • 2.服务发现方启动类添加注解

image-20240601164613950

  • 3.服务发现方编写接口

image-20240601165533941

这里只需要声明接口,无需实现方法[OpenFeign动态代理实现]。接口中的几个关键信息:

  • @FeignClient("item-service") :声明服务名称
  • @GetMapping :声明请求方式
  • @GetMapping("/items") :声明请求路径
  • @RequestParam("ids") Collection<Long> ids :声明请求参数
  • List<ItemDTO> :返回值类型

有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>。我们只需要直接调用这个方法,即可实现远程调用了。

  • 4.服务发现方直接远程调用
    image-20240601165127358

总而言之,OpenFeign替我们完成了服务拉取、负载均衡、发送http请求的所有工作

1.2 连接池

==Feign底层发起http请求,依赖于其它的框架==。其底层客户端实现包括:

  • URLConnection:[默认]不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

以HttpClient为例:

①pom.xml文件引入依赖

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

②yml配置文件

1
2
3
4
5
6
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
#线程池的核心值需要压测和实际情况调整!!!!!!!!!!!1
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

1.3 最佳实践方案

我们在2.1的使用步骤其实只是模拟了一种调用,但可能多个模块之间互相调用这种方式就有很大弊端。

因此可以提出继承方式和抽取方式:

image-20240601205026514

方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。

方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。

1.3.1 两种抽取方式

1.继承方式

就是将所有用得到的dto,po,vo啥的都放到一个微服务里面。

image-20240601204832364

2.抽取方式

每个微服务存放自己需要的dto,po,vo啥的。只有需要的放到对应微服务。

image-20240601204850644

1.3.2 抽取Feign客户端

就是将cart-service关于调用的代码和vo,dto等挪到hm-api公共模块内。

1.3.3 扫描包

一般情况下,如果调用feign和注册feign不在一个微服务内,那就可能出现扫描包扫描不到报错。就需要进行设置扫描包:

image-20240601204312798

1.4 日志管理

OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。

1.4.1 配置文件yml方式

image-20240423213829442

1.4.2 Java代码方式

image-20240423214701673

提出一些问题:

我们将黑马商城拆分为5个微服务:

  • 用户服务
  • 商品服务
  • 购物车服务
  • 交易服务
  • 支付服务

由于每个微服务都有不同的地址或端口,入口不同,在与前端联调的时候发现了一些问题:

  • 请求不同数据时要访问不同的入口,需要维护多个入口地址,麻烦
  • 前端无法调用nacos,无法实时更新服务列表

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,这就存在一些问题:

  • 每个微服务都需要编写登录校验、用户信息获取的功能吗?
  • 当微服务之间调用时,该如何传递用户信息?

通过==网关==技术解决上述问题。笔记分为3章:

  • 第一章:网关路由,解决前端请求入口的问题。
  • 第二章:网关鉴权,解决统一登录校验和用户信息获取的问题。
  • 第三章:统一配置管理,解决微服务的配置文件重复和配置热更新问题。

==服务管理–帮助前后端联调,全局门卫==

1.网关路由

1.1 网关概述(门卫)

顾明思议,网关就是网络的==关口==。数据在网络间传输,当一个网络 –传输–> 另一网络时,就需要经过网关来做数据的路由转发数据安全的校验

image-20240606172320142

现在,微服务网关就起到同样的作用。前端请求不能直接访问微服务,而是要请求网关:

  • 网关可以做安全控制,也就是登录身份校验,校验通过才放行
  • 通过认证后,网关再根据请求转发到想要访问的微服务

image-20240606172632286

在SpringCloud当中,提供了两种网关实现方案:

  • Netflix Zuul:早期实现,目前已经淘汰
  • SpringCloudGateway:基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强

1.2 在项目中的地位

image-20240604172940613

1.3 快速入门

1.3.1 创建项目

创建一个微服务hm-gateway项目:

image-20240606173445134

1.3.2 引入依赖

pom.xml文件引入依赖:

image-20240606173435981

1.3.3 启动类

创建启动类【一定要注意启动类位置和其他包在同一级,不然启动类扫描注解就报错】:

image-20240618110428918

1.3.4 配置路由

==(目前最全,直接挪进去改改)==

接下来,在hm-gateway模块的resources目录新建一个application.yaml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#端口信息
server:
port: 8087
#spring配置
spring:
application:
name: gateway #微服务名称(用于nacos微服务注册)
cloud:
nacos:
server-addr: 192.168.92.129:8848 #微服务nacos地址
#路由过滤
gateway:
#1.路由过滤
routes:
#第一个微服务
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
#第二个微服务
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
#第三个微服务
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
#第四个微服务
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
#第五个微服务
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**

#2.默认过滤器
default-filters: # 默认过滤项
- AddRequestHeader=Truth,Itcast is freaking awesome!

#3.跨域问题
globalcors:
add-to-simple-url-handler-mapping: true #解决options请求被拦截问题
cors-configurations:
'[/**]': #拦截一切请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

==配置文件概述:==

其中,路由规则的定义语法如下:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: item
uri: lb://item-service
predicates:
- Path=/items/**,/search/**

四个属性含义如下:

  • id:路由的唯一标示
  • predicates:路由断言【判断是否符合条件】 –>十一种,但是只用Path这一类
  • filters:路由过滤条件【请求时添加信息】 –>三大类过滤器(执行顺序:默认过滤器,路由过滤器,全局过滤器)
  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

其中yml配置中的routes可以查看源码(底层其实就是我们配置的6个属性,其中我们常用其中4个):
image-20240607145613009

1.3.5 测试

image-20240607111543349

2.网关鉴权(+登录校验)

  • 单体架构,我们只需要完成一次用户登录,身份校验就可以在所有业务中获取到用户信息。
  • 微服务架构,每个微服务都需要做用户登录校验就不太合理了

2.1 鉴权思路分析

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,×不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,×麻烦

既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

【顺序:登录校验 –> 请求转发到微服务】

image-20240618111909518

因此,①JWT登录校验 —->② 网关请求转发(gateway内部代码实现)

2.2 Gateway内部工作基本原理

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。

image-20240607151254092

如图所示:

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

==总结:==

image-20240618134038219

如图所示,最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。

如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到NettyRoutingFilter之前,这就符合我们的需求。

2.3 网关过滤链-三种过滤器

网关过滤器链中的过滤器有两种:

  • GatewayFilter路由过滤器(gateway自带),作用范围比较灵活,可以:【指定的路由Route】 –一般自定义的话比较麻烦【直接yml配置】
  • GlobalFilter全局过滤器,作用范围:【所有路由】,不可配置。 –一般使用这个好弄
  • HttpHeadersFilter处理传递到下游微服务的请求头

其实GatewayFilterGlobalFilter这两种过滤器的方法签名完全一致:

1
2
3
4
5
6
7
/**
* 处理请求并将其传递给下一个过滤器
* @param exchange 当前请求的上下文,其中包含request、response等各种数据
* @param chain 过滤器链,基于它向下传递请求
* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

工作基本原理的第二步WebHandler:FilteringWebHandler请求处理器在处理请求时,会将②GlobalFilter装饰为①GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。

2.4 自定义过滤器

2.4.1 GatewayFilter

Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route

方式一-yml文件配置

例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。

使用只需要在application.yaml中这样配置:【配置到gateway-routes下面就表明属于一个route】

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
#过滤器
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value

如果想作用于全部路由,则可以配置:【配置到gateway下面就表明不属于任何一个route,属于全部路由】

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
#在这里配置只在部分route下有效
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**

#默认过滤器【全部路由】
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value

方式二-自定义类

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory

  • 第一种:参数yml配置+自定义过滤器

【注意:该类的名称一定要以GatewayFilterFactory为后缀!】

image-20240618135158605

然后在yml配置中使用:

1
2
3
4
5
spring:
cloud:
gateway:
default-filters:
- PrintAny #直接写自定义GatewayFilterFactory类名称中前缀类声明过滤器
  • 第二种:自定义过滤器+动态配置参数【比较复杂不建议】

image-20240607153516182

然后在yml配置中使用:

1
2
3
4
5
spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。

还有一种用法,无需按照这个顺序,就是手动指定参数名:

1
2
3
4
5
6
7
8
9
spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3

第二种方法的总体图对比:

image-20240607154320369

2.4.2 GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数[因为默认是全局路由]:

image-20240607153823420

2.5 问题一-怎么进行登录校验

现在我们知道可以通过定义两种过滤器,定义到NettyRoutingFilter之前就行。

我们以自定义GlobalFilter来完成登录校验:

image-20240610213352568

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package com.hmall.gateway.filter;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private final JwtTool jwtTool;

private final AuthProperties authProperties;
//因为不需要拦截的路径有/** 所以我们使用这种特殊matcher类进行匹配
private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){ //yml配置的不需要拦截的路径和request的路径进行判断
// 无需拦截,直接放行
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (headers!=null && !headers.isEmpty()) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
// TODO 5.如果有效,传递用户信息
System.out.println("userId = " + userId);
// 6.放行
return chain.filter(exchange);
}

private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}

@Override
public int getOrder() {
return 0;
}
}

2.6 问题二-网关怎么传递用户信息

截止到2.5,网关已经可以完成登录校验并获取登录用户身份信息。

但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来获取登录用户信息,并存入ThreadLocal,方便后续使用。

据图流程图如下:

image-20240610213950132

2.6.1 网关如何转发用户信息

网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。

具体操作:【在2.5校验器实现的登录校验里面将jwt解析出来的UserId以请求头方式传递】

image-20240618152159108

2.6.2 下游微服务怎么获取用户信息

微服务可以从请求头中获取登录用户信息。利用SpringMVC的拦截器来获取登录用户信息,并存入ThreadLocal,方便后续使用。

据图流程图如下:【==编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行==】

image-20240618161828959

整体代码结构:

image-20240618162921217

具体操作:

因为当前用户ID会在多个微服务中使用,所以我们可以在hm-common微服务中编写:

  • 1.根据SpringMvc拦截器创建规则创建自定义拦截器

image-20240618160956852

  • 2.创建MvcConfig添加自定义的拦截器

image-20240618161119070

  • 3.可以修改之前写死的位置业务逻辑,这样可以在通过Threadlocal获取信息

  • 4.需要注意的是:因为是写在hm-common微服务,这个配置类默认不会生效(和其他微服务的扫描包不一致,无法扫描到,因此无法生效)。基于SpringBoot自动装配原理,我们可以将其添加到resources目录下的META-INF/Spring.factories文件中:

  • 5.如果我们需要保证其他微服务获取这个拦截器,而网关不获取(登录校验了,所以没必要获取啊),就可以添加注解

image-20240618162712521

2.7 问题三-微服务之间怎么传递用户信息

前端发起的请求都会经过网关再到微服务,由于我们之前编写的过滤器和拦截器功能,微服务可以轻松获取登录用户信息。

但有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务。

比如下单业务,流程如下:

image-20240618163838037

下单的过程中,需要调用商品服务扣减库存,调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是,订单服务调用购物车时并没有传递用户信息,购物车服务无法知道当前用户是谁!

由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头

微服务之间调用是基于OpenFeign来实现的,并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢?–借助Feign中提供的一个拦截器接口:RequestInterceptor

image-20240619142520506

我们只需要==实现这个接口,然后实现apply方法,利用RequestTemplate类来添加请求头,将用户信息保存到请求头中==。这样以来,每次OpenFeign发起请求的时候都会调用该方法,传递用户信息。

具体实现:

image-20240619142843772

这样注入bean之后如果要使用,就要在Openfeign远程调用的启动类添加:

image-20240619143047728

==总结:网关解决传递信息的三大问题==

  • 1.怎么做到先校验?后转发(网关路由是配置的,请求转发是Gateway内部代码) —在gateway内部工作基本原理的NettyRoutingFilter过滤器前面定义一个过滤器(①路由过滤器②全局过滤器),过滤器中进行校验JWT信息,然后通过mutate方法转发用户信息。
  • 2.怎么做到网关给用户传递用户信息 —网关到微服务通过API添加用户信息到http请求头,微服务通过SpringMVC拦截器获取用户信息,将用户信息存储到ThreadLocal中
  • 3.怎么做到用户之间调用传递用户信息 —就是利用发送http请求(Openfeign)时通过提供的拦截器添加

image-20240619143917520

[JWT里面传递UserId信息,网关添加过滤器进行校验token同时将UserId添加到请求头,通过mutate方法传递给微服务,微服务通过SpringMVC拦截器获取UserId信息,然后存储到ThreadLocal,业务就可以使用。如果微服务之间调用就通过OpenFeign发送http请求的时候添加拦截器保存UserId]

==配置管理–高效维护配置和动态变更属性==

1.微服务重复配置过多,维护成本高 —-> 共享配置

2.业务配置经常变动,每次修改都要重启服务 —-> 热更新

3.网关路由配置写死,如果变更就要重启网关 —-> 热更新

image-20240619145505779

这些问题都可以通过统一的配置管理器服务[Nacos第二大特性]解决 —–Nacos不仅仅具备注册中心功能,也具备配置管理的功能:

微服务共享的配置可以统一交给Nacos保存和管理,在Nacos控制台修改配置后,Nacos会将配置变更推送给相关的微服务,并且无需重启即可生效,实现配置热更新。

网关的路由同样是配置,因此同样可以基于这个功能实现动态路由功能,无需重启网关即可修改路由配置。

1.配置共享

我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:

  • ①在Nacos中添加共享配置
  • ②微服务拉取配置

1.1 添加共享配置

在nacos控制台分别添加微服务共同配置:

image-20240619153300369

最终形成多个yaml文档:

image-20240619153352401

1.2 拉取共享配置

将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。

不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml

也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?

SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,如果我们将nacos地址配置到bootstrap.yaml中,那么在项目引导阶段就可以读取nacos中的配置了。

1.2.1 文件读取顺序

image-20240619154015718

1.2.2 拉取步骤

  • 1.导入依赖:
1
2
3
4
5
6
7
8
9
10
<!--nacos配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

image-20240619154146436

  • 2.编写bootstrap文件:

image-20240619154311986

1.3 多配置文件读取顺序

可能不同环境下有不同的yaml文件[像单体架构的时候properties,yml,yaml等情况],因此当出现相同属性时就有优先级:==名字越长越牛逼==

image-20240423173524235

1.4 配置共享整理总结

其实就是把原来的application.yml文件拆分成三个部分:①application公共配置;②Nacos地址和读取①文件配置;③application个性化配置

①nacos空间多个共享文件:原来application.yml中多个微服务可共享的信息

②新建bootstrap.yml文件:原来application.yml里面关于nacos的配置+添加config信息(读取nacos配置的多个共同部分yml文件);

③application.yml:保留一部分自己特有的属性和①nacos里面${}需要的属性

2.配置热更新(无需重启)

这就要用到Nacos的配置热更新能力了,分为两步:

  • 在Nacos中添加配置[配置属性]
  • 在微服务读取配置[bootstrap.yml文件拉取配置,具体业务位置使用]

image-20240619160718950

2.1 Nacos配置文件

首先,我们在nacos中添加一个配置文件,将购物车的上限数量添加到配置中:

image-20240619160940082

注意文件的dataId格式:

1
[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 服务名:我们是购物车服务,所以是cart-service
  • spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
  • 后缀名:例如yaml

2.2 配置热更新

我们在微服务中读取配置,实现配置热更新。【一般我们使用第一种方式,第二种要用两个注解】

现在我们需要读取Nacos配置文件中的信息hm.cart.maxAmount属性:

image-20240619161955080

2.2.1 方式一

cart-service中新建一个属性读取类:

image-20240619161154107

接着,在业务中使用该属性加载类:

image-20240619161245631

2.2.2 方式二

直接搭配@RefreshScope注解和@Value注解获取

image-20240619161914727

3.动态路由

用到了在学

Hexo博客报错github传输大文件GH001异常

1.报错原因

我在Docker文件夹下上传了一个iso文件,这个文件大于了github的100M大小报错。

在我hexo g的时候没问题,但是hexo d的时候会出错。

image-20240528105520819

但是本地删除了iso文件还是不行,最后查询意思是之前的记录仍然存在,只能从本地仓库删除并且把以前的提交记录全部修改

2.修改办法

2.1 在此目录下打开git bash

image-20240528105706115

2.2 输入指令 git log通过此处找到报错前最新的版本

image-20240528105915849

2.2 还有一种办法就是通过github查看版本

image-20240528110139383

2.3 至此直接git reset id 就可以恢复到对应版本

image-20240528110223842

3.参考办法

记一次异常艰难的博客部署(二)—— hexo d 指令向GitHub传输大文件导致的 GH001 报错解决 | 邓小闲的小楼 (rimbaud-lee.github.io)

微服务-分布式事务

==1.分布式事务产生原因==

首先我们看看项目中的下单业务整体流程:

image-20240620154853978

由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务:

  • 交易服务:下单事务
  • 购物车服务:清理购物车事务
  • 库存服务:扣减库存事务

整个业务中,各个本地事务是有关联的。因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败。

我们知道每一个分支事务就是传统的单体事务,都可以满足ACID特性,但全局事务跨越多个服务、多个数据库,不能满足!!!!!!!!!!!!

  • 产生原因

事务并未遵循ACID的原则,归其原因就是参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性了。

这就是分布式事务问题,出现以下情况之一就可能产生分布式事务问题:

  • 业务跨多个服务实现
  • 业务跨多个数据源实现

==2.CAP定理==

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统要有三个指标:

  • Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致

  • Availability(可用性):用户访问分布式系统时,读/写操作总能成功

  • Partition tolerance(分区容错性):即使系统出现网络分区,整个系统也要持续对外提供服务

他认为任何分布式系统架构方案都不能同时满足这三个目标,这个结论就是CAP定理。

2.1 一致性

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致

image-20241008165045608

2.2 可用性

Availability (可用性):用户访问分布式系统时,读或写操作总能成功。

只能读不能写,或者只能写不能读,或者两者都不能执行,就说明系统弱可用或不可用。

2.3 分区容错性

Partition tolerance(分区容错性):即使系统出现网络分区partition,整个系统也要持续对外提供服务tolerance。

其中partition(分区):当分布式系统节点之间出现网络故障导致节点之间无法通信的情况。

image-20241008170759823

如上图,node01和node02之间网关畅通,但是与node03之间网络断开。于是node03成为一个独立的网络分区;node01和node02在一个网络分区。

其中tolerance(分区容错):当系统出现网络分区,整个系统也要持续对外提供服务。

2.4 三者矛盾(P一定有)

在分布式系统中,网络不能100%保证畅通(partition网络分区的情况一定会存在)。而我们的系统必须要持续运行,对外提供服务。所以分区容错性(P)是硬性指标,所有的分布式系统都要满足。

而设计分布式系统的时候要取舍的就是一致性(C)和可用性(A)。

【P一定有,C和A不一定有】

image-20241008172328325

如果允许可用性(A):这样用户可以任意读写,但是由于node03不能同步数据,那就会出现数据不一致情况【只满足AP】

如果允许一致性(C):如果用户不允许随意读写(不允许写,允许读)一直到网络恢复,分区消失,只能满足数据一致性【只满足CP】

2.5 解决三者矛盾(BASE理论)

因为P一定有,C和A不一定有:所以要考虑到底是牺牲一致性还是可用性?—>BASE理论

  • Basically Available 基本可用:分布式系统在出现故障时,允许损失部分可用性,【保证核心可用性】
  • Soft State软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
  • Eventually Consistent最终一致性:虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

总而言之,BASE理论其实就是一种取舍方案,不再追求完美,而是追求完成目标。

  • AP思想:【AT模式】各个子事务分别执行和提交,无需锁定数据。允许出现结果不一致,然后采用弥补措施恢复,实现最终一致。
  • CP思想:【XA模式】各个子事务执行后不要提交,而是等待彼此结果,然后同时提交或回滚。在这个过程中锁定资源,不允许其它人访问,数据处于不可用状态,但能保证一致性。

—————————————

==解决方案(中间人-事务协调者)==

解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的Seata了。

1.Seata

官方地址:Seata

分布式事务产生的一个重要原因:参与事务的多个分支事务互相无感知, 不知道彼此的执行状态。

解决方案:就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。

image-20240620161226852

1.1 Seata架构

Seata也不例外,在Seata的事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

image-20240620162213687

  • 现来方式:直接执行全局事务,然后中途调用各个分支事务,执行结束就完成【各个分支不知道彼此是否正确】

  • 现在方式:直接执行全局事务(事务管理器TM管理开始和结束),然后中途调用各个分支事务(各个RM告知TC这个全局事务有我,我开始了,我结束了),执行结束就完成【中途有什么问题TC都知道,随时可能回滚】

1.2 代码实现思路(两个方面)

TMRM【Seata的客户端部分】,引入到参与事务的微服务依赖中即可。(将来TMRM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。)

TC【事务协调中心】,是一个独立的微服务,需要单独部署。

—————————————

==Seata具体操作(分两个部分)==

1.TC部署

1.1 准备数据库表

image-20240620170911535

其中seata-tc.sql内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
CREATE DATABASE IF NOT EXISTS `seata`;
USE `seata`;


CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;


CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;


CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

1.2 准备配置文件

  • 准备seata目录(包含application.yml配置文件),到时候docker容器可以挂载

image-20240620172739258

其中application.yml信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
server:
port: 7099 #控制台端口

spring:
application:
name: seata-server

logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
# extend:
# logstash-appender:
# destination: 127.0.0.1:4560
# kafka-appender:
# bootstrap-servers: 127.0.0.1:9092
# topic: logback_to_logstash

#控制台信息 ip:7099进入之后账号和密码
console:
user:
username: admin
password: admin

seata:
#配置中心
config:
# support: nacos, consul, apollo, zk, etcd3 多种配置中心
type: file
# nacos:
# server-addr: nacos:8848
# group : "DEFAULT_GROUP"
# namespace: ""
# dataId: "seataServer.properties"
# username: "nacos"
# password: "nacos"
#注册中心
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa 多种注册中心
type: nacos
nacos:
application: seata-server
server-addr: nacos:8848 #①ip地址 ②nacos就是容器名,意味着nacos和seata要在同一网络中(这样可通过容器名访问)
group : "DEFAULT_GROUP"
namespace: ""
username: "nacos"
password: "nacos"
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
max-commit-retry-timeout: -1
max-rollback-retry-timeout: -1
rollback-retry-timeout-unlock-enable: false
enable-check-auth: true
enable-parallel-request-handle: true
retry-dead-threshold: 130000
xaer-nota-retry-timeout: 60000
enableParallelRequestHandle: true
recovery:
committing-retry-period: 1000
async-committing-retry-period: 1000
rollbacking-retry-period: 1000
timeout-retry-period: 1000
undo:
log-save-days: 7
log-delete-period: 86400000
session:
branch-async-queue-size: 5000 #branch async remove queue size
enable-branch-async-remove: false #enable to asynchronous remove branchSession
store:
# support: file 、 db 、 redis
mode: db
session:
mode: db
lock:
mode: db
#数据库配置
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql:3306/seata?rewriteBatchedStatements=true&serverTimezone=UTC
user: root
password: 123
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# redis:
# mode: single
# database: 0
# min-conn: 10
# max-conn: 100
# password:
# max-total: 100
# query-limit: 1000
# single:
# host: 192.168.150.101
# port: 6379
metrics:
enabled: false
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
transport:
rpc-tc-request-timeout: 15000
enable-tc-server-batch-send-response: false
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
boss-thread-size: 1

1.3 Docker部署

  • 1.导入镜像文件和配置文件
image-20240620171216896
  • 2.加载镜像文件
image-20240620171453072
  • 3.运行docker容器
1
2
3
4
5
6
7
8
9
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.92.129 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network heima \
-d \
seataio/seata-server:1.5.2

image-20240620172200138

  • 4.查看容器运行情况:docker logs -f seata

image-20240620172332870

  • 5.在浏览器输入IP:7099即可打开控制台

image-20240620172510841

2.微服务集成Seata

2.1 引入依赖

【所有分支事务都需要引入】为了方便各个微服务集成seata,我们需要把seata配置共享到nacos,因此trade-service模块不仅仅要引入seata依赖,还要引入nacos依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--统一配置管理,读取nacos共享配置文件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--读取bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

<!--如果只需要seata集成微服务,那就只导入这个依赖!!!!!!!!!!-->
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

image-20240621094124650

2.2 添加配置(统一配置到nacos)

【一般直接配置到apolication.yml文件】因为多个分支事务都需要,那我就可以将seata的配置放在nacos统一配置,剩下的就是改造application.yml和bootstrap.yml文件信息。

2.2.1 配置公共配置

server-addr一定要配置自己的ip:【不然容易注册不到nacos上去!!!】

image-20240621093202209

让微服务能找到TC的位置:

image-20240621093439565

这样配置之后,各个分支事务都去配置这个TC信息:

2.2.2 分支事务新建bootstrap.yml文件

这样配置之后,各个分支事务都去配置这个TC信息:

image-20240621094830123

2.2.3 分支事务调整application.yml文件

image-20240621094943628

2.3 添加数据库保存快照

seata的客户端(TM和RM)在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。因此我们要先准备一个这样的表。

对三个分支事务hm-trade、hm-cart、hm-item三个数据库加入一个undo_log日志表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

添加完成之后:

image-20240621100417899

2.4 修改具体业务

我们重新启动项目之后,可以查看seata日志:

image-20240621112336246

然后针对出问题的方法进行修改【修改为GlobalTransactional注解】:

image-20240621102212713

@GlobalTransactional注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。如果中途有分支事务出现问题,我们就可以告知TC进行回滚操作,保证全局事务要么成功/要么失败。

3.实现步骤

  • 1.准备TC所需要的数据库,准备配置文件和镜像文件 —【可以直接去服务器利用docker配置TC】
  • 2.微服务继承Seata
    • 2.1 引入seata依赖
    • 2.2 在yml配置seata信息 【因为涉及多个分支事务,所以一般配置到nacos】
    • 2.3 原有出现问题的方法替换@Tradtional注解为@GlobalTransactional注解解决分布式事务

==Seate四种底层原理-[四种]==

Seata支持四种不同的分布式事务解决方案:

  • XA
  • TCC(Try-Confirm-Cancel)
  • AT(Automatic Transaction)
  • SAGA

代码使用思路

【使用过程中,只是yml配置多一个属性】

其实就是Seata实现步骤:

①导入依赖

②yml配置(基础配置+模式配置属性) —-多了一个配置data-source-proxy-mode

③全局事务位置加注解@GlobalTransactional,分支事务加@Transactional

1.XA模式[统一控制,统一提交]

==①各事务执行完都锁住②统一判断是全部提交/全部撤回==

XA 规范 是X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。

1.1 基本概念

主要分为==两个阶段==提交:

一阶段的工作:【TM通知各个RM执行本地事务,RM向TC注册和报告完成情况,但是不提交保持数据库锁】

①RM注册分支事务到TC【告知我是哪个TM的,我完成什么任务】

②RM执行分支业务sql但不提交【完成任务了】tryPayOrderByBalance

③RM报告执行状态到TC【告诉你我完成了】

二阶段的工作:【TC基于一阶段RM提交事务状态来判断下一步操作是回滚还是提交】

①TC检测各分支事务执行状态【看看各个RM完成如何】

​ a.如果都成功,通知所有RM提交事务【ok,提交吧】

​ b.如果有失败,通知所有RM回滚事务【no,回滚吧】

②RM接收TC指令,提交或回滚事务【TC告诉我其他人好了/有问题,就提交/回滚】

image-20240621140448858

1.1 我们启动服务通过注解@GlobalTranscational开启全局事务

1.2 我们操作的时候调用多个分支事务

1.3 分支事务先向TC进行注册,告知TC我的哪个TM负责的,我要完成什么【告知之后可以进行业务逻辑】

1.4 开始执行业务,进行sql语句的完成【但是不提交!!!!】

1.5 执行业务sql完成之后报告TC我已经完成我自己的任务了,报告事务状态【TC就知道分支业务完成状态(有的完成了,有的失败了)】

因为第一阶段结束我们可以进行结束全局事务,后续看看是回滚还是提交

2.1 结束全局事务

2.2 TM告知TC检查一下第一阶段各分支事务执行状态,看是不是所有都完成

2.3 因为要全局事务要么提交/要么回滚,如果都成功,通知所有RM提交事务,如果有失败,通知所有RM回滚事务

流程图如下:

image-20240621143957304

1.2 具体实现操作

1.2.1 yml配置

image-20240621131831960

1.2.2 修改具体业务

对应全局事务位置添加@GlobalTranscational:

image-20240621131953128

针对各个分支事务添加@transactional:

image-20240621132234331

1.2.3 测试

我们加入手机到购物车,然后修改手机库存stock=0下单之后trade-service会提示:

image-20240621134914617

1.3 XA使用总结

image-20240621140206477

1.4 XA优缺点

  • XA模式的优点是什么?

    • 事务的强一致性,满足ACID原则【第一阶段只完成不提交,只有第二阶段才告知一起回滚,还是一起提交】

    • 常用数据库都支持,实现简单,并且没有代码侵入【比较好理解,而且比较规整】

  • XA模式的缺点是什么?

    • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差【第一阶段只能等第二阶段指令,阻塞时间长】

    • 依赖关系型数据库实现事务【关系型数据库】

1.5 XA模式和AT模式对比

XA模式【模式强一致,一步一步来】 AT模式【模式最终一致,可以第二阶段回退】
第一阶段 只完成不提交(锁定资源,阻塞) 直接提交(不锁定资源)
第二阶段 数据库机制完成回滚 数据快照完成回滚(第一阶段执行业务之前生成快照)
性能 低(只有第二阶段才决定事务提交/回退) 高(第一阶段就提交,第二阶段可以恢复)

2.AT模式[各自提交,有问题快照恢复]

分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷(一直阻塞等到第二阶段TC告知RM才可以进行操作)

==①你可以自己提交,然后提交的时候搞个快照(备份),不用锁定资源②如果都成功就删除快照(备份),不成功就用快照(备份)恢复==

2.1 基本概念

主要分为==两个阶段==提交:

一阶段的工作:

  1. TM发起并注册全局事务到TC
  2. TM调用分支事务
  3. 分支事务准备执行业务SQL
  4. RM拦截业务SQL,根据where条件查询原始数据,形成快照。【在执行业务sql之前生成快照
1
2
3
{
"id": 1, "money": 100
}
  1. RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90【我已经完成了自己的任务,并且提交了】
  2. RM报告本地事务状态给TC

二阶段的工作:

  1. TM通知TC事务结束【ok了,你判断一下吧】
  2. TC检查分支事务状态【如果都成功删除快照,如果有失败就用快照恢复数据库回滚】
    1. 如果都成功,则立即删除快照
    2. 如果有分支事务失败,需要回滚。读取快照数据({“id”: 1, “money”: 100}),将快照恢复到数据库。此时数据库再次恢复为100

image-20240621151403207

流程图如下:

image-20240621153523017

2.2 具体实现操作

2.2.1 yml配置

类似于XA,就是将属性改为AT

image-20240621151209260

2.2.2 修改具体业务

类似与XA,就是全局事务位置加注解@GlobalTransanctional,分支事务位置加注解@Transanctional

2.2.3 添加快照undo表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

2.2.4 测试

类似于XA测试,只不过多了快照数据进入到undo表

2.3 AT使用总结

1.yml添加配置

2.业务添加注解@GlobalTransanctional即可

3.添加快照表【比XA模式就多一个这个】

2.4 AT优缺点

  • XA模式的优点是什么?

    • 第一阶段就直接提交了,性能较好【后续如果需要就使用快照恢复】
  • XA模式的缺点是什么?

    • 第一阶段就提交,在第二阶段完成的极小时间段内可能出现数据不一致【用空间换时间】—99%没问题,极端情况下【特别是多线程并发访问AT模式的分布式事务时,有可能出现脏写问题(丢失一次更新)】

2.5 AT模式—脏写问题

这种模式在大多数情况下(99%)并不会有什么问题,不过在极端情况下,特别是多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:

image-20241008215009702

  • 解决思路:引入全局锁【在释放DB锁之前,先拿到全局锁】,避免同一时刻有另外一个事务来操作当前数据。

全局锁(TC管理):记录这行数据,其他事务可以CRUD】

DB锁(数据库管理):锁住这行数据,其他事务不可以CRUD】

image-20241008215937381

就是在原来基础上,增加获取全局锁部分[记录当前操作这行数据的事务];其他的事务获取全局锁(失败重试30次,间隔10ms一次)

2.6 XA模式和AT模式对比

XA模式【模式强一致,第二阶段统一处理】 AT模式【模式最终一致,可以第二阶段回退】
第一阶段 只完成不提交(锁定资源,阻塞) 直接提交(不锁定资源)
第二阶段 数据库机制完成回滚 数据快照完成回滚(第一阶段执行业务之前生成快照)
性能 低(只有第二阶段才决定事务提交/回退) 高(第一阶段就提交,第二阶段可以恢复)

3.TCC模式[各自提交,有问题人工恢复]

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • try:检测和预留资源;
  • confirm:完成资源操作业务;要求 try 成功 confirm 一定要能成功。
  • cancel:释放预留资源,【try的反向操作】

3.1 流程分析[举例]

例子:一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

  • 阶段一(try):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣减30

image-20241009090835461

  • 阶段二(Confrim):假设要提交,之前可用金额已经扣减,并且转移到冻结金额。因此可用金额不变,直接冻结金额-30即可:

image-20241009090948444

  • 阶段三(Cancel):如果要回滚,则释放之前冻结的金额(冻结金额-30,可用金额+30)

image-20241009091156001

3.2 事务悬挂和空回滚

假如一个分布式事务中包含两个分支事务,try阶段,一个分支成功执行,另一个分支事务阻塞

img

如果阻塞时间太长,可能导致全局事务超时而触发三阶段的cancel操作。两个分支事务都会执行cancel操作:

image-20241009091343545

其中一个分支是未执行try操作的,直接执行了cancel操作,反而会导致数据错误。因此,这种情况下,尽管cancel方法要执行,但其中不能做任何回滚操作,这就是空回滚【一个分支事务没执行try操作,但要被牵连执行cancel操作,需要执行cancel操作但是不能做任何回滚操作(不应该回滚)】

image-20241009091922827

对于整个空回滚的分支事务,将来阻塞结束try方法依然会执行。但是整个全局事务其实已经结束了,因此永远不会再有confirm或cancel,也就是说这个事务执行了一半,处于悬挂状态【阻塞结束,try执行,但是整体全局事务已经结束,无后续】

3.3 TCC使用总结

CC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强【不需要快照,人工恢复】
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库【人工补偿,不依赖数据库事务】

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦【人工恢复,需要代码入侵】
  • 软状态,事务是最终一致【不是强一致性,BASE理论】
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理、事务悬挂和空回滚处理
,