不积跬步,无以至千里

可重入和幂等


可重入和幂等性是在做系统设计的时候经常听到的名词,大概来说,可重入是可以重复调用,幂等则是每次多次重复调用的结果是相同的.了解下具体在系统设计的时候,什么样的场景下需要做可重入/幂等的考虑,具体实现上来说,有哪些方案.

可重入(Reentrancy)

首先,粘一段wiki上的解释:

可重入函数 一个函数在运行的过程中,可以被中断后再执行,那么这个函数就可以认为是可重入的.

不可重入函数在实时系统设计中被视为不安全函数

若一个函数是可重入的,则该函数应当满足下述条件:

  • 不能含有静态(全局)非常量数据
  • 只能处理由调用者提供的数据,即,不能修改自己的数据
  • 调用的函数也必需是可重入的

以上解释说明可重入这个名词其实是来源于操作系统,我在看各种资料的时候,有个疑问:可重入的函数是现成安全的吗?

不同的资料上给出的说法不一样,但是在讨论之前,需要首先明确一个概念:中断后再次执行的话,什么样的执行结果可以被认为这个函数是可重入的?

看下wiki上的一个可重入的例子:

int tmp;

void swap(int* x, int* y)
{
    /* Save global variable. */
    int s;
    s = tmp;

    tmp = *x;
    *x = *y;
    *y = tmp;     /* Hardware interrupt might invoke isr() here. */

    /* Restore global variable. */
    tmp = s;
}

void isr()
{
    int x = 1, y = 2;
    swap(&x, &y);
}
//https://en.wikipedia.org/wiki/Reentrancy_(computing)#Reentrant_but_not_thread-safe

以上代码,假如在注释指定的地方中断,tmp会被赋值成x.此时去读取tmp的话,值为x.下一次再次开始执行直到执行完成,tmp又会被赋值成tmp原始值.这样在不同执行阶段中断执行,读取的tmp的值会不一样,这样还算是可重入函数吗?

再来研究下可重入函数的第一个定义:

Reentrant code may not hold any static or global non-constant data. Reentrant functions can work with global data. For example, a reentrant interrupt service routine could grab a piece of hardware status to work with (e.g., serial port read buffer) which is not only global, but volatile. Still, typical use of static variables and global data is not advised, in the sense that only atomic read-modify-write instructions should be used in these variables (it should not be possible for an interrupt or signal to come during the execution of such an instruction). Note that in C, even a read or write is not guaranteed to be atomic; it may be split into several reads or writes.[4] The C standard and SUSv3 provide sig_atomic_t for this purpose, although with guarantees only for simple reads and writes, not for incrementing or decrementing.[5] More complex atomic operations are available in C11, which provides stdatomic.h

以上这段话,简单来说就是可重入函数不该含有全局变量或静态变量,但是可以使用全局变量,但是在操作这个全局变量的话,需要是原子性的.

那么问题来了,如果依赖了一个全局变量,有可能一个代码在执行过程中,另一个线程修改了这个全局变量;而这个代码恢复执行后,执行结果由可能会发生改变.如果说这样的一个函数是可重入函数的话,那么就我的理解(不中断跟中断后执行,结果是可预期)相违背了.

对于这个有争议的理解,我暂时没有找到更权威清晰的讲解说明.这个概念暂且不表.

幂等(Idempotence)

幂等的概念来自于数据,就是说一个数学运算重复多次执行的结果是相同的.如1的x次幂还是1.

在计算机中的概念则是说,一个子程序,可以被多次执行,而不会对系统造成不期望的结果.这个概念在系统设计的时候很重要,因为对于不幂等的方法或者接口,需要记录这个接口是否已经被执行过.

在具体的业务场景中,什么情况下需要保证一个接口是幂等的呢?最典型的就是支付场景下,用户点击付款按钮的时候,是绝对不希望在一个付款动作中,被扣款多次的.这种情况下就需要考虑如何做接口的幂等,不仅仅是接口的幂等,如果能做到业务流程的幂等的话,还需要考虑如何预防用户误操作点击多次付款接口.

首先考虑接口的幂等,接口做幂等性就是保证相同的请求参数,可以重复请求,且不会造成问题

最典型的就是,对每一个请求,使用一个唯一的标识,如请求ID,对于重复的请求ID,认为这两个请求是重复请求.为了避免两个重复的请求发生并发,我们需要为数据表建立唯一索引,如果数据表无法建立唯一索引,也可以考虑使用redis加锁,因为redis的各个请求操作是在一个队列中执行的,所以可以避免并发操作问题.我也遇到过数据表的数据量过大或者存在历史数据,导致无法添加唯一索引,这个时候可以添加一个专门的数据锁表,根据具体的业务逻辑,拼接一个唯一的字符串标识,这样,通过添加一张外部关联表解决了无法在业务表添加唯一索引的问题

业务流程上的幂等已经不单单是接口层面上考虑的事情了,需要根据具体的业务逻辑来做处理.

如对于同一个订单的支付,业务层可能会调用多次下游的支付接口,并且传入了不同的请求单号.假如两次支付调用成功,那么用户就付出了两倍该付的价钱,这种情况是不合理的,那么这时候就可以要求业务在请求支付的时候传入订单的金额,下游支付系统可以对针对同一个订单的支付的总金额做一个校验.

另一场景是,由下游渠道返回给上游系统一个token,上游渠道在请求下游渠道时携带token,该token只能使用一次,使用过后将被置为无效.这种方式适用于上游系统和下游系统存在多次交互.

总结下来,幂等性可以通过指定唯一标识的方式来做;哪个系统是主动发起方,就由哪个系统指定唯一的标识.