7

架构精进之路的个人空间

 2 years ago
source link: https://my.oschina.net/jiagoujingjin/blog/5302713
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

hello,大家好,我是张张,「架构精进之路」公号作者。

在日常工作中的一些技术设计方案评审会上,经常会有提到注意服务接口的幂等性问题,最近就有个同学就跑到跟前问我,幂等性到底是个啥?

在目前分布式/微服务化的今天,提供的服务能力丰富多样,基于 HTTP 协议的 Web API 是时下最为流行的一种分布式服务提供方式,对于服务的幂等性保障尤为重要。

我想了想,觉得有必要好好给大家普及一下才行。

今天计划就关于服务幂等性的一系列问题,在此将材料总结整理,分享给大家~

1、何为幂等性?

幂等(idempotence),来源于数学中的一个概念,例如:幂等函数/幂等方法(指用相同的参数重复执行,并能获得相同结果的函数,这些函数不影响系统状态,也不用担心重复执行会对系统造成改变)。

简单理解即:多次调用对系统的产生的影响是一样的,即对资源的作用是一样的。

4c04bf1a-23b2-413a-8cea-62b245fc5c73.jpg

幂等性强调的是外界通过接口对系统内部的影响, 只要一次或多次调用对某一个资源应该具有同样的副作用就行。

注意:这里指对资源造成的副作用必须是一样的,但是返回值允许不同!

2、幂等性主要场景有哪些?

根据上面对幂等性的定义我们得知:产生重复数据或数据不一致,这个绝大部分是由于发生了重复请求。

这里的重复请求是指同一个请求在一些情况下被多次发起。

导致这个情况会有哪些场景呢?

  • 微服务架构下,不同微服务间会有大量的基于 http,rpc 或者 mq 消息的网络通信,会有第三个情况【未知】,也就是超时。如果超时了,微服务框架会进行重试。

  • 用户交互的时候多次点击,无意地触发多笔交易。

  • MQ 消息中间件,消息重复消费

  • 第三方平台的接口(如:支付成功回调接口),因为异常也会导致多次异步回调

  • 其他中间件/应用服务根据自身的特性,也有可能进行重试。

3、幂等性的作用是什么?

幂等性主要保证多次调用对资源的影响是一致的。

在阐述作用之前,我们利用资源处理应用来说明一下:

HTTP 与数据库的 CRUD 操作对应: 

   PUT :CREATE

   GET :READ

   POST :UPDATE

   DELETE :DELETE

(其实不光是数据库,任何数据如文件图表都是这样)

SELECT * FROM users WHERE xxx;

不会对数据产生任何变化,天然具备幂等性。

INSERT INTO users (user_id, name) VALUES (1, 'zhangsan');

case1:带有唯一索引(如:`user_id`),重复插入会导致后续执行失败,具有幂等性;

case2:不带有唯一索引,多次插入会导致数据重复,不具有幂等性。

case1:直接赋值,不管执行多少次 score 都一样,具备幂等性。

UPDATE users SET score = 30 WHERE user_id = 1;
case2:计算赋值,每次操作 score 数据都不一样,不具备幂等性。
UPDATE users SET score = score + 30 WHERE user_id = 1;

case1:绝对值删除,重复多次结果一样,具备幂等性。

DELETE FROM users WHERE id = 1;

case2:相对值删除,重复多次结果不一致,不具备幂等性。

DELETE top(3) FROM users;

总结:通常只需要对写请求(新增 &更新)作幂等性保证。 

4、如何解决幂等性问题?

我们在网上搜索幂等性问题的解决方案,会有各种各样的解法,但是如何判断哪种解决方案对于自己的业务场景是最优解,这种情况下,就需要我们抓问题本质。

经过以上分析,我们得到了解决幂等性问题就是要控制对资源的写操作。

我们从问题各个环节流程来分析解决:

456500c7-fe2f-4803-9212-c74d6a860918.jpg

4.1 控制重复请求

控制动作触发源头,即前端做幂等性控制实现

相对不太可靠,没有从根本上解决问题,仅算作辅助解决方案。

主要解决方案:

  • 控制操作次数,例如:提交按钮仅可操作一次(提交动作后按钮置灰)

  • 及时重定向,例如:下单/支付成功后跳转到成功提示页面,这样消除了浏览器前进或后退造成的重复提交问题。

4.2 过滤重复动作

控制过滤重复动作,是指在动作流转过程中控制有效请求数量。

1)分布式锁

利用 Redis 记录当前处理的业务标识,当检测到没有此任务在处理中,就进入处理,否则判为重复请求,可做过滤处理。

订单发起支付请求,支付系统会去 Redis 缓存中查询是否存在该订单号的 Key,如果不存在,则向 Redis 增加 Key 为订单号。查询订单支付已经支付,如果没有则进行支付,支付完成后删除该订单号的 Key。通过 Redis 做到了分布式锁,只有这次订单订单支付请求完成,下次请求才能进来。

分布式锁相比去重表,将放并发做到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。

2)token 令牌

应用流程如下:

1)服务端提供了发送 token 的接口。执行业务前先去获取 token,同时服务端会把 token 保存到 redis 中;

2)然后业务端发起业务请求时,把 token 一起携带过去,一般放在请求头部;

3)服务器判断 token 是否存在 redis 中,存在即第一次请求,可继续执行业务,执行业务完成后将 token 从 redis 中删除;

4)如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码不被重复执行。

43cb3b60-5cac-4eb7-99e8-1ae302445f3a.jpg

3)缓冲队列

把所有请求都快速地接下来,对接入缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的请求数据。

优点:同步转异步,实现高吞吐。

缺点:不能及时返回处理结果,需要后续监听处理结果的异步返回数据。

0b63d3b4-cac8-443d-89da-0118f4989745.jpg

4.3 解决重复写

实现幂等性常见的方式有:悲观锁(for update)、乐观锁、唯一约束。

1)悲观锁(Pessimistic Lock)

简单理解就是:假设每一次拿数据,都有认为会被修改,所以给数据库的行或表上锁。

当数据库执行 select for update 时会获取被 select 中的数据行的行锁,因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。

select for update 获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。(注意 for update 要用在索引上,不然会锁表)

START TRANSACTION; # 开启事务SELETE * FROM users WHERE id=1 FOR UPDATE;UPDATE users SET name= 'xiaoming' WHERE id = 1;COMMIT; # 提交事务

2)乐观锁(Optimistic Lock)

简单理解就是:就是很乐观,每次去拿数据的时候都认为别人不会修改。更新时如果 version 变化了,更新不会成功。

不过,乐观锁存在失效的情况,就是常说的 ABA 问题,不过如果 version 版本一直是自增的就不会出现 ABA 的情况。

UPDATE users SET name='xiaoxiao', version=(version+1) WHERE id=1 AND version=version;

缺点:就是在操作业务前,需要先查询出当前的 version 版本

另外,还存在一种:状态机控制

例如:支付状态流转流程:待支付->支付中->已支付

具有一定要的前置要求的,严格来讲,也属于乐观锁的一种。

3)唯一约束

常见的就是利用数据库唯一索引或者全局业务唯一标识(如:source+序列号等)。

这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。

全局 ID 生成方案:

  • UUID:结合机器的网卡、当地时间、一个随记数来生成 UUID;

  • 数据库自增 ID:使用数据库的 id 自增策略,如 MySQL 的 auto_increment。Redis 实现:通过提供像 INCR 和 INCRBY 这样的自增原子命令,保证生成的 ID 肯定是唯一有序的。

  • 雪花算法-Snowflake:由 Twitter 开源的分布式 ID 生成算法,以划分命名空间的方式将 64-bit 位分割成多个部分,每个部分代表不同的含义。

小结:按照应用上的最优收益,推荐排序为:乐观锁 > 唯一约束 > 悲观锁。

通常情况下,非幂等问题,主要是由于重复且不确定的写操作造成的。

1、解决重复的主要思考点

从请求全流程,控制重复请求触发以及重复数据处理

  • 客户端 控制发起重复请求

  • 服务端 过滤重复无效请求

  • 底层数据处理 避免重复写操作

2、控制不确定性主要思考点

从服务设计思路上做改变,尽量避免不确定性:

  • 统计变量改为数据记录方式

  • 范围操作改为确定操作

听了我以上大段的讲述后,他好像收获感满满的似的说:大概理解了...

但是出于自身责任感,我还得叮嘱他几句:

1)幂等性处理 虽然复杂了业务处理,也可能会降低接口的执行效率,但是为了保证系统数据的准确性,是非常有必要的;

2)遇到问题,善于发现并挖掘本质问题,这样解决起来才能高效且精准;

3)选择自身业务场景适合的解决方案,而不要去硬套一些现成的技术实现,无论是组合还是创新,要记住适合的才是最好的。

愿大家能够掌握问题分析以及解决的能力,都不要一上来就急于解决问题,可以多做些深入分析,了解本质问题之后再考虑解决办法进行解决。

希望今天的讲解对大家有所帮助,Thanks for reading!

🎉 福利:关注公众号回复关键字:Redis,即可免费获取《Redis设计与实现》电子书

·················· END ··················

关注公众号,免费领学习资料

十年研发路,大厂架构师,CSDN博客专家

专注架构技术学习及分享,职业与认知升级

坚持分享接地气儿的干货,期待与你一起成长




fe582c34-1269-4605-a212-b1dd55574e15.png

「架构精进之路」专注架构研究,技术分享

点“赞”和“在看”哦 9ba41778-fe27-49a8-b76d-78fda1da587d.gif

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK