前言
幂等性,是开发人员在日常开发中必须要思考的,尤其是转账、领取等波及金额交易的场景,如果呈现幂等性的问题,造成的结果是十分重大的。
本文将分享一下什么是幂等性以及如何保障幂等性。
什么是幂等性
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操作的特点是 其任意屡次执行所产生的影响均与一次执行的影响雷同。幂等函数,或幂等办法,是指能够应用雷同参数反复执行,并能取得雷同后果的函数。这些函数不会影响零碎状态,也不必放心反复执行会对系统造成扭转。
幂等性产生起因
- 前端未做限度,导致用户反复提交
- 应用浏览器后退,或者按 F5 刷新,或者应用历史记录,反复提交表单
- 网络稳定,引起反复申请
- 超时重试,引起接口反复调用
- 定时工作设置不合理,导致数据反复解决
- 应用音讯队列时,音讯反复生产
如何保障幂等性
1. 前端解决
- 提交按钮点击置灰,或者减少 loading
- 页面重定向(PRG),PRG 模式即
POST-REDIRECT-GET
,当用户进行表单提交时,会重定向到另外一个提交胜利页面,而不是停留在原先的表单页面。这样就防止了用户刷新导致反复提交。同时避免了通过浏览器按钮后退 / 后退导致表单反复提交。
2. 先 select 后 insert + 惟一索引抵触
在保留数据前,咱们须要先 select 一下数据是否存在。如果数据已存在,则返回失败(具体操作视业务状况而定),如果数据不存在,则执行 insert 操作。
但在高并发的场景下,可能会呈现 两个申请 select 的时候,都没有查到数据,而后都执行了 insert 操作,所以此时会有反复数据产生,因而在数据库中,咱们须要增加惟一索引来保障幂等。
流程图如下:
此计划实用于新增操作的接口,如用户注册。
3. 建去重表
某些业务场景,是容许反复数据存在的,仅在流程的某个环节才不容许呈现反复数据,这种状况间接在表中增加惟一索引是不适合的,所以就须要创立一张去重表。
CREATE TABLE `table_name` (`id` bigint(15) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_id` varchar(100) NOT NULL COMMENT '订单号',
`create_time` datetime DEFAULT NULL COMMENT '创立工夫',
PRIMARY KEY (`id`),
UNIQUE KEY `index_order_id` (`order_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='去重表';
流程图如下:
特地留神,防重表与业务表必须在同一数据库,并且操作要在同一事务中。
此计划实用于在业务中有惟一标识的插入场景中,比方在领取业务中,若一个订单只会领取一次,则订单 ID 能够作为惟一标识。
4. 应用乐观锁
乐观锁,正如其名,具备强烈的独占和排他个性。它指的是对数据被外界(包含本零碎以后的其余事务,以及来自内部零碎的事务处理)批改持激进态度,因而,在整个数据处理过程中,将数据处于锁定状态。乐观锁的实现,往往依附数据库提供的锁机制(也只有数据库层提供的锁机制能力真正保证数据拜访的排他性,否则,即便在本零碎中实现了加锁机制,也无奈保障内部零碎不会批改数据)。
在交易场景中,用户账户余额有 100 元,转出 50 元,失常状况下用户的余额残余 50 元。
update account set amount-50 where id = 123;
如果此时有多个雷同的申请,可能会导致用户的金额变为正数。所以此时能够应用乐观锁,将用户的行数据锁住,在同一时刻只容许一个申请取得锁,其余申请期待。
select * from account where id = 123 for update;
流程图如下:
须要特地留神的是:如果应用的是 mysql 数据库,存储引擎必须用 innodb,因为它才反对事务。此外,这里 id 字段肯定要是主键或者惟一索引,不然会锁住整张表。
因为乐观锁是须要在同一事务中锁住一行数据,所以如果事务比拟长,会造成大量申请期待,影响接口性能。
5. 应用乐观锁
乐观锁(Optimistic Locking)绝对乐观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁大多数状况下依附数据库的锁机制实现,以保障操作最大水平的独占性。但随之而来的就是数据库性能的大量开销,特地是对长事务而言,这样的开销往往无奈接受。而乐观锁机制在肯定水平上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据减少一个版本标识,在基于数据库表的版本解决方案中,个别是通过为数据库表减少一个“version”字段来实现。读取出数据时,将此版本号一起读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的以后版本信息进行比对,如果提交的数据版本号等于数据库表以后版本号,则予以更新,否则认为是过期数据。
乐观锁次要基于版本标识(version)进行操作,即每次操作查问数据时都要先查问出版本标识(version),而后依据版本标识(version)进行 update 操作。
select id,amount,version from account id = 123;
update account set amount=amount-50,version=version+1 where id=123 and version = 1;
当多个雷同的申请查问信息时,版本标识是雷同的,当其中一个申请实现 update 操作,后续申请影响条数均为 0。
流程图如下:
6. 依据状态机
很多时候,业务流程是有状态流转的,这个时候能够应用状态机来保障幂等性。
如订单业务中,存在状态「1- 已下单,2- 已领取,3- 已实现,4- 已勾销」,依照业务流程,状态是顺次流转的,所以在 update 操作时,咱们就要依据本次的状态来更新下一次的状态。
update order_info set status = 3 where id = 123 and status = 2;
流程图如下:
7. 应用分布式锁
分布式锁的逻辑是,每次申请都通过业务惟一 ID 来尝试获取锁,如果获取胜利,就进行后续业务逻辑操作,如果获取失败,就舍弃申请间接返回。
分布式锁通常是基于 redis 来实现的。
流程图如下:
分布式锁是通过设置 redis 的过期工夫来进行管制。如果过期工夫设置太短,则无奈无效避免反复申请;如果过期工夫设置太长,则影响 redis 存储空间,甚至会影响后续业务操作。因而须要依据具体的业务状况,来设置正当的过期工夫。
8. 基于 token 机制
此计划蕴含两个申请阶段:
1. 客户端申请服务端申请获取 token
2. 客户端携带 token 再次申请,服务端校验 token 后进行操作。
流程图如下:
这里有一个留神的点:
服务端验证 token 是否存在,要应用删除 key 的形式,即 redis.del(key),删除胜利则示意校验 token 通过;
不能应用先查再删的操作,即先 redis.get(key),后 redis.del(key),这种形式在高并发下无奈保障幂等。
* 参考资料
如何实现接口幂等性
高并发下如何保障接口的幂等性?