分布式系统中接口的幂等性(转)

业务场景

公司有个借贷的项目,具体业务类似于阿里的蚂蚁借呗,用户在平台上借款,然后规定一个到期时间,在该时间内用户需将借款还清并收取一定的手续费,如果规定时间逾期未还上,则会产生滞纳金。

用户发起借款因此会产生一笔借款订单,用户可通过支付宝或在系统中绑定银行卡到期自动扣款等方式进行还款。还款流程都走支付系统,因此用户还款是否逾期以及逾期天数、逾期费等都通过系统来计算。

但是在做订单系统的时候,遇到这样一个业务场景,由于业务原因允许用户通过线下支付宝还款,即我们提供一个公司官方的支付宝二维码,用户扫码还款,然后财务不定期的去拉取该支付宝账户下的还款清单并生成规范化的Excel表格录入到支付系统。

支付系统将这些支付信息生成对应的支付订单并落库,同时针对每笔还款记录生产一个消息信息到消息系统,消息的消费者就是订单系统。订单系统接受到消息后去结算当前用户的金额清算:先还本金,本金还清再还滞纳金,都还清则该笔订单结清并提升可借贷额度,……,整个流程大致如下:

从上面的流程描述可以知道,相当于原来线上的支付现在转移到线下进行,这会产生一个问题:支付结算的不及时。例如用户的订单在今天19-05-27到期,但是用户在19-05-26还清,财务在19-05-27甚至更晚的时候从支付宝拉取清单录入支付系统。这样就造成了实际上用户是未逾期还清借款而我们这边却记录的是用户未还清且产生了滞纳金。

当然以上的是业务范畴的问题,我们今天要说的是支付系统发送消息到订单系统的环节中的一个问题。大家都知道为了避免消息丢失或者订单系统处理异常或者网络问题等问题,我们设计消息系统的时候都需要考虑消息持久化和消息的失败重试机制。

对于重试机制,假如订单系统消费了消息,但是由于网络等问题消息系统未收到反馈是否已成功处理。这时消息系统会根据配置的规则隔段时间就 retry 一次。你 retry 一次没错,是为了保证系统的处理正常性,但是如果这时网络恢复正常,我第一次收到的消息成功处理了,这时我又收到了一条消息,如果没有做一些防护措施,会产生如下情况:用户付款一次但是订单系统计算了两次,这样会造成财务账单异常对不上账的情况发生。那就可能用户笑呵呵老板哭兮兮了。

2|0接口幂等性

为了防止上述情况的发生,我们需要提供一个防护措施,对于同一笔支付信息如果我其中某一次处理成功了,我虽然又接收到了消息,但是这时我不处理了,即保证接口的 幂等性

维基百科上的定义:

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

任意多次执行所产生的影响均与一次执行的影响相同,这是幂等性的核心特点。其实在我们编程中主要操作就是CURD,其中读取(Retrieve)操作和删除(Delete)操作是天然幂等的,受影响的就是创建(Create)、更新(Update)。

对于一些业务场景影响比较大的,接口的幂等性是个必须要考虑的问题,例如金钱的交易方面的接口。否则一个错误的、考虑不周的接口可能会给公司带来巨额的金钱损失,那么背锅的肯定是程序员自己了。

3|0幂等性实现方式

对于和web端交互的接口,我们可以在前端拦截一部分,例如防止表单重复提交,按钮置灰、隐藏、不可点击等方式。

但是前端做控制实际效益不是很高,懂点技术的都会模拟请求调用你的服务,所以安全的策略还是需要从后端的接口层来做。

那么后端要实现分布式接口的幂等性有哪些策略方式呢?主要可以从以下几个方面来考虑实现:

3|1数据库去重表

往去重表里插入数据的时候,利用数据库的唯一索引特性,保证唯一的逻辑。唯一序列号可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。例如设计如下的数据库表。

CREATE TABLE `t_idempotent` (
  `id` int(11) NOT NULL COMMENT ‘ID‘,
  `serial_no` varchar(255)  NOT NULL COMMENT ‘唯一序列号‘,
  `source_type` varchar(255)  DEFAULT NULL COMMENT ‘资源类型‘,
  `status` int(4) DEFAULT NULL COMMENT ‘状态‘,
  `remark` varchar(255)  DEFAULT NULL COMMENT ‘备注‘,
  `create_by` bigint(20) DEFAULT NULL COMMENT ‘创建人‘,
  `create_time` datetime DEFAULT NULL COMMENT ‘创建时间‘,
  `modify_by` bigint(20) DEFAULT NULL COMMENT ‘修改人‘,
  `modify_time` datetime DEFAULT NULL COMMENT ‘修改时间‘,
  PRIMARY KEY (`id`)
  UNIQUE KEY `key_s` (`serial_no`,`source_type`)  COMMENT ‘保证业务唯一性‘
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘幂等性校验表‘;

看几个关键性字段,serial_no:唯一序列号的值,在这里我设置的是通过注解@IdempotentKey来标识请求对象中的字段,通过对他们Md5加密获取对应的值。

public class PaymentOrderReq {

    /**
     * 支付宝流水号
     */
    @IdempotentKey(order=1)
    private String alipayNo;

    /**
     * 支付订单ID
     */
    @IdempotentKey(order=2)
    private String paymentOrderNo;

    /**
     * 支付金额
     */
    private Long amount;
}

因为支付宝流水号和订单号在系统中是唯一的,所以唯一序列号可由他们组合 Md5 生成,具体的生成方式如下:

private void getIdempotentKeys(Object keySource, Idempotent idempotent) {
    TreeMap<Integer, Object> keyMap = new TreeMap<Integer, Object>();
    for (Field field : keySource.getClass().getDeclaredFields()) {
        if (field.isAnnotationPresent(IdempotentKey.class)) {
            try {
                field.setAccessible(true);
                keyMap.put(field.getAnnotation(IdempotentKey.class).order(),
                        field.get(keySource));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                logger.error("", e);
                return;
            }
        }
    }
    generateIdempotentKey(idempotent, keyMap.values().toArray());
}

生成幂等Key

private void generateIdempotentKey(Idempotent idempotent, Object... keyObj) {
     if (keyObj.length == 0) {
         logger.info("idempotentkey is empty,{}", keyObj);
         return;
     }
     StringBuilder sb = new StringBuilder();
     for (Object key : keyObj) {
         sb.append(key.toString()).append("|");
     }
     idempotent.setRemark(sb.toString());
     idempotent.setSerialNo(Md5Util.md5(sb.toString()));
 }

一切准备就绪,则可对外提供幂等性校验的接口方法,接口方法为:

public <T> void idempotentCheck(IdempotentTypeEnum idempotentType, T keyObj) throws IdempotentException {
    Idempotent idempotent = new Idempotent();
    getIdempotentKeys(keyObj, idempotent );
    if (StringUtils.isBlank(idempotent.getSerialNo())) {
        throw new ServiceException("fail to get idempotentkey");
    }
    idempotentEvent.setSourceType(idempotentType.name());
    try {
        idempotentMapper.saveIdempotent(idempotent);
    } catch (DuplicateKeyException e) {
        logger.error("idempotent check fail", e);
        throw new IdempotentException(idempotent);
    }
}

当然这个接口的方法具体在项目中合理的使用就看项目要求了,可以通过@Autowire注解注入到需要使用的地方,但是缺点就是每个地方都需要调用。我个人推荐的是自定义一个注解,在需要幂等性保证的接口上加上该注解,然后通过拦截器方法拦截使用。这样简单便不会造成代码侵入和污染。

另外,使用数据库防重表的方式它有个严重的缺点,那就是系统容错性不高,如果幂等表所在的数据库连接异常或所在的服务器异常,则会导致整个系统幂等性校验出问题。如果做数据库备份来防止这种情况,又需要额外忙碌一通了啊。

3|2Redis实现

上面介绍过防重表的设计方式和伪代码,也说过它的一个很明显的缺点。所以我们另外介绍一个Redis的实现方式。

Redis实现的方式就是将唯一序列号作为Key,唯一序列号的生成方式和上面介绍的防重表的一样,value可以是你想填的任何信息。唯一序列号也可以是一个字段,例如订单的订单号,也可以是多字段的唯一性组合。具体校验流程如下图所示,实现代码也很简单这里就不写了。

由于企业如果考虑在项目中使用 Redis,因为大部分会拿它作为缓存来使用,那么一般都会是集群的方式出现,至少肯定也会部署两台Redis服务器。所以我们使用Redis来实现接口的幂等性是最适合不过的了。

原文地址:https://www.cnblogs.com/ZenoLiang/p/10941157.html

时间: 2024-11-04 14:15:44

分布式系统中接口的幂等性(转)的相关文章

分布式系统互斥性与幂等性问题的分析与解决

前言 随着互联网信息技术的飞速发展,数据量不断增大,业务逻辑也日趋复杂,对系统的高并发访问.海量数据处理的场景也越来越多.如何用较低成本实现系统的高可用.易伸缩.可扩展等目标就显得越发重要.为了解决这一系列问题,系统架构也在不断演进.传统的集中式系统已经逐渐无法满足要求,分布式系统被使用在更多的场景中. 分布式系统由独立的服务器通过网络松散耦合组成.在这个系统中每个服务器都是一台独立的主机,服务器之间通过内部网络连接.分布式系统有以下几个特点: 可扩展性:可通过横向水平扩展提高系统的性能和吞吐量

接口的幂等性原则

接口调用存在的问题 现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景.什么是接口幂等性 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而

分布式服务接口的幂等性如何设计

面试官心理分析 从这个问题开始,面试官就已经进入了实际的生产问题的面试了. 一个分布式系统中的某个接口,该如何保证幂等性?这个事儿其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题.啥意思呢? 你看,假如你有个服务提供一些接口供外部调用,这个服务部署在了 5 台机器上,接着有个接口就是付款接口.然后人家用户在前端上操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,好了,结果一个订单扣款扣两次. 或者是订单系统调用支付系统进

多版本并发控制(MVCC)在分布式系统中的应用

问题 最近项目中遇到了一个分布式系统的并发控制问题.该问题可以抽象为:某分布式系统由一个数据中心D和若干业务处理中心L1,L2 ... Ln组成:D本质上是一个key-value存储,它对外提供基于HTTP协议的CRUD操作接口.L的业务逻辑可以抽象为下面3个步骤: read: 根据keySet {k1, ... kn}从D获取keyValueSet {k1:v1, ... kn:vn} do: 根据keyValueSet进行业务处理,得到需要更新的数据集keyValueSet' {k1':v1

如何保证接口的幂等性。。。。。

在微服务架构下,我们在完成一个订单流程时经常遇到下面的场景: 一个订单创建接口,第一次调用超时了,然后调用方重试了一次 在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次 一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款.但是你先接收到已付款,然后又接收到了已创建 在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢.消息中

分布式系统中的CAP原理

分布式系统中的CAP原理,布布扣,bubuko.com

大型网站架构系列:缓存在分布式系统中的应用(三)

本文是<缓存在分布式系统中的应用>第三篇文章. 上次主要给大家分享了,缓存在分布式系统中的应用,主要从不同的场景,介绍了CDN,反向代理,分布式缓存,本地缓存的常规架构和基本原理. 因为时间关于,原计划分享<缓存常见问题>的内容,没有讲.本次主要针对缓存的常见个问题,做一个介绍.主要有以下议题: 一.分享大纲 分享大纲 数据一致性 缓存高可用 缓存雪崩 缓存穿透 参考资料 分享总结 二.数据一致性 缓存是在数据持久化之前的一个节点,主要是将热点数据放到离用户最近或访问速度更快的介质

java中接口中成员的定义

java中的接口的作用是提供编程框架,它作为统一的规范让其他类进行扩展,是java中非常优秀的设计. 这娃用以下代码总结了java中接口可以定义的成员以及它们默认被修饰的关键字: //外部接口的访问修饰符只能是public或默认修饰符 ,并且它的成员只能用public访问修饰符修饰, 接口不能用final修饰 public interface A { //成员变量,默认用public static final 修饰 String name="ahei"; //成员方法,默认用publi

分布式系统中的线程与进程

进程 虽然进程构成了分布式系统中的基本组成单元,但是操作系统提供的用于构建分布式系统的进程在粒度上还是太大了,而就粒度而言,将每个进程细分为若干控制线程的形式则更加合适. 为了程序执行的需要,操作系统创建多个虚拟处理器,每个虚拟处理器运行一个程序.为了保持对这些虚拟处理器的跟踪,操作系统中有一张进程表.其包含的条目中存储着CPU寄存器值.内存映像.打开的文件.统计信息.特权信息等. 操作系统特别注意确保独立的进程不会有意或无意地破坏其他独立进程运行的正确性.也就是说,多个进程并发地共享同一个CP