1、订单链路

订单中心

电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。

订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

image-20220228080432263

订单流程

image-20220228081824936

Feign远程异步调用丢失请求头、上下文问题

Feign远程调用丢失请求头问题

image-20220228091104146

feign异步情况丢失上下文问题

image-20220228091118962

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ Feign远程调用丢失请求头问题
+ 原因
+ fegin调用没带session
+ 创建一个新request,没有请求头
+ 解决(同步老请求cookie)
+ 加个feign请求调用的拦截器
+ ThreadLocal获取老请求请求属性——cookie
+ 设置新请求cookie
+ feign异步情况丢失上下文问题
+ 原因
+ requecontexHolder底层是同一线程共享数据
+ 通过ThreadLocal获取老请求,异步编排非同一线程
+ 解决
+ 执行异步前,提前共享请求数据

也可以传传用户id,传用户会话,后者安全些,

1、id传参

2、传递cookie

2、接口幂等性

订单确认页流程

保证幂等情况

1
2
3
4
+ 用户多次点击按钮
+ 用户页面回退再次提交
+ 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
+ 其他业务情况

幂等解决方案:

1、token机制

1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。

危险性(非原子操作):
1、先删除 token 还是后删除 token;
(1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致, 请求还是不能执行。
(2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别 人继续重试,导致业务被执行两边
(3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
2、Token 获取、比较和删除必须是原子性
(1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导 致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行
(2) 可以在 redis 使用 lua 脚本完成这个操作 if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end

1
2
3
4
必须原子性:lua脚本
1、获取令牌
2、对比token
3、删除令牌

2、各种锁机制

数据库悲观锁

select * from xxxx where id = 1 for update; 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。 另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。

数据库乐观锁

这种方法适合在更新的场景中, update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1 根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候 带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务 version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订 单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变 为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。乐观锁主要使用于处理读多写少的问题

业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数 据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。

3、各种唯一约束

数据库唯一约束

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。 这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键 的要求不是自增的主键,这样就需要业务生成全局唯一的主键。 如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。

redis set防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。

4、防重表

使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且 他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。

redis 防重也算

5、全局请求唯一id

调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。

可以使用 nginx 设置每一个请求的唯一 id;

proxy_set_header X-Request-Id $request_id;

总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
+ token令牌
+ 原子性获取、对比、删除token
+ 锁
+ 数据库乐观锁、悲观锁
+ 分布式锁
+ 唯一约束
+ 数据库主键唯一约束
+ MD5加密redis set
+ 防重表
+ 操作之后以唯一索引放入去重表中,且与业务表同一数据库保证同一事务
+ 全局请求唯一id
+ 调用接口生成唯一id,放到redis中去重
+ 链路追踪

3、分布式事务

1、本地事务

事务的基本性质

数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称就是 ACID。

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败;

  • 一致性:数据在事务的前后,业务整体一致;

  • 隔离性:事务之间互相隔离;

  • 持久性:一旦事务成功,数据一定会落盘在数据库。

事务的隔离级别

  • READ UNCOMMITTED(读未提交):该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。

  • READ COMMITTED(读提交):一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。

  • REPEATABLE READ(可重复读):该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL的 InnoDB 引擎可以通过 next-key locks 机制(参考下文”行锁的算法”一节)来避免幻读。

  • SERIALIZABLE(序列化):在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

事务的传播级别

  1. PROPAGATION_REQUIRED: :如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

  2. PROPAGATION_SUPPORTS: :支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。

  3. PROPAGATION_MANDATORY: :支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

  4. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

  6. PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。

  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

问题:

1
2
3
4
5
6
7
8
+ 远程调用超时——假异常
+ 抛出异常导致全部回滚
+ 扣减积分异常
+ 扣减积分可以回滚
+ 本地的订单可以回滚
+ 远程锁库存无法回滚
+ 事务无法控制远程服务
+ 还需手动释放库存

2、SpringBoot事务

@Transactional

内部事务互调问题:

在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。

原因:同一对象内事务互调默认失效,绕过了代理对象。

解决:

  1. 导入 spring-boot-starter-aop

  2. @EnableTransactionManagement(proxyTargetClass = true)

  3. @EnableAspectJAutoProxy(exposeProxy=true)

  4. AopContext.currentProxy() 调用方法

1
2
3
4
5
+ 本地事务互调解决
+ 使用代理对象调用事务方法
+ 引入aop的aspectj
+ 开启aspectj动态代理功能
+ spring中bean可以代替 aspectj

3、分布式事务

image-20220228092926460

1、远程服务假失败

2、远程事务回滚

分布式CAP定理

分布式系统经常出现的异常机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。

image-20220228094112202

1
2
+ CAP定理
+ 一致、可用二选一,分区容错性无法避免

Raft原理

image-20220228094509282

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+ 分布式系统实现一致性的Raft算法、paxos等
+ leader选举
+ 随从自旋时间(随机),等待命令
+ 自旋结束的随从变成候选人,开启一轮新的选举
+ 随从若当前未投票可发送投票
+ 随从投过票后重新自旋
+ leader发送追加日志信息给随从
+ 消息发送以间隔(心跳时间)发送
+ 随从收到消息后重置自旋时间
+ leader宕机
+ 随从自旋时间结束,变为候选者
+ 开启选举,成为leader
+ 如果出现两个候选者,出现投票分离
+ 谁的投票请求先到投谁,其他的拒收
+ 票数一样
+ 重写来一轮自旋,候选者选举
+ 直到候选者票数最多选出leader
+ 日志复制
+ leader将所有改变复制给其他node
+ 使用追加日志方式复制(在每一个心跳时发送)
+ 随从收到同步后回复消息
+ leader收到大多数回复后提交,并响应客户端
+ 在下一个心跳时间,请求其他node提交

image-20220228094701842

image-20220228094710919

1
2
3
4
5
6
7
8
9
+ 出现分区错误
+ 分区之后
+ 接收不到消息,选举领导
+ 大多数同意才能进行改变
+ 恢复网络分区
+ 低轮次leader退位
+ 处于低轮次的node未提交日志全部回滚
+ 匹配新leader日志
+ 全部node一致

Raft算法官网:https://raft.github.io/

Base理论

是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性

BASE 是指

  • 基本可用(Basically Available)

    • 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
      • 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。
      • 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 软状态( Soft State)

    • 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication 的异步复制也是一种体现。
  • 最终一致性( Eventual Consistency)

    • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
1
2
3
4
5
6
7
+ base
+ 基本可用
+ 允许损失部分可用性
+ 软状态
+ 允许系统存在中间状态
+ 最终一致性
+ 经历一定时间后,最终能达到一致性状态

强一致性、弱一致性、最终一致性

​ 从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是 强一致性。如果能容忍后续的部分或者全部访问不到,则是 弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是 最终一致性。

分布式事务解决方案

2PC模式

数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。

MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:

第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。

第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。

image-20220228100342688

  • XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低。

  • XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景

  • XA 目前在商业数据库支持的比较理想,在 在 mysql 数据库中支持的不太理想,mysql 的XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。

  • 许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。

  • 也有 3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)

1
2
3
+ 2PC模式(XA):二阶段提交
+ 使用场景小
+ 高并发性能差

柔性事务-TCC

刚性事务:遵循 ACID 原则,强一致性。

柔性事务:遵循 BASE 理论,最终一致性;

与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

image.png

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。

二阶段 commit 行为:调用 自定义 的 commit 逻辑。

二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

image-20220228100848491

1
2
3
4
+ 柔性事务-TCC事务补偿型方案
+ 一阶段prepare——try
+ 二阶段commit——confirm
+ 二阶段rollback——cancel

柔性事务-最大努力通知型方案(结合MQ)

按规律进行通知, 不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通知次数后即不再通知。

案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调。

柔性事务-可靠消息 + 最终一致性方案(异步确保型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

防止消息丢失:

  1. 做好消息确认机制( pulisher , consumer 【手动 ack 】)
  2. 每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍.

Seata分布式事务

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

image

1
2
3
4
5
6
7
+ 创建UNDO_LOG表(每一个数据库)(AT模式:自动模式)
+ 回滚表
+ 自动补偿
+ 安装事务协调器:seata-server
+ 使用seata DataSourceProxy代理自己的数据源
+ 全局事务注解
+ GlobalTransactional

问题:AT模式吞吐量低,不支持高并发

1
2
3
4
+ seata默认AT模式
+ 不用做高并发场景
+ 后台简单场景,不要求高并发
+ 包含各种锁机制

高并发场景:考虑基于消息服务的柔性事务方案

1、锁库存成功数据库保存锁库存工作单

2、引入延迟队列——完成库存自动解锁

image-20220228101557537