实现幂等性的几种方案
1 前言
大家好,我是阿沐!”幂等“这个词语或许小伙伴很少见,基本上中小型公司或者一些大公司都未使用过,但是并不代表小伙伴们没有接触到。
为啥我会扯到这个技术话题?缘由就是 20 年我面试了一些大厂包括身边朋友的面试经历,例如腾讯、网易、字节等等大厂,其中大都会遇到”幂等的概念、理解以及实现与应用“,那么下面就听我一一道来幂等的相关知识。
2 什么是幂等性?
数学中:在一次元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同;在二次元运算为幂等时,自己重复运算的结果等于它自己的元素。
计算机学中:幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因为重复执行对系统造成变化。
3 为什么要使用幂等性?
众所周知,目前随着 js 的发展 web 端慢慢转变了前后端分离,通过接口实现;小程序、app 同样都是 api 实现,基本上接口都是正常的返回信息,并未涉及到重复提交或者是并发提交的情况。但假如我们考虑的细致一点,比如电商系统,抽奖活动、用户反馈、订单支付、消息消费、商品评价、商品点赞等这些都是和幂等息息相关。举几个例子给小伙伴们看下:
- ① 用户重复下订单:当用户下单时,因为网络问题或者手速过快,导致重复下单。
- ② 消息重复消费:当使用 MQ 消息中间件时候,如果消息中间件发生异常出现错误未及时提交消费信息,导致消息被重复消费。
- ③ 抽奖活动(券):当用户参加抽奖活动需要消耗抽奖券时,如果出现并发请求导致抽奖券余额更新错误。
- ④ 重复提交表单:当用户填写表单提交时,可能会因为用户点多次连击提交或者网络波动导致服务端未及时响应,会导致用户重复的提交表单,就出现了同一个表单多次请求。
这些只是我们常见的一些状况,还需要根据自己的项目的实际情况进行分析,判断是否需要幂等操作,举个简单例子:运营做了一次大型活动,参与人数 10w+(每人只能给一个用户点赞冲榜),活动结束后运营需要复盘,这个时候发现一些用户给一个人点赞又多次状况。这个时候,嘿嘿嘿...... 开发过来背锅喽!
4 我们如何在业务功能上实现幂等性?
通常数据库实现主要是利用数据库表中主键唯一约束+唯一索引的特性,如果主键唯一或者设置了复合唯一索引,在”插入“数据的时候就是幂等性操作。例如还有悲观锁、乐观锁、redis 锁、token 令牌、依赖前后端配合生成请求序列号。ps:基本上面试时,大厂都会被问到的问题,不要问我为什么?因为面试官喜欢问。
下面是设计一个活动抽奖大转盘用户抽奖券余额:抽奖券来源参加活动获取,抽奖消耗券场景:
CREATE TABLE `mumu_test` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键自增id', `userid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', `act_id` varchar(125) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '活动ID', `lottery` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '余额', PRIMARY KEY (`id`), UNIQUE KEY `uniq_uid_aid` (`userid`,`act_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='沐沐测试春节抽奖券记录表';
唯一主键索引实现幂等性
通常情况下,我们在做这种用户活动抽奖券记录数据时,会先 select 下看看是否已经有插入的记录了,如果已存在则 update,否则 insert。那么我现在先说说不存在添加数据的情况:
存在用户在做活动任务时,因为网络抖动导致服务端响应超时,这个时候用户以为并没领取奖券成功,就会疯狂的点击领取按钮,那么就会导致同一个任务奖券出现多次请求,那么我们第一次 insert 添加肯定成功了,当并发请求过来时就会重复执行以下 sql 语句:
inser into mumu_test('userid','act_id','lottery')values(123,'spring',1)
由于存在 userid+act_id 唯一键,那么就会出现只有一条数据插入成功,其他的数据就会插入失败,保证了数据的幂等。推荐使用
乐观锁实现幂等性
通俗地讲:它的心态就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。它就像单纯的孩子一样,总是认为不会产生并发冲突的场景,我只是在你提交操作时检查是否违反数据完整性。所以乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。
划重点:一般使用版本号控制 version,即为数据增加一个版本标识,一般是通过为数据库表行数据增加一个数字类型的“version”字段来实现。当读取数据时,会将 version 字段的值一同读出,数据每更新一次,对此 version 值加 1 操作。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的 version 值进行比对,如果数据库表当前版本号与第一次取出来的 version 值相等,则予以更新,否则认为是非法操作。
场景应用:针对上面的表我们增加一个版本标识:
alter table mumu_test add `version` smallint(5) unsigned not null default '0' comment '版本号'
添加成功之后,更新操作:
1.获取抽奖券:update mumu_test set lottery = lottery + 5, version = version + 1 where id = 123 and version = 1; 2.消耗抽奖券update mumu_test set lottery = lottery - 1, version = version + 1 where id = 123 and version = 2;
实现原理:更新数据的同时 version+1,然后判断本次 update 操作的影响行数,如果大于 0,则说明本次更新成功,如果等于 0,则说明本次更新没有让数据变更。当并发请求过来时,只需要拿到 select 的版本号,进行更新操作即可(where 可带上主键 id),保证幂等。推荐使用
悲观锁实现幂等性
顾名思义,悲观锁它是一种悲观的心里状态,对应于生活中悲观的人总是想着事情往坏的方向发展。它像是一个彻底地 loser,它认为别人每次去拿数据都会修改这条数据,所以每次拿数据的时候,都会使数据处于锁定状态。执行下面 sql 语句锁住该条记录:
select * from mumu_test where userid = 123 and act_id = 'spring' for update;
大家可以看到我并没有使用主键 id 是查询,首先我们并不知道这条记录 id 值,所以我们通过 uid+aid 组合的唯一建作为锁表行记录条件,一定要使用主键或者唯一建,不然会将整张表都被锁住,那么其他的用户就无法操作了。
因为悲观锁是需要在同一个事物操作过程中锁住一行数据,假如我们事务逻辑耗时比较久,就会导致后面请求的堆积,直接影响到了整体响应时长。不推荐使用
Token 令牌如何实现幂等性
所谓的 token 令牌其实就是为了防止用户重复提交一个表单信息,这一点基本上 PHP 的框架都会带有 token 验证。服务端需要生成一个全局唯一的 id,(例如:snowflake 雪花算法,美团 Leaf 算法,滴滴 TinyID 算法,百度 Uidgenerator 算法,uuid,redis 等)。
- 客户端每次进入表单页面可以优先申请一个唯一令牌存储本地,服务端存储令牌 token 值(redis,文件,memcache 都可)
- 每次发送请求时可以在 Headers 头部中带上当前这个 token 令牌
- 服务端验证 token 是否存在,存在则删除 token,执行后续业务逻辑;不存在则响应客户端重复提交提示语
生成全局唯一 id 的代码,大家可以网上自行搜索,基本上是千篇一律的,放心抄过来使用就可以了。
最后总结
幂等性基本上在中大型公司项目需求中都能遇到,尤其是现在消息中间件(kafka、rabbitmq 等)的广泛使用,更加注重消息的幂等。那么像我之前在电商公司,支付订单、抽奖券、部分活动相关的中台服务对接口的幂等性都是很重要的,所以我们在日常开发中,可以针对不同的业务场景选择合适的幂等方案,即可满足要求同时也减少性能影响,更重要的是不会因为出 bug 被产品运营 diss。开发真的好卑微啊~~~
聊幂等性这个词语,也是自己想了很久。在这之前我推荐不少开发(经验基本上 5 年+)到大厂,他们给的反馈就有幂等这个概念的询问。ps:当然并不说明他们能力不行、技术差,幂等名词一般大家很少去注意,尤其是常年待在一个公司,并不一定能接触到幂等业务,就算接触到了类似场景;而都是认为对于一致性的要求不能重复插入数据,并不会想起是幂等这个名词。所以并不奇怪,大家也不要在面试中遇到新的名词就内心慌乱,手心出汗、腿发抖、发冷汗,我们完全可以跟面试官聊,是否可以换一种方式来问这个问题;我相信大部分的面试官都能接受,顶多就认为你知识量不够广,不知道这些专业术语等等,不会太影响你后面的解决思路。
参考:
https://cloud.tencent.com/developer/news/838221
======
补充:
实现幂等性的几种方案:
1.select+insert+主键/唯一索引冲突
2.直接insert + 主键/唯一索引冲突
3.状态机幂等
4.抽取防重表
5.token令牌
6.悲观锁(如select for update)
7.乐观锁
8.分布式锁
======
实现分布式锁的几种方案:
1.数据库。(唯一索引,乐观锁,悲观锁)
2.redis。
3.zookeeper。