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的失败情况,做好幂等处理、事务悬挂和空回滚处理

微服务-服务保护

==微服务拆分容易出现的问题==

1.雪崩问题

1.1 产生背景

在微服务远程调用的过程中,还存在几个问题需要解决。

业务健壮性问题:

例如在之前的查询购物车列表业务中,购物车服务需要查询最新的商品信息,与购物车数据做对比,提醒用户。大家设想一下,如果商品服务查询时发生故障,查询购物车列表在调用商品服务时,是不是也会异常?从而导致购物车查询失败。

但从业务角度来说,为了提升用户体验,即便是商品查询失败,购物车列表也应该正确展示出来,哪怕是不包含最新的商品信息。

级联失败问题:

还是查询购物车的业务,假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。

此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。

image-20240620110735548

依次类推,整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。

image-20240620110826809

==雪崩【级联失败】==:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用。

1.2 产生原因

  • 微服务相互调用,服务提供者出现故障或阻塞。

  • 服务调用者没有做好异常处理,导致自身故障。

  • 调用链中的所有服务级联失败,导致整个集群故障

1.3 解决方案

  • 尽量避免服务出现故障或阻塞。–请求限流和线程隔离

    • 保证代码的健壮性;

    • 保证网络畅通;

    • 能应对较高的并发请求;

  • 服务调用者做好远程调用异常的后备方案,避免故障扩散 –服务熔断

这些方案或多或少都会导致服务的体验上略有下降,比如请求限流,降低了并发上限;线程隔离,降低了可用资源数量;服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。因此这些方案都属于服务降级的方案。但通过这些方案,服务的健壮性得到了提升。

1.请求限流(降低访问流量)

==限制访问微服务的请求并发量,避免服务因流量激增出现故障==

服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。

image-20240620111528990

2.线程隔离(降低独占资源数量)

==限制分给其他服务的线程数,保证不会因为一个服务挂了导致其他服务消耗完自己资源也挂了==

为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。

image-20240620112057241

举例子说明:

image-20240620112134291

如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。

3.快速失败(fallback后备方案)

快速失败:给业务编写一个调用失败时的处理的逻辑,称为fallback。当调用出现故障(比如无线程可用)时,按照失败处理逻辑执行业务并返回,而不是直接抛出异常。

image-20240620112448590

4.服务熔断(提前预测,不对劲就fallback)

==【相当于一个提前预判】设定一个断路器(开关),统计请求的异常比例和慢调用比例,超过阈值我就拒绝不让你用。熔断了去走服务的后备fallback逻辑(备份方案)==

image-20240620133335898

思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截访问该服务的一切请求。而当设定熔断时间结束后它会尝试放行一次请求测试,如果成功就是服务恢复时,断路器会放行访问该服务的请求; 如果放行不通过继续走熔断状态,所有请求走fallback快速失败。

image-20240620144802785

2. 常见服务保护技术

目前我们常见的就是以下两种,我们都推荐使用Sentinel:【实习公司用Hystrix】

image-20240619172500972

==Sentinel==

==服务保护方案–Sentinel基础使用==

1.介绍

Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中。官方网站:

https://security.feishu.cn/link/safety?target=https%3A%2F%2Fsentinelguard.io%2Fzh-cn%2F&scene=ccm&logParams=%7B%22location%22%3A%22ccm_docs%22%7D&lang=zh-CN

Sentinel 的使用可以分为两个部分:

  • 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

2.安装

为了方便监控微服务,我们先把Sentinel的控制台搭建出来

2.1 下载jar包

下载地址:Releases · alibaba/Sentinel · GitHub

2.2 启动测试

  • 1.存放jar:将jar包放在任意非中文、不包含特殊字符的目录下,重命名为sentinel-dashboard.jar

image-20240620100439761

  • 2.启动:在当前目录下cmd打开命令行输入指令启动:
1
2
3
4
5
输入以下命令:
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

#其余配置项可以参考官方文档:
https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9

运行结果:

image-20240620100605675

image-20240620100945847

需要输入账号和密码,默认都是:sentinel

登录后,即可看到控制台,默认会监控sentinel-dashboard服务本身

image-20240620101024738

3.微服务整合

我们以微服务-黑马商城中的cart-service购物车模块为例:

3.1 引入依赖

我们在cart-service服务pom.xml文件引入依赖

1
2
3
4
5
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

因为整合到springalibaba,所以依赖名也是spring-cloud-starter前缀

image-20240620101220519

3.2 yml配置控制台

我们在cart-service服务yml文件引入依赖

1
2
3
4
5
6
7
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # sentinel控制台地址
# 因为restful风格,如果不设置的话同一个controller下的接口都是一个资源
http-method-specify: true #是否设置请求方式作为资源名称

image-20240620101330326

3.3 测试

重启cart-service,然后访问查询购物车接口,sentinel的客户端就会将服务访问的信息提交到sentinel-dashboard控制台。并展示出统计信息:

image-20240620101510077

点击cart-service的簇点链路菜单,会看到下面的页面:

image-20240620101637577

所谓簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源。【默认情况下,Sentinel会监控SpringMVC的每一个Endpoint(接口)】

因此,我们看到/carts这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。

3.4 簇点链路

【默认情况下】Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源(我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是/carts路径)

image-20240620101946062

解决方案:我们可以选择打开Sentinel的请求方式前缀,把请求方式 + 请求路径作为簇点资源名

image-20240620102041605

然后,重启服务,通过页面访问购物车的相关接口,可以看到sentinel控制台的簇点链路发生了变化:

image-20240620102128531

==服务保护方案–Sentinel四大解决方案==

我们以微服务-黑马商城中的cart-service购物车模块为例:

1.请求限流

前提:我们已经将cart-service购物车模块和sentinel建立连接,我们就可以通过控制台进行操作

1.1 控制台设置限流QPS

把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6.

image-20240620102556908

1.2 Jmeter测试

【可参考笔记-jmeter快速入门】

我们利用Jemeter做限流测试,我们每秒发出10个请求:

image-20240620102852440

最终在sentinel监控结果如下:

image-20240620103024899

可以看出GET:/carts这个接口的通过QPS稳定在6附近,而拒绝的QPS在4附近,符合我们的预

2.线程隔离

2.1 控制台设置并发线程数

image-20240620104405158

2.2 Jmeter测试

【可参考笔记-jmeter快速入门】

我们利用Jemeter测试,每秒发送100个请求:

image-20240620104448371

最终在sentinel监控结果如下:

进入查询购物车的请求每秒大概在100,而在查询商品时却只剩下每秒10左右,符合我们的预期。

image-20240620104557342

此时如果我们通过页面访问购物车的其它接口,例如添加购物车、修改购物车商品数量,发现不受影响:

image-20240620104632824

利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。由于查询商品的功能耗时较高(我们模拟了500毫秒延时),再加上线程隔离限定了线程数为5,导致接口吞吐能力有限,最终QPS只有10左右。这就导致了几个问题:

第一,超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个降级处理逻辑【fallback快速失败】。

第二,由于查询商品的延迟较高(模拟的500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断

3.快速失败-Fallback(后备方案)

触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。

3.1 两种配置方式

image-20240620134907425

3.2 举例-以方式二为例:

3.2.1 yml导入依赖

修改cart-service模块的application.yml文件,开启Feign的sentinel功能:

1
2
3
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
image-20240620104846227

3.2.2 编写降级处理类

在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory

image-20240620144115712

3.2.3 注入bean

hm-api模块中的com.hmall.api.config.DefaultFeignConfig类中将ItemClientFallback注册为一个Bean

image-20240620144206407

3.2.4 给对应Openfeign添加属性

hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory

image-20240620144229206

3.2.5 Jmeter测试

image-20240620144351522

但是未被限流的请求延时依然很高:

image-20240620144417302

导致最终的平均响应时间较长。

4.服务熔断

查询商品的RT较高(模拟的500ms),从而导致查询购物车的RT也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。

对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。当商品服务接口恢复正常后,再允许调用。这其实就是断路器的工作模式了。

Sentinel中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例。当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。

断路器的工作状态切换有一个状态机来控制:

image-20240620144802785

状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态【正常状态】
    • 请求失败:则切换到open状态【熔断状态】

4.1 控制台设置熔断规则

image-20240620142613725

这种是按照慢调用比例来做熔断,上述配置的含义是:

  • RT超过200毫秒的请求调用就是慢调用
  • 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断
  • 熔断持续时长20s

4.2 Jmeter测试

image-20240620145038189

此时整个购物车查询服务的平均RT影响不大:

image-20240620145054930

==Sentinel服务保护总结==

1.具体使用操作

  • 1.启动sentinel控制台(jar包)

到这已经可以打开sentinel控制台

  • 2.具体使用的微服务pom.xml文件导入sentinel依赖
  • 3.具体使用的微服务yml文件配置sentinel控制台信息

到这已经可以在sentinel控制台查看

具体的四大解决方案【1请求限流,2线程隔离,4服务熔断在平台设置就行】

  • 1和2.控制台通过具体微服务-簇点链路的流控按钮设置请求限流(设置QPS)和线程隔离(设置并发线程数)

  • 3.fallback快速失败需要编写代码(Openfeign所在微服务进行编写)

  • ①在Openfeign所在微服务编写降级处理类实现FallbackFactory重写create方法里面将ItemClient被其他微服务调用的方法都重写处理(后备方案,可以是信息提示等)

  • ②将降级处理类加入bean

  • ③在微服务ItemClient的FeignClient注解添加属性fallbackFactory=①降级处理类

  • 4.控制台通过具体微服务-簇点链路的熔断按钮设置熔断策略

2.四大解决方案总结

1.请求限流(限制访问量)和线程隔离(限制线程数,不让此服务占用所有资源)相当于对单个微服务调用做保护

2.快速失败(如果请求限流/线程隔离资源没了,那就可以走这个fallback后备方案,不至于直接抛出异常)和服务熔断(提前对这个服务判断如果经常出异常,那就熔断)是在微服务调用其他微服务情况时候做保护

如果比喻成买火车票乘车:

1.请求限流就是限制购买人数

2.线程隔离就是车次分到A候车厅(六个通道)和B候车厅(四个通道),如果B满了或者速度太慢可以去A候车厅的通道,不至于直接瘫痪。

3.服务熔断就是我提前预测哪里经常会出问题,我可以一段时间内不让大家去那候车可以去备用候车厅;如果一段时间后,我可以测试走一个人如果通那就打开去候车,如果不行那就继续用备用候车厅。

4.快速失败就是如果这个通道走不了,那你不能让乘客不上车啊,所有可以大喇叭提示等待或者提供备用候车厅上车

==底层原理==

无论是①Hystix还是②Sentinel都支持线程隔离,实现方式不同。

①Hystix:线程池隔离[默认],信号量隔离

②Sentinel:信号量隔离

1.线程隔离

线程隔离有两种方式实现:

  • 线程池隔离:给每个微服务调用业务分配一个线程池【利用线程池本身实现隔离效果】
  • 信号量隔离:给每个业务设定线程数量,达到信号量上限时就禁止新的请求【不创建线程池,而是使用计数器模式】
image-20241009100448925

两者的优缺点:

信号量隔离 线程池隔离
优点 轻量级,无额外开销 支持主动超时,支持异步调用
缺点 不支持主动超时,不支持异步调用 线程的额外开销比较大
场景 高频调用,高扇出 低扇出

2.四种算法

在熔断功能中,需要统计异常请求或慢请求比例,也就是计数。在限流的时候,要统计每秒钟的QPS,同样是计数。可见计数算法在熔断限流中的应用非常多。sentinel中采用的计数器算法就是滑动窗口计数算法。

2.1 固定窗口计数

image-20241009102754788

  • 每个窗口维护1个计数器,每有1次请求就将计数器+1。限流就是设置计数器阈值,本例为3,图中红线标记
  • 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃。

image-20241009102955454

说明:

  • 第1、2秒,请求数量都小于3,没问题
  • 第3秒,请求数量为5,超过阈值,超出的请求被拒绝

特殊情况:【无法结合前后的时间窗口的数据做综合统计】—只能统计当前某1个时间窗的请求数量是否到达阈值

image-20241009190539989

说明:

  • 假如在第5、6秒,请求数量都为3,没有超过阈值,全部放行
  • 但是,如果第5秒的三次请求都是在4.5-5s之间进来;第6秒的请求是在5-5.5s之间,那么4.5-5s之间就有6次请求!也就是说每秒的QPS达到了6,远超阈值。

2.2 滑动窗口计数

固定时间窗口算法中窗口有很多,其跨度和位置是与时间区间绑定,因此是很多固定不动的窗口。而滑动时间窗口算法中只包含1个固定跨度的窗口,但窗口是可移动动的,与时间区间无关。

具体规则如下:

  • 窗口时间跨度Interval大小固定,例如1秒
  • 时间区间跨度为Interval / n ,例如n=2,则时间区间跨度为500ms
  • 窗口会随着当前请求所在时间currentTime移动,窗口范围从currentTime-Interval时刻之后的第一个时区开始,到currentTime所在时区结束。

image-20241009194901251

限流阈值依然为3,绿色小块就是请求,上面的数字是其currentTime值。

  • 在第1300ms时接收到一个请求,其所在时区就是1000~1500
  • 按照规则,currentTime-Interval值为300ms,300ms之后的第一个时区是5001000,因此窗口范围包含两个时区:5001000、1000~1500,也就是粉红色方框部分
  • 统计窗口内的请求总数,发现是3,未达到上限。

若第1400ms又来一个请求,会落在1000~1500时区,虽然该时区请求总数是3,但滑动窗口内总数已经达到4,因此该请求会被拒绝:

image-20241009194936726

假如第1600ms又来的一个请求,处于15002000时区,根据算法,滑动窗口位置应该是10001500和1500~2000这两个时区,也就是向后移动:

image-20241009194947384

这就是滑动窗口计数的原理,解决了我们之前所说的问题。而且滑动窗口内划分的时区越多,这种统计就越准确。

2.3 令牌桶算法(Sentinel的热点参数)

其基本思路如图:【Sentinel中的热点参数(一段时间内频繁访问的用户id)限流

image-20241009201027208

说明:

  • 生成令牌:以固定的速率生成令牌,存入令牌桶【令牌桶满了以后,多余令牌丢弃】
  • 进入请求:必须先尝试从桶中获取令牌,①获取到令牌后才可以被处理②令牌桶中没有令牌,则请求等待或丢弃

基于令牌桶算法,每秒产生的令牌数量==QPS上限

当然也有例外情况,例如:

  • 某一秒令牌桶中产生了很多令牌,达到令牌桶上限N,缓存在令牌桶中,但是这一秒没有请求进入。
  • 下一秒的前半秒涌入了超过2N个请求,之前缓存的令牌桶的令牌耗尽,同时这一秒又生成了N个令牌,于是总共放行了2N个请求。超出了我们设定的QPS阈值。

因此,在使用令牌桶算法时,尽量不要将令牌上限设定到服务能承受的QPS上限。而是预留一定的波动空间,这样我们才能应对突发流量。

2.4 漏桶算法(Sentinel的排队等待)

漏桶算法与令牌桶相似,但在设计上更适合应对并发波动较大的场景,解决令牌桶中的问题。

简单来说就是请求到达后不是直接处理,①放入一个队列。②固定的速率从队列中取出并处理请求。[叫漏桶算法,就是把请求看做水,队列看做是一个漏了的桶]

image-20241009203016735

漏桶的优势就是流量整型,桶就像是一个大坝,请求就是水。并发量不断波动,就如图水流时大时小,但都会被大坝拦住。而后大坝按照固定的速度放水,避免下游被洪水淹没。

因此,不管并发量如何波动,经过漏桶处理后的请求一定是相对平滑的曲线:

image-20241009203304507

MybatisPlus

1.Mybatis介绍

在日常开发中应该能发现,单表的CRUD功能代码重复度很高,也没有什么难度。而这部分代码量往往比较大,开发起来比较费时。

因此,目前企业中都会使用一些组件来简化或省略单表的CRUD开发工作。目前在国内使用较多的一个组件就是MybatisPlus。

官方网站如下:

当然,MybatisPlus不仅仅可以简化单表操作,而且还对Mybatis的功能有很多的增强。

==Mybatis——-基础使用==

1.1 pom.xml引入依赖

MybatisPlus提供了starter,实现了自动Mybatis以及MybatisPlus的自动装配功能,坐标如下:

image-20240425162339464

如图所示,由于这个starter包含对mybatis的自动装配,因此完全可以替换掉Mybatis的starter

1.2 定义Mapper层

为了简化单表CRUD,MybatisPlus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD:

我们直接==实现BaseMapper接口==即可

image-20240425173015132

1.3 对比

我们可以看出这样直接调用简单的CRUD方法即可,就不用自己去mapper层写方法和对应xml文件了。==只需要继承BaseMapper就能省去所有的单表CRUD==。

image-20240425173132151

1.4 底层实现原理

刚才①引入依赖和②mapper层继承BaseMapper接口就可以进行CRUD,那MP怎么知道是哪张表?表中有哪些字段?

这也是因为UserMapper在继承BaseMapper的时候指定了一个泛型和数据库对应的实体类

image-20240425174306575

MybatisPlus就是根据PO实体的信息来推断出表的信息,从而生成SQL的。默认情况下:

  • MybatisPlus会把PO实体的类名驼峰转下划线作为表名
  • MybatisPlus会把PO实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型
  • MybatisPlus会把名为id的字段作为主键

image-20240425174818635

但很多情况下,默认的实现与实际场景不符,因此MybatisPlus提供一些注解便于我们声明表信息

2.常见注解==解决po和mysql字段映射==

==如果不按照约定的话,需要使用以下三种注解来解决:==

MybatisPlus中比较常用的几个注解如下:

@TableName:用来指定表名

@TableId:用来指定表中的主键字段信息

@TableField:用来指定表中的普通字段信息

image-20240425175502493

其中,具体的细节如图所示:==使用查看==

image-20240425175807030

2.1 @TableName

  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类

所有属性:

image-20240425180531600

2.2 @TableId

  • 描述:主键注解,标识实体类中的主键字段
  • 使用位置:实体类的主键字段

image-20240425180555143

其中type=IdType.xxxx取值范围:

image-20240425180718677

2.3 @TableField

描述:普通字段注解

image-20240425180842453

3.yml常见配置

在application.yml文件配置:

image-20240425171943982

==Mybatis——-核心功能==

刚才都是以id为条件的简单CRUD,一些复杂的SQL语句就需要用到一些高级功能。

1.条件构造器==提供复杂where语句==

修改、删除、查询的SQL语句都需要指定where条件

因此BaseMapper中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。

image-20240425181511109

参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

image-20240425181331409

其中,Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:

image-20240425181538417

而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:

image-20240425181553298

而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:

image-20240425181601512

1.1 QueryWrapper

==主要对where语句的条件进行设置==

对于查询:

1
2
3
select id,username,info,balance
from user
where name like "%o%" AND balance >= 1000

image-20240426152253324

对于修改:

1
2
3
update 
set balance=2000
where username='Jack'

image-20240426152523969

1.2 UpdateWrapper

==弥补BaseMapper中update()只能写 set Xxx==,提出的updatewrapper可以写成set balance=balance-xx这种形式

以更新多个id为例:

1
2
3
update user
set balance=balance-200
where id in(1,2,3)

这个set的赋值结果是基于字段现有值,这时候需要使用UpdateWrapper中的==setSql功能:==

image-20240426153602036

1.3 LambdaQueryWrapper

==1.1和1.2会在构造条件时候写死字段名称==,现在1.3就可以通过变量的getter方法结合反射获取

image-20240426154148926

2.自定义SQL

1.2中演示了一个修改余额-200的时候将sql维护应该放在持久层,而不是业务层:
image-20240426154345876

==利用Wrapper生成查询条件,然后再结合mapper自定义xml文件编写sql==

2.1 原位置变化

以刚才案例为例:

image-20240426154550302

2.2 Mapper层方法定义

image-20240426154750747

2.3 写sql语句

方式一:直接在mapper的方法上写@Select方法

方式二:在mapper.xml文件中写动态sql

和以往的区别就是:==where语句直接用${ew.customSqlSegment}替换==

image-20240426154956395

总结如下

与以往的变化就是我传入参数和where判断条件,mapper方法加一个@Param(“ew”)标志,然后sql里面就直接用${ew.customSqlSegment}替换

3.Service接口

通用接口为==Iservice==,默认实现为==ServiceImpl==。其中封装方法可以分为:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

3.1 五大类方法解释

3.1.1 新增(save)

image-20240426155746820

3.1.2 删除(remove)

image-20240426155808475

3.1.3 修改(update)

image-20240426155852795

3.1.4 查询

3.4.1 查询一条(get)

image-20240426155954622

3.4.2 查询多条(list)

image-20240426160001166

3.4.3 计数(count)

image-20240426160025902

3.1.5调用mapper层自定义sql

通过getBaseMapper获取Mapper,然后就mapper.自定义sql()

image-20240426160126219

3.6 基本用法

现在的变化就是,==拿现成的直接用==:

image-20240426160527425

具体操作就是:

image-20240426160603797

1
--保证自定义mapper继承basemapper 【底层使用时候直接还是调用basemapper的方法】

3.7 快速搭建(直接看)

==1.业务简单的话直接调用mp方法;==

==2.业务复杂的话就跟原来方式一样,controller调用service方法,然后在mapper层写具体sql==

3.7.1 简单业务-直接调用mp方法

image-20240426170341983

3.7.2 复杂业务-原始模式优化

image-20240426171113039

之后调用mapper层的sql:

image-20240426170626547

3.8 Lambda查询[添加属性]

就是在基本的方法上(属性,最新值)再多使用一个属性(==判断条件==,属性,最新值)

这样就可以把动态sql里面标签这种麻烦的操作放在serviceImpl类上进行操作

image-20240426175243646

3.9 批量新增

三种方案:

image-20240427230857329

最推荐第三种我们在yml配置文件中添加&rewriteBatchedStatements=true

==Mybatis——-扩展功能==

1.代码生成

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦

==为了方便生成基本固定的代码==

1.1 下载插件

image-20240428181825670

1.2 配置数据库

image-20240428182003520

1.3 配置信息生成代码

image-20240428182342159

1.4查看代码

image-20240428182458664

2.静态工具—-Db

有一种可能就是有AService用来查询用户和BService用来查询地址,他们都实现了Iservice可以实现一些简单的CRUD。现在需要查询用户和对应的地址,就可能AService调用BService,然后BService也要调用AService就会导致@Autowired时候循环依赖

MybatisPlus提供一个静态工具类:==Db==,==就是用来解决多个service层互相调用导致的循环依赖==,其中一些静态方法与IService中的方法签名基本一致,也可以帮助我们实现CRUD的功能

image-20240428183527812

在使用的时候,就可以直接像平时书写习惯直接调用

3.逻辑删除

多表查询时删除A表的数据同时也会删除B数据,但是B里面有一些比较重要的数据我们不想删除。因此,我们采用==逻辑删除==的方案:

可以考虑在表中添加一个字段flag(标记数据是否被删除),这样我们在删除数据的时候还需要将flag设置为true,如果在查询数据的时候还需要添加一个and flag=xxx的条件。这样的话就会让之前的查询和删除逻辑都要跟着变化,非常麻烦。

因此,MybatisPlus就添加了对逻辑删除的支持。

只有MybatisPlus生成的SQL语句才支持自动的逻辑删除【就是直接拿来用的哪些CRUD方法】

自定义SQL就需要自己手动处理逻辑删除

3.1 配置逻辑删除

我们对于Address表添加一个字段deleted用于判断是否删除:

image-20240429170953086

3.2 底层实现

我们在使用MybatisPlus自己的CRUD方法时候支持自动逻辑删除:

image-20240429121132491

具体的两个语法操作:

1.删除的时候我们就会将delete更改为一个update语句拼接一个deleted=false未被删除的判断

image-20240429171532251

2.查询的时候我们就会在where语句拼接一个deleted=false未被删除的判断

image-20240429171544562

3.3 注意事项

开启逻辑删除功能之后,可以像普通删除一样做CRUD,基本不用考虑代码逻辑功能问题。

但是,逻辑删除本身也有缺点:

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率
  • sql中全都需要对逻辑删除字段做判断,影响查询效率

==因此,不太建议采用逻辑删除功能,如果数据不能删除,可以采用数据迁移到其他表的办法==

4.枚举处理器(字段有多个值)

对某个字段(0是正常,1是不正常)判断时候如果写==1这样很不美观,并且如果0和1的含义修改了要修改很多地方,因此我们可以使用枚举(很像c语言的参数宏定义)来处理

针对于之前案例User类的status属性,就可以这样修改:

image-20240429163735826

在原始的mybatis底层帮我们把Java中的类型和数据库的类型一一对应,但是对于枚举类型和Json类型无法解决。因此mybatisplus针对枚举和Json类型提出了新的处理器:

image-20240429164303308

4.1 配置枚举处理器

image-20240429164034948

4.2 定义枚举类

这样就可以将1和2分别代表正常和冻结,我们在使用的时候只需要调用UserStatus.NORMAL就可以对比了

此外,@EnumValue可以保证我们可以按照value的类型和数据库一一对应;而@JsonValue可以保证我们输出给前端的时候可以将描述词/对应值返回(而不是返回NORMAL/FORZEN这种类型)

image-20240429165234689

4.3 修改PO和VO类型

主要是将类型Integer改为UserStatus枚举类

image-20240429165517353

4.4 修改具体逻辑位置

原来位置是用数字比对,可读性太差,现在就可以优雅地使用枚举类

image-20240429165653581

5.Json类型处理器(字段是Json类型)

如果实体类有一个属性是Json类型,那么Java中的Json类型和数据库中的匹配就有问题:
就跟4枚举处理器里面将的,MybatisPlus在Myabtis的基础上提供了Json类型处理器

image-20240429172137951

5.1 配置Json类型处理器

image-20240430142629145

因为没有提供在application.yml配置的方式,只能通过给实体类属性添加注解

image-20240430142453316

5.2 测试查看

info字段已经改成了一个Json类型

image-20240430142717732

6.配置加密

目前我们配置文件中很多参数都是明文存储,如果开发人员跑路很容易导致敏感信息泄露。

MyBatisPlus从3.3.2版本开始提供了一个==基于AES算法的加密工具==,帮助我们对配置中的敏感信息做加密处理。

6.1 生成秘钥

以数据库的账户密码为例:

image-20240430143223630

6.2 配置秘钥

在application.yml文件中修改:

image-20240430143508047

6.3 测试

测试类:在测试类的注解上配置:

image-20240430143859062

启动项目:

image-20240430144043661

==Mybatis——-插件功能==

其实MybatisPlus提供了多个插件,而我们重点关注分页插件

image-20240430164735500

1.分页插件

1.1 配置分页功能

image-20240430152339416

1.2 测试简单分页

image-20240430152253190

1.3 测试复杂分页

针对于1.2的话其实就是更针对业务逻辑:

image-20240430164354320

==Mybatis——-使用操作==

1.可以创建好数据库表

2.根据mybatis插件生成:

​ po(可以添加注解保证数据库和Java实体类对应,对于枚举和Json类型都有新推出的处理器解决),

​ service(extends IService),

​ serviceImpl( extends ServiceImpl<XxxMapper, Xxx> implements IAddressService), [引入mapper方法:①注入xxxMapper ②直接getBaseMapper]

​ controller,

​ mapper(extends BaseMapper)

3.按照原有的设计思路写代码:

​ 3.1 简单的就直接调用service的CRUD方法【service接口默认也有实现类ServiceImpl<XXXMapper,实体类>,这样也说明底层还是直接调用BaseMapper方法】

​ 3.2 复杂的话,①xml文件按照原来的动态sql书写

​ 3.3 复杂的话,②使用xxxMapper.条件构造器[创建复杂where语句]

​ 3.4 复杂的话,③使用lambdaQuery()/lambdaUpdate()添加一些where语句 –新特性【好用】

【只不过在书写过程中有很多好用的扩展功能】

,