java订单支付核心业务

spu和sku

spu:虚拟商品,针对某个商品进行搜索,页面展示的就是spu的信息,是一组具有共同属性的商品集,如品牌信息,其中包含多个sku
sku:具体商品,用户点击页面的spu进入的页面会详细显示对应的sku,spu商品集中的商品根据不同的特性而细分出的具体商品就是sku,如颜色、具体尺寸、价格,用户下单时使用的就是sku信息
在这里插入图片描述

购物车设计

  1. 购物车由多个购物项组成,一个购物项由同一sku的多个数量构成,购物项在后端有对应的实体类,其中会保存sku的id
    在这里插入图片描述
  2. 存放在redis中,利用hash存放,因为不需要考虑过期时间,hash的ley存放每个用户标识,field存放每个skuid,value存放具体购物项信息,包括sku个数和总价
    在这里插入图片描述

用户微服务工作流程

  1. 用户登录——用户微服务:生成token,用户信息存入redis
  2. 用户访问具体业务微服务——网关微服务:验证redis中是否存在token——业务微服务:每个业务微服务都有拦截器,在拦截器中远程调用用户微服务中根据token查询用户详情信息的方法,从redis中根据token取出用户信息并绑定到当前线程

购物车微服务工作流程

  1. 用户添加商品到购物车:
    1. 从当前线程获取用户id,根据用户id从redis中获取当前用户的购物车hash集合
    2. 判断当前商品的skuid是否在购物车hash集合中已经存在,如果已经存在则增加其购物项数量,不存在则远程调用商品微服务,利用skuid查找对应的sku信息,再以skuid为field,sku详情信息作为购物项详情信息,创建新的购物项放入当前用户对应的redis购物车记录中
  2. 用户更新购物车中已有商品:
    1. 从当前线程获取用户id,根据用户id从redis中获取当前用户的购物车hash集合
    2. 根据当前商品skuid查询当前用户对应购物车中的购物项,再根据前端传递的更新信息,如选中状态、数量更新购物项信息,再写回redis中保存
  3. 用户删除购物车商品
    1. 从当前线程获取用户id,根据用户id从redis中获取当前用户的购物车hash集合
    2. 根据当前商品skuid删除用户购物车中对应的购物项,再写回redis中保存
  4. 用户下单成功后删除购物车商品
    1. 从当前线程获取用户id,根据用户id从redis中获取当前用户的购物车hash集合
    2. 循环遍历用户所有下单成功的skuid,根据skuid在购物车中删除对应的购物项并写回redis保存

订单微服务工作流程

整体流程:

  1. 用户直接在商品详情页点击立即购买、或用户在购物车中勾选商品点击立即购买
  2. 进入订单确认页面,在确认订单页面选择收货地址、付款方式等信息,同时根据sku详情页或购物车中选择的购物项生成订单项对象
  3. 在订单确认页面点击提交订单按钮,将对应商品的库存数量进行上锁,前端页面为用户弹出支付页面
  4. 用户若成功支付,则将商品库存上锁的数量在数据库中进行时间减少,将订单项信息写入订单项表、订单信息写入订单主表、订单物流信息写入订单物流表;若用户超时未支付,将库存数量信息进行解锁

订单表设计

分为订单主表和订单项表,每个订单项都会记录与其对应的唯一的主订单编号,订单主表和项表之间是一对多关系

  1. 订单主表:存放当前下单的用户id、订单总金额、下单时间、下单方式、订单状态(已付款、未付款。。。)、若属于未付款订单还会有超时时间
  2. 订单项表:存放用户在点击下单按钮时,购物车页面中勾选的每一个具体购物项,主要就是sku的基本信息
    在这里插入图片描述
  3. 订单物流表:每个订单主表记录生成后都还会生成与其对应的订单物流表记录,通过记录订单主表id与订单主表关联,其中还会记录该订单的收货地址、收货人姓名、电话等信息

订单确认

在这里插入图片描述

订单确认页面用于在用户实际提交订单前,为用户展示所有即将下单的商品信息及用户的地址信息,方便用户进行下单前的确认的地址选择,还有当前订单唯一标识,用于防止当前订单被重复提交。

进入订单确认页面有两种方式:

  1. 用户直接在sku详情页面点击立即购买
  2. 在购物车中勾选购物项,点击立即购买

两种方式在进入到订单确认页面时,都需要访问订单微服务中的确认订单接口,在接口中获取当前订单确认页面中需要显示的所有即将下单的订单项信息、用户的所有地址,并生成订单唯一标识

  1. 订单项信息:如果用户是在sku页面直接下单,则前端在调用订单确认接口时会传递当前skuid,然后根据skuid远程调用商品微服务查询商品详情信息,根据订单详情信息生成订单项信息,如果是在购物车页面下单,则不会传入skuid参数,当订单确认接口没有接收到skuid参数时,就远程调用购物车微服务,根据用户id查询所有用户已选择的购物车项,再根据购物车项生成订单项信息并显示到订单确认页面中;
  2. 用户的地址信息:通过远程调用用户微服务的方法,传入当前用户的id来获取;
  3. 订单唯一标识:通过雪花算法生成,生成后直接将标识写入redis中,防止用户进行同一订单重复提交
确认订单流程图

在这里插入图片描述

以上三项信息生成后会返回给前端,前端根据这些信息展示订单确认页面

订单提交

在这里插入图片描述

用户在订单确认页面点击提交订单按钮时,访问订单提交接口,前端需要将下列信息传递给订单提交接口:

  1. 生成订单确认页面时生成的订单唯一标识,用于防止重复提交订单
  2. 用户在前端页面所选择的收货地址信息
  3. 所有订单项
  4. 前端所计算的订单总价,防止用户在订单确认页面停留时,后端数据库的价格已经修改的情况,后端需要再根据数据库的价格计算最新的价格与前端计算的价格对比,若不一致再提醒前端用户价格已经改变
订单提交流程

在这里插入图片描述

红色部分代表分布式事务问题,绿色部分代表出现多线程和分布式锁问题,具体流程如下:

  1. 订单重复提交校验:校验当前订单的唯一标识,防止重复提交,校验的步骤如下
    1. 查询redis中是否已经有该订单的唯一标识
    2. 如果存在就将其从redis删除,删除成功后redis会返回1
    3. 如果不存在返回0
    4. 判断redis返回的结果,若为1则继续执行后续提交订单代码,否则拦截,返回当前订单已提交信息给用户

因为第一第二两步都针对redis进行了操作,所以很有可能会导致出现线程安全问题,比如同一个订单有两个线程在提交,第一个线程读取到redis中的标记还没执行第二步的删除操作前,另一个线程也读取到了redis的标记,这样就会导致其中一个线程删除成功,另一个线程虽然读取到了标记但却删除失败,导致程序出现异常,所以这里可以使用lua脚本来操作redis,保证两步操作的原子性,只可能同时成功或失败:
在这种防止重复提交的模式下可能会出现的问题:

  1. redis标记删除成功,但后续订单提交代码出现异常,导致订单没有成功提交,这种问题发生时让用户再重新下单一次,重新生成订单确认页面信息就好
  2. redis没有连接上,导致redis中其实有标记但是用户下单失败,这种情况让用户在订单确认页面再提交一次就好,因为redis中的标记并没有被消费,不会出现重复提交问题
  1. 订单前后端验价:获取当前订单的所有订单项,远程调用商品微服务的根据skuid查询价格的接口到数据库查询对应的价格,根据每个订单项的skuid获取所有的价格,将订单项中商品数量*sku价格获取订单项总价,再将所有订单项累加得到根据数据库中价格计算出的用户当前订单的总价,将这个总价和前端传来的显示给用户的订单总价对比,若不相同则说明后端数据库的价格已经修改,和用户购买时前端页面显示的价格不一致,提示用户刷新页面后重新下单;若一致则继续执行后续订单提交代码
  2. 校验库存是否足够并锁库存:将每个订单项对象转换成sku锁库存对象,其中记录skuid、需要锁定的数量、订单的唯一标识以及当前订单项是否上锁成功的标记,远程调用商品微服务方法进行锁库存,传入所有sku锁库存对象,商品微服务中库存表的设计和锁库存步骤如下:

库存表字段设计:商品微服务所连接的商品库存表中会有两个与库存信息有关的字段,一个stock字段记录真实的剩余库存数量,一个locked_stock字段记录当前商品被锁定的库存数量。
锁库存步骤:尝试利用sku锁库存对象集合中所有对象去库存表锁库存——>若有锁定失败的sku锁库存对象则返回下单失败信息——>若所有sku锁库存对象对象都锁定成功,将锁定成功的sku锁库存对象集合存入redis中

  1. 将sku锁库存对象集合进行foreach遍历,针对每个sku锁库存对象都会用lambda表达式生成一条更新库存表的语句,根据当前sku锁库存对象获取skuid,根据skuid查询对应库存表中记录的sku的locked_stock和stock,判断stock-locked_stock的结果是否大于等于当前订单想要锁定的数量,若大于就将locked_stock的值加上当前订单想要锁定的数量,并设置当前sku锁库存对象的锁定标记为成功;否则标记库存锁定失败,这里我们将查询locked_stock和stock、修改locked_stock的操作直接整合成了一条sql发送给数据库执行,使用的是基于条件判断的乐观锁,所以不会出现分布式锁的问题。
  2. 将sku锁库存对象集合中上锁标记为失败的sku锁库存对象过滤出来,如果过滤结果不为空,说明有的sku锁库存对象锁定失败,这时就需要进行回滚,将之前成功上锁的sku库存数量进行解锁,具体实现就将sku锁库存对象集合中上锁标记为成功的sku锁库存对象过滤出来进行遍历,根据每个sku锁库存对象中保存的skuid去库存表中,将locked_stock的值再减去之前加上的值,数据回滚完成后返回锁库存失败标记给订单微服务,提示对应的sku商品库存不足
  3. 若过滤结果为空则说明当前所有sku锁库存对象都锁定库存成功,将sku锁库存对象集合以订单唯一标识为key再保存一份到redis中,之后就从redis获取订单对应的sku锁库存对象集合信息,若redis数据丢失就从数据库读取,并返回锁库存成功标记给订单微服务
  1. 判断商品微服务中的锁订单是否成功,若不成功就提示用户库存不足信息,若成功继续执行后续提交订单代码,创建订单主表信息和订单项表信息存入数据库,其中订单主表信息目前是未支付状态,会生成一个超时时间,超过这个世界若还没有支付则会取消订单,再调用购物车微服务从购物车中删除用户选择的商品,最后返回订单编号

分布式锁的产生

分布式锁的问题有两个不同的原因:

  1. mysql层面:由于每个线程都出现了多条操作数据库数据的sql语句

在部署的多个服务器实例的多个线程中,出现了针对同一数据库中同一条记录的多条查询、修改语句,在一个线程执行这多条语句过程中,其他线程也可能会去mysql中执行这些语句,所以要解决这个问题,就需要保证这多条语句在被一个线程执行过程中不能被其他线程同时执行

  1. 分布式部署层面:由于项目做了集群部署,每个运行的实例中无法共享锁对象

在我们的项目中,只部署一个服务器的情况下,在用户下单减库存时,数据中减库存的步骤可以分为两步:先查询商品库存,再判断当前库存是否大于等于用户下单数量,若大于则减去库存下单成功,若小于则下单失败,这几步操作并不是原子性的,很有可能出现多线程的问题,就是第一个用户在查询到库存数量,还没来得及减去数据库中库存时,另一个用户线程抢夺到cpu执行权,此时另一个用户线程也判断当前库存足够,但其实数据库中剩余的库存很可能只能满足一个用户的购买需求,这就会导致超卖的出现,在单服务器情况下,只需要使用sync或lock锁就能解决,针对数据库库存这个共享资源进行加锁,保证只能有一个线程来操作。
但在分布式集群部署下,就算针对上面的情况使用sync或lock锁,也不能解决问题,因为每个服务器中的共享资源是相互独立的,每个服务器内部同一时间只能有一个用户线程操作共享资源,但所有服务器在同一时间很可能都会有一个线程在操作共享资源,导致分布式锁问题的出现。分布式锁问题的根源就是由于每个服务器内都有一个共享资源,这个共享资源并不能锁住所有服务器实例

只要能解决这两个原因中任何一个,都可以解决分布式锁问题,所以有以下解决方案

解决方法一:zookeeper利用同名临时节点做锁对象 (分部署下部署共享锁资源)
每个线程若想针对共享资源进行操作,都必须去zk中一个持久化节点下创建一个同名临时节点,因为zk的节点名不能重复,所以同一时间只可能有一个服务器的一个线程创建成功临时节点,只有创建临时节点成功的线程才能操作分布式共享资源。当一个线程创建成功临时节点后,其他所有想要操作共享资源的线程都将被阻塞,并会与zk建立一个tcp长链接监听这个临时节点的情况,当共享资源被成功创建临时节点的线程操作完毕,该线程进行解锁,临时节点就会被清除,所有阻塞的线程又会尝试去创建该临时节点。zk的好处是可以避免分布式的死锁问题,也就是如果创建临时节点的线程在执行操作共享资源的代码过程中宕机,就会断开与zk的tcp长连接,zk的临时节点就会自动被清除,避免死锁

解决方法二:redis (分部署下部署共享锁资源)
原理:set设置值的时候若key存在就会覆盖原来的值,而setnx设置值的时候若key存在会创建失败。

所有针对分布式共享资源进行操作的线程都去redis中去创建同key的记录,若创建成功则操作业务代码,若不成功进入阻塞。

问题:容易出现死锁
1. 设置key的过期时间,但时间的设置很难控制,时间短了会超卖,长了影响整体业务流程效率
2. redisson看门狗机制,底层使用setnx创建key,会有一个默认的失效时间,同时看门狗会针对创建key的线程进行监听,每隔十秒看一下加锁的线程是否还在正常运行,若还在运行就将失效时间进行延长,若发现故障则会删除key释放锁资源。这种方法会比较消耗cpu,因为会不断进行失效时间的延长。使用时引入pom自动装配依赖,直接注入redisson工具类,调用lock或者unlock方法进行上锁解锁

解决方法三:mysql的悲观锁 (保证操作共享资源的多条sql语句只能被一个线程执行)
开启数据库事务情况下,在查询语句后跟上for update开启悲观锁,只会有一个事务能抢夺到这条查询语句的锁资源,当事务执行完成被提交后释放锁资源。其他没有获得锁资源的事务会进入阻塞状态

解决方法四:mysql基于版本控制的乐观锁 (保证操作共享资源的多条sql语句只能被一个线程执行)
在数据库中设置一个字段version用于版本控制,先根据id查询想要修改的记录的版本号,然后在执行修改的update语句的set中跟上version=查询到的版本号+1,where中跟上一个version=查询到的版本,就能实现乐观锁的控制。

实现原理:mysql执行sql语句是一条一条进行的,无论有多少服务器或者线程,所以在执行乐观锁的update这一条语句时就不会出现多线程安全问题,假设一个线程查询到version后,还未执行乐观锁update之前,这时其他线程也执行了查询version的语句,并且成功使用update进行了修改,这时前一个线程在执行update的时候就会发现自己之前查询出的version和数据库中最新的version已经不一致,所以前一个线程的update并不能成功执行,这时候前一个线程又会重复执行之前查询version并执行update的语句,直到执行update时的version与数据库中的实际version相等才能成功修改

具体实现方式:mybatis-puls中针对乐观锁提供了插件支持,我们只在对应需要用到乐观锁的表中设置一个version字段,引入插件,在version字段上使用@Version注解,就能实现用乐观锁控制mysql的数据修改

解决方法五:mysql基于条件的乐观锁 (将多条sql整合成一条sql)
如果我们能直接将原本需要多条语句才能执行完成的共享资源操作sql整合成一条,自然就不会出现分布式锁问题,因为无论部署了多少服务器实例,在sql层面同时只能执行一个线程的一条sql操作代码

解决方法六: zookeeper利用临时有序节点做锁资源 (分部署下部署共享锁资源)
之前第一种方案时使用临时节点做锁对象其实不够完美,因为当并发特别高时会出现羊群效应,一个临时节点会被很多线程进行监听,当临时节点被删除所有监听的线程都会去请求zk创建新的临时节点,导致zk突然收到很多请求,降低zk的性能
在这里插入图片描述
zk中一共有四种类型节点:持久、持久有序、临时、临时有序,在持久化和临时节点中不能创建同名节点,而有序中可以同名,底层会自动生成一个递增的序号。

为了避免羊群效应,我们利用临时持久节点做锁资源,当需要针对某个方法进行加锁时,根据方法名去zk中创建持久节点,当有线程需要上锁时,就去对应方法的持久节点下注册一个临时有序节点。我们约定只有当前持久节点下序号最小的临时节点才能获取到锁资源执行业务代码,在执行完成后自动删除自己所创建的临时有序节点。zk的临时节点之间可以实现相互的监听,所以可以让每个临时有序节点监听它前面是否有比他序号大的临时有序节点,如果没有则说明当前节点已经是序号最小的节点,获取锁资源执行业务代码
在这里插入图片描述
利用zk和redis实现锁资源的对比:

zk:zk本身是基于cp实现的注册中心,能保证数据的强一致性,是通过监听的方式判断是否获取锁资源,线程的开销比较小,但缺点是若上锁解锁频繁,会导致zk不停新建和删除临时有序节点,对zk的性能需要有很高的要求

redis:redis本身不是基于cp实现的,所以不能保证数据强一致性,是通过不断尝试setnx创建key获取锁资源的,线程的这个自旋过程的开销比较大,但优点是redis做锁对象时的并发相比zk会高很多

进行技术选择时,要具体看当前项目的实际情况,如果项目是用nacos做的注册中心,redis做缓存,则选择redis做锁资源比较好,单独为了使用锁而配置zk的话成本比较高;如果项目用的zk做注册中心就可以使用zk

分布式事务

根源:同一sevice方法执行中通过rpc中操作不同微服务连接的不同数据库而产生,因为这些不同数据库的事务是相互独立的
在这里插入图片描述

在订单提交步骤中,会出现远程调用商品微服务的锁库存方法,操作库存表中的locked_stock,若商品微服务减库存成功返回订单微服务提交订单,但若订单微服务后续代码出现问题,就只能回滚订单微服务中之前执行的代码,而商品微服务中锁库存的代码已经执行并且提交,就会导致分布式事务问题的出现

分布式事务就是要保证在远程调用过程中,多个服务器中资源的acid

分布式事务的cap理论

  1. 分区容忍性:不可避免,在分布式部署下,不同节点之间肯定会因为网络波动原因导致网络分区的出现,网络分区出现就会导致两个不同分区的节点之间信息不能交换
  2. 强一致性:任何时候访问任意两个不同节点的同一数据得到的结果都相同
  3. 高可用性:任何时候都能访问集群中的健康节点并得到响应的结果,而不是超时或拒绝访问
  1. 基于AP实现分布式事务:各个子事务分别提交,允许出现结果结果不一致,再采用针对提交的事务进行回滚,实现最终一致性
  2. 基于CP实现分布式事务:各个子事务执行后相互等待,同时提交,若出现错误,同时回滚,达成强一致性

两种方式在实现时都需要一个事务协调者进行事务间的通信

在这里插入图片描述

分布式事务实现协议

  1. 两阶段提交协议(2PC);基于XA规范实现,需要数据库的实现支持

在这里插入图片描述
一阶段:事务协调者通知每个事务参与者执行本地事务,本地事务执行完成后报告事务执行者执行的结果,但不进行事务提交,等待其他子事务的执行结果,等待期间继续持有数据库锁

二阶段:事务协调者收到所有子事务的响应后判断下一步操作,若一阶段都成功,则通知所有事务参与者提交事务;否则通知所有事务参与者回滚事务

  1. 三阶段提交协议(3PC):基于强一致性,遵守事务的四大原则acid
    在这里插入图片描述
    相比2PC,3PC还多了一个准备阶段canCommit,
  1. canCommit阶段协调者询问每个参与者当前是否能进行事务处理并提交事务,若有参与者响应no或响应超时则回滚
  2. preCommit阶段协调者通知每个参与者执行事务,执行成功后向事务协调者报告执行情况,若有参与者响应no或响应超时则回滚
  3. doCommit阶段协调者通知每个参与者提交事务,执行成功后向事务协调者报告执行情况,若有参与者响应no或响应超时则回滚

基于mq实现最终一致性

这里以支付微服务中用户支付成功——>订单微服务修改订单状态——>商品微服务修改库存状态这个过程中前面的过程举例使用mq实现最终一致性
在这里插入图片描述
应用场景:用户付款成功,支付微服务需要调用订单微服务修改订单状态为已支付

数据库表设计:

  1. 支付微服务需要操作数据库中的支付日志表和消息表,支付日志表存放用户支付信息,消息表存放需要通过mq发送给订单微服务的消息
  2. 订单微服务需要操作数据库中的订单主表和消息表,修改订单主表信息,消息表中存放需要通过mq发送给支付微服务的消息

修改流程:

  1. 用户支付成功后,向支付日志表写入支付信息,主要包括用户的支付方式、支付平台返回的支付订单号,后续涉及退款时需要使用到,并且将修改订单状态的消息写入到消息表中(这里针对支付日志表的写入支付信息和消息表的消息添加可以保证原子性,因为处在同一数据库中),支付微服务会设置有一个定时器,定时从消息表中读取所有消息记录发送给mq。
  2. 订单微服务有一个监听器接收mq中支付微服务发送的修改订单状态的信息,然后去订单微服务的消息表中查看当前的消息是否存在
    1. 如果存在直接签收mq的消息,并且通过mq给支付微服务发送消息,表名当前消息已经消费成功
    2. 如果不存在就修改订单主表中订单状态为已支付,并且把消息写入到订单微服务的消息表中,并且通过mq给支付微服务发送消息(这里针对订单主表的状态修改和消息表的消息添加可以保证原子性,因为处在同一数据库中)
  3. 支付微服务在收到订单微服务发送的消息后去消息表中删除对应的消息信息,该消息就不会再通过定时器向订单微服务发送

流程中可能出现问题的部分:

  1. 支付微服务的消息发送到mq失败:定时器会一直读取再发送,最终会发送成功
  2. mq向订单微服务重复发送同一消息:订单微服务中会先去消息表中查询当前消息是否已经存在
  3. 订单微服务执行修改订单状态和写入消息表记录时失败:同一数据库内,可以直接回滚,之后再从mq中读取消息进行消费
  4. 订单微服务向mq发送消息失败:支付微服务会继续发送已经消费成功的消息,但是会在消息表中查询到数据后被拦截,再次发送消息给mq
  5. mq重复向支付微服务发送消息:支付微服务中先判断消息是否存在,若不存在就说明已经删过,直接签收消息

特点:基于mq解决分布式事务时。只要支付微服务本地事务成功提交。就只允许全局事务的成功。不可能出现失败

数据不一致情景:当支付微服务事务提交成功。但订单微服务事务第一次提交失败,直到订单微服务提交事务成功,才能实现一致性

分布式事务和分布式锁区别

  1. 分布式锁是由于集群部署环境中出现了多个线程操作同一共享资源的多条sql语句,在单机部署下可以使用sync加锁解决,但集群部署下使用sync还是不能解决,因为服务器实例会做集群部署,每个服务器都能同时执行sync代码。比如在订单提交接口中需要对商品库存信息进行修改,这里修改的步骤会分为查询、判断、修改,导致分布式锁问题出现。解决思路两种:要么将多条sql合成一条sql、要么为集群服务器设置同一共享资源锁
  2. 分布式事务是由于在同一sevice业务操作逻辑中需要针对多个微服务连接的不同数据库进行操作,不同数据库的事务是相互独立的,这就会导致一个service逻辑中出现多个事务,出现分布式事务问题。比如订单支付成功后,需要将支付信息写入支付微服务中的支付信息表中,同时还要修改订单微服务中订单主表的订单状态为已支付。

seata

TC:事务协调者,统管
TM:事务管理者
RM:资源管理器,每个微服务管理各个子事务的提交

XA模式(CP思想)

  1. 一阶段中各子事务只执行sql,执行完成后不提交,等待所有子事务都向事务协调者报告执行完毕的状态;
  2. 二阶段中事务协调者统一通知所有事务参与者同时提交事务,若有失败立即回滚

XA模式的一致性比AT高,但是执行效率没有AT高,因为有部分子事务先执行成功后需要进入等待状态,事务被挂起,此时还会占有对应的资源,必须等所有子事务都准备完毕才能提交事务并释放资源,所以执行效率比AT低,但所有事务是同时提交的,所以可以实现强一致性

AT模式 (AP思想)

  1. 一阶段:各个事务参与者执行业务代码并提交子事务给对应数据库,同时还会生成一个回滚日志并且一同提交给数据库,提交后释放连接资源
  2. 二阶段:若所有子事务全部提交成功,整个事务结束;若有子事务提交失败,调用每个提交成功的子事务生成的回滚日志进行反向补偿

AT模式下的执行效率比XA高,但一致性不如XA模式高,因为在一阶段中各个事务参与者执行完事务后会直接提交,不会等待其他子事务,提交后就会直接释放连接资源,所以不会被挂起,效率比XA高;但如果有子事务提交失败,在进行反向补偿之前就会出现短时间的数据不一致,所以一致性不如XA高

Seata的AT原理

下面以订单微服务中提交订单接口远程调用商品微服务进行锁库存,然后订单微服务再记录订单信息的分布式事务场景举例

在这里插入图片描述

  1. 订单微服务中的RM请求TM开启全局事务,TM请求TC,TC返回TM全局事务id,TM再将id返回给当前订单微服务的RM
  2. 订单微服务RM根据事务id去TC注册修改订单状态子事务
  3. 订单微服务开启本地事务,执行当前订单微服务的sql,修改订单表中的订单状态,同时将执行的sql记录到undo log表中,订单微服务提交事务
  4. 订单微服务向TC报告事务提交完毕
  5. 订单微服务通过rpc框架调用商品微服务,调用时会携带全局事务id
  6. 商品微服务根据全局事务id去TC注册商品微服务中修改库存信息的事务
  7. 商品微服务开启本地事务,执行锁库存操作,同时将执行的sql记录到undo log表中,商品微服务提交事务
  8. 商品微服务向TC报告事务执行完毕,如果TC发现某商品微服务报告执行失败,触发反向补偿机制,将之前已经提交了的订单微服务中的事务进行回滚,回滚方式就是将undo log表中记录的sql进行反向执行
  9. 删除各个事务参与者本地的undo log表中的sql

数据不一致情景:AT模式下的seata并不是严格意义上的CP,当商品微服务修改库存事务提交成功后,若订单微服务写入订单信息事务提交失败,在TC进行全局反向补偿前。就会出现数据不一致问题

seata启动方式

开发环境用单机,全局事务、分支事务信息存在本地文件中;生产环境用集群,设置将全局事务、分支事务信息存在同一个数据库或者redis中

seata的配置

需要配置mapping映射,将一个全局事务的所有参与者映射到同一个全局事务分组中,集群中的seata会配置到注册中心注册,各个服务可以在注册中心获取seata集群的信息

seata的使用

在需要开启全局事务的方法上使用@GlobalTransactional

seata使用中的问题

AT模式下的undo log日志表在一个全局事务执行成功后应该被全部删除,无论是否需要进行反向补偿。若事务完成但undo log中还有数据,可能的原因:

  1. seata配置出错
  2. 反向补偿失败,导致反向补偿机制没有删除undo log文件,我们设置了一个专门的定时器,定时读取补偿失败的日志,进行手动补偿再删除

订单取消

用户提交订单后若超时未支付,需要自动取消订单,释放锁定的库存

定时任务

  1. spring task:功能强大,使用简单,但不支持精确到年,使用时在入口类开启定时器,再编写对应的定时器类交给spring容器管理,定时器类中的定时方法用@Scheduled注解写cron表达式,运行时就能自动在cron表达式的时间调用

超时订单取消的实现:

  1. 订单微服务中编写清理支付超时的定时器方法,每分钟执行一次,其中利用mybatisplus的lambdaQueryWrapper生成一条查询语句,查询订单主表中所有最后支付时间小于当前时间的订单项,再循环获取将每个超时订单的订单编号,以订单编号作为参数调用关闭订单方法
  2. 关闭订单方法中根据订单编号去订单主表中查询订单,若没有查询到,或者查询到订单状态不是待支付状态,则直接返回;若查询到订单状态是待支付,就将其修改为自动取消状态写入订单表中,再以查询到的订单的编号作为参数远程调用商品微服务的释放库存的方法 (分布式job问题,使用Redisson)
  3. 释放库存的方法中会先根据订单编号信息去redis中查询之前在订单提交接口中锁库存成功后放入redis的所有sku锁库存对象,然后循环遍历所有sku锁库存对象,再利用lambdaUpdateWrapper生成一条更新语句,根据每个sku锁库存对象的skuid查询商品库存,再将商品的锁库存减去之前加上的上锁的库存,最后将redis中记录的该订单的sku锁库存对象集合进行删除
  1. quartz:使用较复杂,支持精确到年

分布式job问题

产生原因:同分布式锁,只不过不是因为用户的调用产生的,而是因为集群中定时器任务产生的

每个微服务实例中都有相同的定时任务,若部署多台实例,就会出现多个定时器同时执行统一定时任务
解决方案一:上分布式锁,基于mysql层面或共享锁层面解决,同一时间只能有一个定时任务线程执行sql或所有定时任务线程共享锁资源

解决方案二:上分布式job框架xxjob
在整个项目中部署一个xxjob协调者,将执行定时任务的具体微服务实例注册到协调者中,在执行器微服务中的定时任务方法上使用xxjob注解后,协调者会定时将任务在触发时通过路由策略交给一个协调者进行执行,避免分布式job问题,若执行失败会有补偿机制

xxjob的部署及使用:

  1. 导入数据库脚本
  2. 配置调度中心项目,配置连接自己的数据库
  3. 启动调度中心项目,访问配置的端口号就能访问对应的页面
  4. 执行定时任务的微服务中引入配置的pom坐标,将微服务作为定时任务执行器注册到调度器中
  5. 访问调度中心管理页面,配置对应的定时任务,选择路由策略后启动,完成配置

xxjob集群:

  1. 配置集群连接同一数据库,若数据库实现了读写分离要保证连接的是主表
  2. 利用nginx做反向代理,将xxjob集群到nginx中,可以实现xxjob的负载均衡

支付微服务

在我们项目中,订单提交时就将库存表中的库存信息进行上锁,好处是可以提升用户体验,只要用户成功下单,就一定能支付成功;

如果是用户在付款时才修改库存表中信息,好处是可以避免黄牛,黄牛经常会利用提交订单会上库存数量锁的特点,抢到商品后不付款而去直接转卖,转卖成功后才付款

用户支付成功后需要真正减去库存,并清除锁库存的数据

第三方聚合支付平台问题

  1. 会有资金安全问题
  2. 产生手续费

常见加密算法

  1. 对称加密:加密解密同样的密钥
  2. 非对称加密:加密解密不同密钥,RSA2

RSA2非对称加密

加密和解密使用不同密钥,密钥分为公钥和私钥,通常使用私钥加密公钥解密,商户需要生成一套商户公钥和私钥,公钥交给支付宝接口解密,私钥用于支付微服务加密,同样支付宝也有公钥和私钥

http和https区别

  1. http默认端口80,没有加密
  2. https默认端口443,加密传输,更加安全,加密原理:建立连接时使用非对称加密,连接建立后数据传输时使用对称加密,而对称加密使用的对称密钥是每次建立非对称连接时所随机生成的,服务器端和客户端双方都会参与到对称密钥的生成过程中,保证安全性,双反就会利用这个共同生成的对称密钥进行加密和解密

https部署

运维负责部署,大概流程:

  1. 申请ca证书
  2. 下载tomcat的证书文件
  3. 在项目的yml中配置https参数加载证书即可

支付宝支付

  1. 客户端点击支付订单按钮,向支付微服务发起支付请求
  2. 支付微服务根据订单编号先远程调用订单微服务查询订单信息(有分布式锁问题),看订单状态是否是待支付,再查询当前订单编号是否已经在支付信息表中存在,若订单是待支付状态且支付信息表中没有查到结果,才执行后续业务
  3. 根据订单信息生成支付信息表记录,支付信息记录订单编号、订单总价,同时设置支付状态为待支付
  4. 支付微服务构建本次请求的参数,主要包括本次订单的编号、金额、同步通知和异步通知的接口调用地址,然后调用alipay接口传入参数(订单编号和支付总价在传递时需要利用RSA2加密)
  5. 支付宝使用公钥解密出订单编号和支付总价,如果用户是在app端发起的支付请求会生成一个加密签名后的支付宝app唤醒信息,如果是网页端的请求会生成一个跳转地址信息,其中都会携带加密后的订单的信息,这些信息只有支付宝能解密,支付宝会将跳转地址使用支付宝私钥加密后传给支付微服务
  6. 支付微服务使用支付宝公钥解密支付地址,最后返回给前端页面
  7. 前端使用支付地址跳转到支付宝,用户在支付宝进行支付,支付成功后由支付宝通知支付微服务支付结果,有下面几种调用方式
    1. 支付宝可以调用支付微服务中的get接口,同步方式通知支付微服务支付结果,支付微服务可以根据同步信息给用户展示支付结果,但实际的支付结果会以post的异步通知为准,因为get不安全,同时只会通知一次
    2. 支付宝根据支付宝中的私钥把用户支付结果加密后调用支付微服务中的post异步通知接口,以异步方式传递给支付微服务用户支付结果,包括支付宝所生成的本次交易订单号,这个订单号需要在支付微服务中获取到后存到订单主表中
  8. 支付微服务中将支付结果利用支付宝公钥解密验签,得到真实的支付结果,其中包含订单编号、订单支付的金额、支付宝生成的交易流水号
  9. 异步通知接口的工作流程 (分布式锁、分布式事务)
    1. 支付微服务根据得到的订单编号查询支付信息表,在支付信息表中查询当前订单的状态是否是待支付(有分布式锁问题),若是已支付说明支付宝之前已经通知过支付微服务,直接返回给支付宝success信息;若是未支付再查询支付信息表中记录的订单价格,将查询的总价和支付宝返回的订单的已支付金额对比是否一致,若一致再获取支付宝返回的流水号,将流水号记录到支付信息表中,同时修改支付状态为已支付
    2. 调用商品微服务,传入订单编号,扣除实际库存:根据订单编号查询redis得到订单对应的所有sku锁库存对象集合,循环遍历所有对象,利用lambdaUpdateWrapper生成一条查询+修改语句,修改当前sku锁库存对象的库存,实际库存—购买数量,锁库存数量+购买数量,再删除redis中存放的sku锁库存对象集合
    3. 调用订单微服务,传入订单编号,修改订单主表中对应订单状态为已支付:利用lambdaUpdateWrapper生成一条查询+修改语句,根据当前订单编号查询订单,并设置其订单状态为已支付,记录当前时间为订单支付时间
    4. 完成上面的处理后会返给支付宝success,如果支付宝调用支付微服务post返回支付结果给商家后,商家没有返回success给支付宝,支付宝会进行不断尝试,最多尝试8次,直到商户处理成功返回success

支付微服务支付全流程图
绿色代表出现分布式锁问题部分,红色代表出现分布式事务问题部分
请添加图片描述在这里插入图片描述

核心业务订单支付全流程图(红色代表分布式事务问题,绿色代表分布式事务锁问题请添加图片描述

核心业务中不同场景下的分布式锁

(下面场景中字体加粗部分代表会出现会分布式锁问题的代码)

  1. 订单提交接口中订单重复提交校验:在redis中先查询订单,再判断查询结果是否存在,若存在则删除

解决方法:利用lua脚本,向redis发送一条脚本直接完成查询、判断、删除操作

  1. 订单提交接口中远程调用库存微服务锁库存:在库存表中先查询商品的信息,判断商品库存是否支持本次下单,支持再修改锁库存的数量

解决方法:利用mysql中基于条件判断的乐观锁,直接将多条sql利用mybatis-plus的lambdaUpdateWrapper整合成一条修改语句,发送到mysql进行执行

  1. 支付微服务中用户支付成功后,支付宝会调用支付微服务的post接口返回支付结果,支付微服务会根据支付结果中支付宝生成的订单编号写入到支付表中记录,写入的流程是先查询支付编号、判断查询结果是否存在、不存在再执行异步通知接口中后续业务,但实际上支付宝可能会多次调用该post接口返回同一订单支付结果,导致项目中出现多个线程在执行上述的查询判断,若不加分布式锁,可能会出现严重后果

解决方法:这里因为针对数据的操作是插入,不能使用lambdaUpdateWrapper使用基于条件判断的mysql乐观锁解决,可以使用redisson,以订单编号作为redisson锁的key

  1. 用户点击支付按钮向支付微服务发起支付请求,支付微服务首先根据订单编号查询订单状态,判断状态是否为待支付,是待支付才向支付宝发起支付请求,用户可能会由于网络延迟多次点击支付按钮发起请求,项目中出现多个同一用户的同一订单支付请求,若不处理就会多次向支付发送同一订单的支付请求

解决方法:使用redission做共享锁资源,以当前订单编号作为key调用redisson上锁,只有成功以订单编号为key创建redis数据的线程才能获取锁资源执行共享代码

核心业务中不同场景下分布式事务

  1. 订单微服务中用户提交订单,先调用商品微服务操作商品表中商品的锁库存信息,锁库存成功后订单微服务生成订单项信息和订单主表信息写入订单数据库中
  2. 订单微服务中有定时任务,定时检查超时未支付的订单并将订单主表中订单的状态修改为超时自动取消,之后会远程调用商品微服务解锁库存的方法,将商品微服务的库存表中的商品库存进行解锁
  3. 用户支付成功,支付宝调用支付微服务异步通知接口,其中先记录订单编号、流水号、支付状态到支付信息表,再调用商品微服务实际扣库存,再调用订单微服务修改订单状态为已支付
  4. 订单微服务中有定时任务,定期清理超时未支付订单,定时任务中需要先将订单主表中该订单状态修改为超时自动取消,同时还需要调用商品微服务,将这些商品的库存数量进行解锁

解决方法:

  1. 基于ap思想的方式解决分布式事务,利用mq解决问题
  2. 基于cp思想实现的seata来实现,可以利用seata的at模式控制此处的分布式事务问题

项目可能的问题

  1. 提交订单接口中会调用商品微服务的锁库存操作,当有锁库存失败时会进行手动数据回滚,若回滚失败了怎么办?
  2. 用户在确认订单页面生成了订单编号,并写入了redis,用户如果退出这个界面怎么办?
  3. 为什么不用把订单编号使用setnx存在redis中避免订单重复提交?

订单很多时redis压力会很大

  1. 解决分布式锁问题时为什么不统一使用一种方案?

我们的原则是尽量使用基于mysql的乐观锁解决分布式锁问题,使用这种方法的前提是能够用lambdaUpdateWrapper将查询、判断、修改的sql进行整合,但有时候在查询、判断后做的是插入操作,就不能利用这种方案,或者查询、判断后做的不是简单的修改而是一套业务流程,就必须使用redisson或zk做共享锁资源


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部