1-面试&实际开发场景
1-1面试场景题目
分布式服务接口的幂等性如何设计(比如不能重复扣款)?
1-2 题目分析
一个分布式系统中的某个接口,要保证幂等性,如何保证?这个事,其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题,为什么呢?
实际案例1:
假如你有个服务提供一个付款业务的接口,而这个服务分别部署在5台服务器上,然后用户在前端操作时,不知道为啥,一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的服务器上,这下好了,一个订单扣款扣了两次。
实际案例2:
订单系统调用支付系统进行支付,结果不消息网络,然后订单系统走了前面我们看到的重试retry机制,那就给你重试一次吧,那么支付系统收到了一个支付请求两次,而且因为负载均衡算法落在了不同的机器上。
小结:
所以你必须得知道这事,否则你做出来的分布式系统恐怕很容易埋坑!
2-幂等性介绍
2-1-概念:
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个简单的例子:那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常了,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要对数据操作加入事务即可,发生错误的时候立即回滚,但是再响应客户端的时候也有可能网络中断或者异常等等情况。
2-2- 产生幂等性问题的原因:
- 网络问题/用户误操作/恶意操作,用户点击了多次
- 网络问题,微服务重试retry
- 网络问题很常见,100次请求,都ok;1万次请求可能1次超时会重试;10万次可能10次超时会重试,100万次可能100次超时会重试;如果100个请求重复了,你没处理,导致订单扣款2次,100个订单都扣错了,每天被100个用户投诉,一个月被3000个用户投诉。
2-3- 使用幂等性的场景
- 前端重复提交:前端瞬时点击多次造成表单重复提交
- 接口超时重试:接口可能会因为某些原因而调用失败,处于容错性考虑会加上失败重试的机制。如果接口调用一半,再次调用就会因为脏数据的存在而产生异常
- 消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开链接,那么已经执行了一半的消息会重新放回队列。被其他消费者重新消费时就会导致结果异常,如数据库重复数据, 数据库数据冲突,资源重复等
- 请求重发:网络抖动引发的nginx重发请求,造成重复调用。
3-幂等性的解决方案
3-1- Insert接口幂等性
1.使用分布式锁保证幂等性
秒杀场景下,一个用户只能购买同一商品一次的解决方法:采用用户ID+商品ID,存储到redis中,使用redis中的setNX操作,等待自然过期。
2.使用token机制保证幂等性
用户注册时,用户点击注册按钮多次,是不是会注册多个用户?我们可以在用户进入注册页面后由后台生成一个token,传给前端页面,用户在点击提交时,将token带给后台,后台使用该token作为分布式锁,setNX操作,执行成功后不释放锁,等待自然过期。
3.使用mysql unique key 保证幂等性
用户注册时,用户点击注册按钮多次,是不是会注册多个用户? 我们可以使用手机号作为mysql用户表唯一key。也就是一个手机号只能注册一次。
3-2- Update接口幂等性
update操作可能存在幂等性的问题:
1.用户更改个人信息,疯狂点击按钮,不会发生幂等性问题,因为数据始终为修改后的数据。
2.用户购买商品,用户在点击后,网络出现问题,可能再次点击,这样就会出现幂等性问题,导致购买了多次,可以使用乐观锁
update order set count=count-1,version=version+1 where id=1 and version=1
3-3- Delete接口幂等性
根据唯一id删除不会出现幂等性问题,因为第二次删除的时候mysql中已经不存在该数据
3-4- Select接口幂等性
查询操作不会改变数据,所以是天然的幂等性操作。
3-5- 混合操作(一个接口包含多种操作)
使用Token
机制,或使用Token
+ 分布式锁的方案来解决幂等性问题。
4-幂等性解决方案实现思路
4-1- Token机制实现
通过Token
机制实现接口的幂等性,这是一种比较通用性的实现方法。
具体流程步骤:
- 客户端会先发送一个请求去获取
Token
,服务端会生成一个全局唯一的ID
作为Token
保存在Redis
中,同时把这个ID
返回给客户端; - 客户端第二次调用业务请求的时候必须携带这个
Token
; - 服务端会校验这个
Token
,如果校验成功,则执行业务,并删除Redis
中的Token
; - 如果校验失败,说明
Redis
中已经没有对应的Token
,则表示重复操作,直接返回指定的结果给客户端。
4-2- 基于MySQL实现
通过MySQL
唯一索引的特性实现接口的幂等性。
具体流程步骤:
- 建立一张去重表,其中某个字段需要建立唯一索引;
- 客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中;
- 因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑;
- 如果插入失败,则代表已经执行过当前请求,直接返回。
4-3- 基于Redis实现
通过Redis
的SETNX
命令实现接口的幂等性。
SETNX key value
:当且仅当key
不存在时将key
的值设为value
;若给定的key
已经存在,则SETNX
不做任何动作。设置成功时返回1
,否则返回0
。
具体流程步骤:
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段;
- 将该字段以
SETNX
的方式存入Redis
中,并根据业务设置相应的超时时间; - 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑;
- 如果设置失败,则代表已经执行过当前请求,直接返回。
5-幂等性解决方案案例实现
5-1-基于Token机制的实现
5-1-1-实现思路
为需要保证幂等性的每一次请求创建一个唯一的标识token,先获取token,并将此token存入到redis,请求接口时,将此token放在header或者作为请求参数请求接口,后端接口判断redis中是否存在此token;
- 如果存在,则正常处理业务逻辑,并从redis中删除此token,那么,如果是重复请求,由于token已经被删除,则不能能够通过校验,返回重复提交
- 如果不存在,说明参数不合法或者是重复请求,返回提示即可
5-1-2-请求流程
- 当页面加载的时候通过接口获取token
- 当访问接口时,会经过拦截器,如果发现该接口中有自定义的幂等性注解,说明该接口需要验证幂等性(查看请求头里是否有key=token的值,如果有,并且删除成功,那么接口就访问成功,否则为重复提交;
- 如果发现该接口没有自定义的幂等性注解,则放行。
5-1-3-代码演示
1、使用的技术
- springBoot
- redis
- 自定义幂等性注解+拦截器请求拦截
- Jmeter压测工具
2、创建项目
3、导入pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springBoot-idempotent</artifactId>
<version