业务场景#某公司有一个借贷项目,具体业务类似于阿里的蚂蚁借贷,用户在平台借钱,然后规定一个到期时间,在这个时间内用户需要偿还贷款并收取一定的手续费。如果未在规定时间内偿还贷款,将产生滞纳金。用户发起贷款时,会生成贷款订单,用户可以通过支付宝还款,也可以在系统绑定银行卡,到期自动扣款。还款过程是通过支付系统进行的,所以用户的还款是否逾期、逾期天数、逾期费用都是通过系统计算出来的。但是我们在做订单系统的时候,遇到这样一个业务场景,由于业务原因,允许用户通过支付宝线下还款,即我们提供一个公司官方的支付宝二维码,用户扫描二维码代码还款,然后财务不正常拉取支付宝账户下的还款清单,生成规范的Excel表格进入支付系统。支付系统根据这些支付信息生成相应的支付单,放入仓库。同时为每条还款记录生成一条消息信息发送给消息系统。消息的消费者是订单系统。订单系统收到消息后,对当前用户的金额进行结算:先偿还本金,再支付滞纳金。如果全部还清,订单将被结算,可贷金额将增加。整个流程大致如下:从上面的流程描述我们可以知道,原来线上的支付现在转到线下,这会造成一个问题:支付结算不及时。比如用户今天19-05-27下单,但是用户在19-05-26付款,财务会在19-05-27从支付宝拉取单子录入支付系统甚至更晚。这造成了用户逾期不还款,但是我们记录了用户不还款,产生了滞纳金。当然,以上是业务范畴的问题。今天要说的是支付系统向订单系统发送消息的过程中出现的一个问题。大家都知道,为了避免消息丢失、订单系统处理异常或网络问题等问题,我们在设计消息系统时需要考虑消息持久化和消息失败重试机制。对于重试机制,如果订单系统消费了消息,但是由于网络等问题,消息系统没有收到反馈是否处理成功。此时消息系统会按照配置的规则每隔一段时间重试一次。你重试一次以保证系统正常处理是对的,但是如果此时网络恢复正常,我第一次收到的消息处理成功了,然后又收到了一条消息。如果不采取一些保护措施,会出现以下情况:用户支付一次,订单系统计算两次,导致无法入账的异常财务账单。然后可能用户笑了,老板哭了。接口幂等性#为了防止上述情况的发生,我们需要提供一种保护措施。同样的支付信息,如果我处理成功了其中一条,虽然我又收到了消息,但此时我不会处理,也就是保证了接口的幂等性。维基百科上的定义:幂等性(idempotence,idempotence)是抽象代数中常见的一个数学和计算机科学概念。编程中幂等操作的特点是它执行任意次数与执行一次效果相同。幂等函数或幂等方法是可以使用相同参数重复执行并获得相同结果的函数。这些函数不会影响系统的状态,无需担心重复执行对系统造成的改变。例如,“setTrue()”函数是一个幂等函数。无论执行多少次,结果都是一样的。通过使用唯一的交易号(序列号)保证更复杂的操作是幂等的。的影响与一次执行的影响相同,这是幂等性的核心特征。其实我们编程中最主要的操作是CURD,其中读取(Retrieve)操作和删除(Delete)操作天然是幂等的,受影响的操作是创建(Create)和更新(Update)。业务中需要考虑幂等性的地方一般是接口的重复请求。重复请求是指同一个请求由于某种原因被多次提交。导致这种情况的场景有几种:前端重复提交:提交一个订单,用户快速重复点击多次,导致后台生成多个内容重复的订单。接口超时重试:对于第三方调用的接口,为了防止由于网络抖动或其他原因导致请求丢失,此类接口一般设计为多次超时重试。消息重复消费:MQ消息中间件,消息重复消费。对于一些影响比较大的业务场景,必须考虑接口的幂等性,比如货币交易的接口。否则,一个错误的、考虑不周的接口可能会使公司损失巨额资金,而责任必须在程序员自己身上。幂等性实现方法#对于与web端交互的接口,我们可以在前端拦截一部分,比如防止表单重复提交,灰显,隐藏,按钮不可点击等。但是,实际带来的好处前端控制力不是很高。懂一点技术的都会模拟请求调用你的服务,所以安全策略还是要从后端接口层做。那么后端实现分布式接口幂等性的策略和方法有哪些呢?主要可以从以下几个方面来考虑实现:Token机制#针对前端重复多次连续点击的情况,比如用户购物提交订单,提交订单接口可以通过Token机制来实现,防止重复意见书。主要流程是:服务端提供发送token的接口。我们在分析业务的时候,哪些业务存在幂等问题,在执行业务之前首先要获取到token,服务器会把token保存在redis中。(微服务必须是分布式的,jvm缓存适用于单机)。然后在调用业务接口请求的时候,把token带过来,一般在请求的header中。服务端判断token在redis中是否存在,存在表示第一次请求。此时删除redis中的token,继续业务。如果判断该token在redis中不存在,则表示重复操作,将重复标记直接返回给客户端,保证业务代码不会重复执行。数据库去重表#在向去重表插入数据时,利用数据库的唯一索引特性,保证逻辑唯一。唯一序列号可以是单个字段,例如订单的订单号,也可以是多个字段的唯一组合。例如,设计如下数据库表。CREATETABLE`t_idempotent`(`id`int(11)NOTNULLCOMMENT'ID',`serial_no`varchar(255)NOTNULLCOMMENT'唯一序列号',`source_type`varchar(255)NOTNULLCOMMENT'资源类型',`status`int(4)DEFAULTNULLCOMMENT'state',`remark`varchar(255)NOTNULLCOMMENT'remark',`create_by`bigint(20)DEFAULTNULLCOMMENT'创建者',`create_time`datetimeDEFAULTNULLCOMMENT'创建时间',`modify_by`bigint(20)DEFAULTNULLCOMMENT'修改person',`modify_time`datetimeDEFAULTNULLCOMMENT'修改时间',PRIMARYKEY(`id`)UNIQUEKEY`key_s`(`serial_no`,`source_type`,`remark`)COMMENT'保证业务唯一性')ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='幂等性检查表';让我们关注以下关键字段。@IdempotentKey在数据中建立了由serial_no、source_type、remark三个字段组成的唯一索引,所以可以利用接口Equality的去重功能,具体代码设计如下,publicclassPaymentOrderReq{/***Alipay序列号*/@IdempotentKey(order=1)privateStringalipayNo;/***支付订单ID*/@IdempotentKey(order=2)privateStringpaymentOrderNo;/***支付金额*/privateLongamount;}因为支付宝序列号和订单号在系统中是唯一的,通过MD5组合可以生成唯一的序列号。具体生成方法如下:privatevoidgetIdempotentKeys(ObjectkeySource,Idempotentidempotent){TreeMap<Integer,Object>keyMap=newTreeMap
