高可用:如何保证接口幂等性?
接 口幂等性是面试中常见的问题,也是日常开发过程中经常需要解决的问题。
什么是幂等(idempotency)?
幂等(idempotency)本身是一个数学概念,常见于抽象代数中,表示一个函数或者操作的结果不受其输入或者执行次数的影响。例如, f(n) = 1^n ,无论 n 为多少,f(n)的值永远为 1。
在软件开发领域,幂等是对请求操作结果的一个描述,这个描述就是不论执行多少次相同的请求,产生的效果和返回的结果都和发出单个请求是一样的。
针对数据操作来说就是:
insert操作要保证不插入重复的数据;update操作要保证多次相同请求数据依然正确。
接口幂等性问题通常是由于网络波动、用户重复操作、超时重试、消息重复消费、响应速度慢等原因导致的。
不保证幂等会有什么后果?
没有保证幂等会导致产生严重的生产级别的 Bug,比较典型的就是涉及到钱的业务场景。就比如在没有保证幂等性的情况下,我作为用户在付款的时候,我同时点击了多次付款按钮,后端处理了多次相同的扣款请求,结果导致我的账户被扣了多次钱。这就是属于非常非常非常严重的 Bug 了!只要业务涉及到钱就一定要格外注意!!!
综上,保证接口的幂等性至关重要。
另外,保证幂等性这个 操作并不是说前端做了就可以的,后端同样也要做。
如何保证接口幂等性?
前端保证幂等性的话比较简单,一般通过当用户提交请求后将按钮致灰来做到。
后端保证幂等性就稍微麻烦一点,方法也是有很多种,比如悲观锁、唯一索引、去重表、乐观锁 、分布式锁、Token 机制等等。
悲观锁和分布式锁的核心思想都是通过加锁来保证同一时刻只有一个请求能被执行。但仅仅这样是不够的,还需要配合根据业务逻辑进行幂等性判断,例如,注册场景检测指定的电话/邮箱/用户名是否已经被注册、订单支付场景检测订单的状态。
实际项目中,一般采用分布式锁这种方案比较多。
悲观锁
在 Java 中,可以使用 ReetrantLock 类、synchronized 关键字这类 JDK 自带的悲观锁来保证同一时刻只有一个线程能够进行修改。不过,JDK 自带的锁属于是本地锁,分布式环境下无法使用。

除了利用 JDK 提供的悲观锁之外,数据库自身也带了排他锁(X 锁)。排他锁又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
在 MySQL 里使用排他锁:
SELECT ... FOR UPDATE
排他锁只能在支持事务的存储引擎(如 InnoDB)中使用,且只能在事务中使用。另外,排他锁只能在有索引的字段上使用,否则会锁住整个表,影响并发性能。
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。