一致性和页面重复提交导致的问题

前言

方案1

@Transation
public void xx(param p){
    a(p);
    int p1 = p/2;
    //rpc
    b(p1);
    //rpc
    c(c1);
    //redis
    d(d1;)
}

 方案2


public void xx(param p){
    甲.a(p);
    int p1 = p/2;
    //rpc
    b(p1);
    //rpc
    c(c1);
    //redis
    d(d1;)
}

//甲serive
@Transation
public void a(){

}

当前xx方法,有4步 其中a是本地事务,b,c,d是分布式

1  这代码会有啥问题

    当a执行成功,b成功.c执行失败,a回滚

    于是一般我们会采用重试的方法,a,b,c,d做好幂等,但是问题来了,谁来触发重试 重试幂等的key怎么拿

    一 我们先分析第一个问题 谁来触发重试

     当这是一个同步请求

        1      (方案1),用户点击后报错了,于是再点下发起一次重试,但是这样是不靠谱的,因为无法保证用户一定会再点一次,并且一直报错一直点,大概率结果就是b被执行,acd没有执行,于是产生了数据不一致

        2      (方案2), 我们尝试将a事务先提交再执行bcd,那么c报错后,用户仍然可能放弃了重试,于是a,b执行成功了,cd没有执行成功,于是产生了数据不一致

          所以当是一个同步请求,因为重试无法保证,不一致无法解决

      当这是一个消费消息或者基于job的重试

       1      (方案1)当消费失败后,消息会基于消息中间件来进行重试,这样各个节点做好幂等就可以了

        2      (方案2)当消费失败后,消息会基于消息中间件来进行重试,这样各个节点做好幂等就可以了

      所以如果是一个消费消息或者基于job的重试,这种方案1,2都可以,那如果是同步请求怎么弄,我们可以参考base理论

即使无法做到强一致性,但每个应用都可以根据自身业务特点采用适当的方式来使系统达到最终一致性,具体实现参考本地消息表或者消息中间件的事务消息(加上对应的补偿方案job和死信)

1 一切的前提是能够保持最终一致,少数情况需要一些冻结或者预占操作,如果无法保证,就不适用

思想是用一条持久化记录,可以持久化在mysql或者中间件,这只是方式的不同,只有持久化成功,就后续基于最终一致,并且最好异步去做,因为同步后续做可能会超时,但是持久化一条记录是很快的,如果有模型需要提交事务,就把事务和本地消息一起落库或者基于中间件事务消息,认为只要本地事务提交成功后续就一定成功,如果没有事务但是需要最终一致,虽然这个叫做一致不是很合适,但我觉得也可以用这个思想,直接持久化消息或者落库然后基于消息异步去做,其实叫持久化的一次调度更合适,只要持久化成功调度一定能执行成功

3  持久化后可以基于job或者消息重试,这也只是重试方式的不同实现,上面的则是思想的实现,2者不是同一回事,可以组合使用

本地消息表是和本地事务一起提交,mq事务消息,用mq替代了mysql来存储本地消息,会在事务提交之前插入一条pre状态,事务提交之后变成confirm状态,保证事务提交后消息也一定会推出去(包括一个确认事务提交的回调机制),具体请参考官方文档

总结:

         同步的时候借助base理论最终一致,当为消息消息或者job消息,我们可以直接借助消息的重试达到最终一致,这样就解决了重试的问题,base理论的思想就对应着方案2,只不过做了升级,对bcd进行重试,abcd都是原子节点,本来也不应该a本地事务受到其余原子节点的影响这样就违背了原子性,另外base理论中的实现方案中也需要本地事务先提交,提交成功就认为后续分布式节点最终一定成功,这里注意一定要能保证下游的一定成功,例如扣钱或者扣库存,就需要有一个预占的操作,举个例子,假设下游落库的一个字段有可能因为业务而导致超长失败,这种就不适合最终一致,因为无法保证,如果本地事务提交了却因为后续报错回滚,那么本地消息也无法保证一定能落库成功,另外一个角度就是base保证了可用性,可用性个人理解为本地事务提交成功,用户不感知报错,当然提交失败,用户不感知也是可用性的体现,从重试的角度分析,我们应该优先考虑方案2

  二  接下来分析第二个问题,幂等

             1 重试的问题解决后 开始考虑幂等键的问题,因为重试的时候 链路上的原子节点是一定要做幂等的

              2当幂等键很好选择的时候,例如就是一次提交动作,更新单据状态,这种幂等逻辑先加锁再判断单据是否提交,或者消费消息或者基于job,这种方案一和方案二的重试都能用业务逻辑或者请求Id幂等,

              3 当幂等键不好选择的时候,且接口容易超时的情况下,例如如果是一次加钱操作,这种服务端就不知道请求是上一次的请求还是用户新起了一次请求,这样会带出一个实操上的问题,就是用户实操的重复提交

                  3.1  用户看到响应超时在当前页面一直点击重试 那么会源源不断的产生新的请求,导致后端逻辑被执行多遍 这里扩散一下,常见的解决方案就是token令牌机制,请求之前客户端向服务端申请一个token或者id,然后提交的时候基于token做幂等

                  3.2 当用户每次退出页面再进来点击重试这种是无法token机制是无法解决的 这时候方案2就起到作用了 因为用户一旦看到报错,可能会退出重进或者刷新页面,这样会看到新的结果,因为事务已经提交用户会发现自己向做的已经做了,这样就不会再提交了,这样就大概率提高了这种可能性,如果用户比较老6还点击 那就没办法了

               

 总结:

         当我们是一个同步请求,服务端又无法做幂等的时候,容易出现超时,导致用户看到不可用,我们可以采用token机制和方案二结合起来看情况使用,来确保用户不管是退出页面还是在原页面重试都不会反复生成新的请求,但是这样采用token机制对服务端消耗较大,一般涉及到钱的支付,金融行业才会大规模使用,如果一般的行业 像供应链行业,saas  一般不会采用token机制,会尽量保证接口的及时响应,和方案二报错不让用户感知和让用户看到事务提交结果来尽量避免用户反复点击,这也是方案的选择,不选择小概率事件而产生的服务器资源消耗,从幂等键的选择角度来看,方案二也是优于方案一的

所以最后我们来总结分析一致性问题

第一点关注的就是谁来重试,重试的问题解决后

第二点是重试key是啥,如果好选就没事,如果不好选,同时你对客户端的接口还容易超时,这时候就要考虑用户习惯的问题了可能会因为超时反复发起请求,如果出现了就用token机制和方案二来解决,要看你的业务情况,是不是半点反复提交都不允许(涉及到钱方面),还是说可以允许

第三点就是不管同步异步,重试还是幂等导致用户多点击的问题,都是方案二是比方案一更好的,能够保证重试方案的落地,能够保证在出现重复提交的时候大概率减少用户的误操作,所以我们以后都尽量采用方案二

第四点: 出现用户重复提交就是用户看到超时以为提交失败,所以我们尽量在本地事务提交后,异步去执行后续节点,从而保证更高的接口的可用性

思想总结:

       1  前提能保证最终一致那就可以用最终一致

        2  本地先一致,后续异步最终一致,方式有不同

        3  重试方式不同与持久化方式是不同的逻辑

        4  当没有一致可言,可以借助持久化的思想来进行调度,持久化成功就认为调度成功

                

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值