Cloud表示应用程序位于云中,而不是传统的数据中心;Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿态运行,充分利用和发挥云平台的弹性+分布式优势。
符合云原生架构的应用程序应该是:采用k8s+docker进行容器化,基于微服务架构提高灵活性和可维护性,借助敏捷方法、DevOps支持持续迭代和运维自动化,利用云平台设施实现弹性伸缩、动态调度、优化资源利用率。
一、微服务
3.1 单体应用
:::success
- 改动影响大,风险高(不论代码改动多小,成本都相同)
- 系统高可用性差(一旦某一功能涉及的代码或者资源有问题,那就会影响整个功能) :::
1.单体架构所有的模块全都耦合在一块,代码量大,维护困难
2.单体架构所有的模块都共用一个数据库,存储方式比较单一。
3.单体架构所有的模块开发所使用的技术一样,微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
3.2 微服务
微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务——单一职责。每个服务都有自己的处理和轻量通讯机制,可以部署在单个或多个服务器上。
优点: :::danger
- 逻辑清晰
这个特点是由微服务的单一职责的要求所带来的。一个仅负责一项很明确业务的微服务,在逻辑上肯定比一个复杂的系统更容易让人理解。
- 可扩展
- 技术异构
在一个大型系统中,不同的功能具有不同的特点,并且不同的团队可能具备不同的技术能力。因为微服务间松耦合,不同的微服务可以选择不同的技术栈进行开发。
- 高可靠
微服务间独立部署,一个微服务的异常不会导致其它微服务同时异常。通过隔离、融断等技术可以避免极大的提升微服务的可靠性 :::
缺点: :::success
- 复杂度高
- 运维复杂
系统由多个独立运行的微服务构成,需要一个设计良好的监控系统对各个微服务的运行状态进行监控。运维人员需要对系统有细致的了解才对够更好的运维系统。
- 分布式系统可能复杂难以管理
- 因为分布部署跟踪问题难 :::
Feign是一种负载均衡的HTTP客户端, 使用Feign调用API就像调用本地方法一样,从避免了调用目标微服务时,需要不断的解析/封装json 数据的繁琐。Feign集成了Ribbon。Ribbon+eureka是面向微服务编程,而Feign是面向接口编程。
3.3 SpringCloud
不论是商业应用还是用户应用,在业务初期都很简单,我们通常会把它实现为单体结构的应用。但是,随着业务逐渐发展,产品思想会变得越来越复杂,单体结构的应用也会越来越复杂。这就会给应用带来如下的几个问题:
- 代码结构混乱:业务复杂,导致代码量很大,管理会越来越困难。同时,这也会给业务的快速迭代带来巨大挑战;
- 开发效率变低:开发人员同时开发一套代码,很难避免代码冲突。开发过程会伴随着不断解决冲突的过程,这会严重的影响开发效率;
- 排查解决问题成本高:线上业务发现 bug,修复 bug 的过程可能很简单。但是,由于只有一套代码,需要重新编译、打包、上线,成本很高。
由于单体结构的应用随着系统复杂度的增高,会暴露出各种各样的问题。近些年来,微服务架构逐渐取代了单体架构,且这种趋势将会越来越流行。Spring Cloud是目前最常用的微服务开发框架,已经在企业级开发中大量的应用。
设计目标
优缺点
优点:
- 组件丰富,功能齐全。Spring Cloud 为微服务架构提供了非常完整的支持。例如、配置管理、服务发现、断路器、微服务网关等;
- 服务拆分粒度更细,耦合度比较低,有利于资源重复利用,有利于提高开发效率
- 减轻团队的成本,可以并行开发,不用关注其他人怎么开发,先关注自己的开发
- 微服务可以是跨平台的,可以用任何一种语言开发
- 适于互联网时代,产品迭代周期更短
缺点:
- 微服务过多,治理成本高,不利于维护系统
- 分布式系统开发的成本高(容错,分布式事务等)
SpringBoot和SpringCloud的区别
SpringBoot专注于快速方便的开发单个个体微服务。
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,
为各个微服务之间提供,配置管理、服务发现、服务熔断、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
Spring Cloud 和dubbo区别
Dubbo 专注 RPC 和服务治理,Spring Cloud 则是一个微服务架构生态。
Zookeeper 保证的是CP ,但对于服务发现而言,可用性比数据一致性更加重要 ,而 Eureka(Nacos) 设计则遵循AP原则
Dubbo 一些问题
- Registry 严重依赖第三方组件(zookeeper 或者 redis),当这些组件出现问题时,服务调用很快就会中断。
- Dubbo 只支持 RPC 调用。使得服务提供方(抽象接口)与调用方在代码上产生了强依赖,服务提供者需要不断将包含抽象接口的 jar 包打包出来供消费者使用。一旦打包出现问题,就会导致服务调用出错,并且以后发布部署会成很大问题(太强的依赖关系)。
- Dubbo 只是实现了服务治理,其他微服务框架并未包含,如果需要使用,需要结合第三方框架实现(比如分布式配置用淘宝的 Diamond、服务跟踪用京东的 Hydra,但使用相对麻烦些),开发成本较高,且风险较大。
Spring Cloud 的一些优缺点
- 有强大的 Spring 社区、Netflix 等公司支持,并且开源社区贡献非常活跃。
- 标准化的将微服务的成熟产品和框架结合一起,Spring Cloud 提供整套的微服务解决方案,开发成本较低,且风险较小。
- 基于 Spring Boot,具有简单配置、快速开发、轻松部署、方便测试的特点。
- 支持 REST 服务调用,相比于 RPC,更加轻量化和灵活(服务之间只依赖一纸契约,不存在代码级别的强依赖),有利于跨语言服务的实现,以及服务的发布部署。
缺点
- 另外,REST 服务调用性能会比 RPC 低一些(但也不是强绑定)
- Spring Cloud 整合了大量组件,相关文档比较复杂,需要针对性的进行阅读。
二、电商
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
所谓的同源是指,域名、协议、端口均为相同。
跨域的解决方案
- 方法1:设置nginx包含admin和gateway。都先请求nginx,这样端口就统一了,使用nginx做反向代理
- 方法2:让服务器告诉预检请求能跨域 ——在网关写一个config配置类,该类用来做过滤,允许所有的请求跨域。
0.ngnix
Nginx是一款轻量级的Web服务器、反向代理服务器
由于防火墙的原因,我们并不能直接访问谷歌,那么我们可以借助VPN来实现,这就是一个简单的正向代理的例子。这里你能够发现,正向代理“代理”的是客户端,而且客户端是知道目标的,而目标是不知道客户端是通过VPN访问的。
反向代理“代理”的是服务器端,而且这一个过程对于客户端而言是透明的。
1.缓存
分布式本地缓存
(1)缓存不共享
在这种情况下,每个服务都有一个缓存,但是这个缓存并不共享,水平上当调度到另外一个台设备上的时候,可能它的服务中并不存在这个缓存,因此需要重新查询。
(2)缓存一致性问题
在一台设备上的缓存更新后,其他设备上的缓存可能还未更新,这样当从其他设备上获取数据的时候,得到的可能就是未给更新的数据。
在这种下,一个服务的不同副本共享同一个缓存空间,缓存放置到缓存中间件中,这个缓存中间件可以是redis等,而且缓存中间件也是可以水平或纵向扩展的,如Redis可以使用redis集群。它打破了缓存容量的限制,能够做到高可用,高性能。
[
](https://blog.csdn.net/wts563540/article/details/109437094)
1.1分布式锁
当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题
所有的服务都到同一个redis进行获取数据,就可以避免这个问题
当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
1.购物车服务
0.1 分布式session不共享不同步的问题
问题描述:
- session不可跨域,它有自己的作用范围。例如:在auth.kedamall.com中保存session,但是网址跳转到kedamall.com中,取不出auth.kedamall.com中保存的session
- 同一个服务,复制多份,session不同步问题。
解决方案: 统一存储
1.1 数据模型分析
购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又是需要持久化,因此这里我们选用 Redis 存储购物车数据。
一个购物车是由各个购物项组成的,但是我们用List进行存储并不合适,因为使用List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用Hash进行存储
每个人都有一个hash表,key为skuId,value为数据
1.2 购物车详细步骤
1.会为临时用户生成一个name为user-key的cookie临时标识,过期时间为一个月
2.ThreadLocal用户身份鉴别
但是注意的是tomcat中线程可以复用,所以线程和会话不是一对一的关系。但是没有关系,会在拦截器中先判断会话有没有用户信息(cookie),
- 在调用购物车的接口前,先通过 Session 信息判断是否登录,并分别进行用户身份信息的封装,并把user-key放在 Cookie 中
- 这个功能使用拦截器进行完成
ThreadLocal在拦截器返回之前使用set方法将用户信息保存,这样Controller就可以使用用户信息。
3.获取用户购物车数据
先获取redis里该用户购物车的那个map,每个用户的购物车都是个map,map名为ATGUIGU:cart:用户id
- 若用户未登录,则直接使用user-key获取购物车数据
- 否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
2.订单服务
讨论:多次点击 【提交订单】 按钮
幂等性:订单提交一次和提交多次结果是一致的
哪些情况要防止:
- 用户多次点击按钮
- 用户页面回退再次提交
- 服务相互调用,由于网络间题,导致请求失败。feign触发重试机制
2.1幂等解决方案
2.1.1token机制
- 服务端提供了发送 token 的接口。 必须在执行幂等业务前,先去获取 token,服务器会把 token 保存到 redis当中。
- 然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
- 服务器判断 token 是否存在 redis 中,存在表示第一次请求,==然后删除 token,继续执行业务。==
- 如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
token的获取、比较、删除必须是原子性,如果不是原子操作,可能会导致在高并发场景下,都get到同样的数据,判断都成功,导致并发问题。 可能由于延迟,订单提交按钮可能被点击多次。为了防止重复提交(保证幂等性),==我们在返回订单确认页时,在Redis中放入一个随机生成的令牌,过期时间为 30mi;,提交的订单时会携带这个令牌,我们将会在订单提交的处理页面核验此令牌==(生成的令牌是根据用户的ID生成的,用户ID从拦截器获取)
2.1.2各种锁
==a.数据库悲观锁==
select * from ×× where id=1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id字段一定是主键或者唯一幸引,不然可能造成锁表的结果,处理起来会非常麻烦。
==b.数据库乐观锁==
这种方法适合在更新的场景中,
updatet _goods set count=count-1,version=version+1 where good_id=2 and version=1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了间题,订单服务又一次发起调用库存,服务,当订单服务传如的version还是1,再执行上面的四!v语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题
2.1.3各种唯一约束
==a.数据库唯一约束==
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的话,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
==b.redis set防重==
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理。
2.1.4防重表
使用订单号orderNo做为去重表的唯一索引,把唯一幸引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
2.2锁定库存
锁定库存使用更新语句,因为要使stock_locked的数量增加。
UPDATE `wms_ware_sku` SET stock_locked = stock_locked + #{num}
WHERE sku_id = #{skuId}
AND ware_id = #{wareId}
AND stock - stock_locked >= #{num}
分布式事务
原始方法:使用异常机制做回滚,为整个下订单的方法加上事务,远程调用锁库存失败,抛出异常,整个方法回滚。
- 但是锁库存方法可能会是假失败,因为网络异常,调用时间太久,会抛出异常整个下订单事务就会回滚,但是锁库存已经成功了,但是下订单失效了。
- 远程服务执行完成,下面的其他方法出现问题,已经执行的远程服务肯定不能回滚。
分布式事务解决方案
- 2PC模式:两阶提交(不怎么使用)
- 柔性事务:遵循BASE理论,最终一致性。⚡️最大努力通知型方案⚡️可靠消息+最终一致性方案(异步确保型)
Seata分布式事务
但在微服务架构中,这3个模块会变为3个独立的微服务,各自有自己的数据源,调用逻辑就变为:
Business 是业务入口,在程序中会通过注解来说明他是一个全局事务,这时他的角色为 TM(事务管理者)。
Business 会请求 TC(事务协调器,一个独立运行的服务),说明自己要开启一个全局事务,TC 会生成一个全局事务ID(XID),并返回给 Business。
RM(资源管理者)会收到 XID,知道自己的事务属于这个全局事务。Storage 执行自己的业务逻辑,操作本地数据库。
RM(资源管理者)会把自己的事务注册到 TC,作为这个 XID 下面的一个分支事务,并且把自己的事务执行结果也告诉 TC。
Account 的执行逻辑与 Storage 一致,均是RM
在各个微服务都执行完成后,TC 可以知道 XID 下各个分支事务的执行结果,TM(Business) 也就知道了。
Business 如果发现各个微服务的本地事务都执行成功了,就请求 TC 对这个 XID 提交,否则回滚。
TC 收到请求后,向 XID 下的所有分支事务发起相应请求。
各个微服务收到 TC 的请求后,执行相应指令,并把执行结果上报 TC。
重要机制
(1)全局事务的回滚是如何实现的呢?
Seata 有一个重要的机制:回滚日志。
每个分支事务对应的数据库中都需要有一个回滚日志表 UNDO_LOG,在真正修改数据库记录之前,都会先记录修改前的记录值,以便之后回滚。
在收到回滚请求后,就会根据 UNDO_LOG 生成回滚操作的 SQL 语句来执行。
如果收到的是提交请求,就把 UNDO_LOG 中的相应记录删除掉。
(2)RM 是怎么自动和 TC 交互的?
是通过监控拦截JDBC实现的,例如监控到开启本地事务了,就会自动向 TC 注册、生成回滚日志、向 TC 汇报执行结果。
(3)二阶段回滚失败怎么办?
例如 TC 命令各个 RM 回滚的时候,有一个微服务挂掉了,那么所有正常的微服务也都不会执行回滚,当这个微服务重新正常运行后,TC 会重新执行全局回滚。
工作过程
- TM 请求 TC,开始一个新的全局事务,TC 会为这个全局事务生成一个 XID。
- XID 通过微服务的调用链传递到其他微服务。
- RM 把本地事务作为这个XID的分支事务注册到TC。
- TM 请求 TC 对这个 XID 进行提交或回滚。
-
AT 模式
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
第二阶段的逻辑就比较简单了。RM和TC之间是有长连接的,如果是正常全局提交,则TC通知多个RM异步清理掉本地的redo和undo log即可。如果是回滚,则TC通知每个RM回滚数据即可。由于我们在操作本地业务操作的前后,做记录了undo和redo log,因此可以通过undo log进行回滚。
但是还会存在一个问题,因为每个事务从本地提交到通知回滚这段时间里,可能这条数据已经被别的事务修改,如果直接用undo log回滚,会导致数据不一致的情况。
此时,RM会用redo log进行校验,对比数据是否一样,从而得知数据是否有别的事务修改过。注意:undo log是被修改前的数据,可以用于回滚;redo log是被修改后的数据,用于回滚校验。
如果数据未被其他事务修改过,则可以直接回滚;如果是脏数据,再根据不同策略处理。
[
](https://blog.csdn.net/a315157973/article/details/103113483)
Seata的数据隔离性
Seata的写隔离级别是全局独占的。
因为事务开启之前,TM会在TC中获取全局锁。锁的key会以行数据的维度来确定,即同一个数据库中的某个表中的某行数据,在同一时间只会被一个事务操作。
读的隔离级别是Read Uncommitted,因为每个服务的本地事务是单独提交的,因此在全局事务未提交之前,是可以读取到部分数据的。
如果应用一定要达到Read Committed级别,可以使用SELECT FOR UPDATE 语句。对于这种语句,Seata会通过SELECT FOR UPDATE 持有数据的行锁,直到全局锁是已提交的,才返回。
Seata默认Read Uncommitted级别是出于性能的考虑。
[
](https://blog.csdn.net/a315157973/article/details/103113483)
消息队列完成最终一致性
实现:RabbitMQ可以通过设置队列的TTL和 死信路由 实现延迟队列
① 订单超时未支付触发订单过期状态修改与库存解锁创建订单时消息会被发送至队列order.delay.queue,经过 TTL 的时间后消息会变成死信以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理
- 如果该订单已支付,则无需处理
- 否则说明该订单已过期,修改该订单的状态并通过路由键order.release.other发送消息至队列stock.release.stock.queue进行库存解锁
② 库存锁定后延迟检查是否需要解锁库存在库存锁定后通过路由键stock.locked发送至延迟队列stock.delay.queue,延迟时间到,死信通过路由键stock.release转发至stock.release.stock.queue,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁
- ==由于关闭订单和库存解锁都有可能被执行多次,因此要保证业务逻辑的幂等性,在执行业务时重新查询当前的状态进行判断==
- ==订单关闭和库存解锁都会进行库存解锁的操作,来确保业务异常或者订单过期时库存会被可靠解锁==