【面试】幂等性问题的思考和总结,防重、幂等,常用解决方案,解决方式

1、幂等性

幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
幂等性接口:是指可以使用相同参数重复执行,并能获得相同结果的接口。

数学中:在一次元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同;在二次元运算为幂等时,自己重复运算的结果等于它自己的元素。
计算机学中:幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因为重复执行对系统造成变化。

防重和幂等的区别:
防重设计主要为了避免产生重复数据,对接口返回没有太多要求。
而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

2、业务场景

● 1、加入购物车、操作购物车
● 2、创建订单
● 3、扣减库存
● 4、消息重复消费
● 5、重复发券
● 6、重复提交表单,添加了重复的记录

上述可以归类为三种:

2.1 前端重复提交

用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录。如果用户不小心多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建了多条记录。这就是接口没有幂等性带来的 bug。

2.2 接口超时重试

对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生了网络异常。这时再次调用时就会因为脏数据的存在而出现调用异常。

2.3 消息重复消费

在使用消息中间件来处理消息队列,且手动 ack 确认消息被正常消费时。如果消费者突然断开连接,那么已经执行了一半的消息会重新放回队列。
当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常,如数据库重复数据,数据库数据冲突,资源重复等。

3、解决方案

3.1 程序:全局id或token(前后端同时处理)

在提交的请求header中增加一个全局id或token,这个全局id或token需要提前向后端获取,提交的时候把这个id或token一起提交过来,全局判断id或token是否已经处理,来支持幂等。
实现:
【面试】幂等性问题的思考和总结,防重、幂等,常用解决方案,解决方式_第1张图片
具体流程步骤:

1. 客户端会先发送一个请求去获取 token,服务端会生成一个全局唯一的 ID 作为 token 保存在 redis 中,同时把这个 ID
返回给客户端
2. 客户端第二次调用业务请求的时候必须携带这个 token(放到Header里)
3. 服务端会校验这个 token,如果校验成功,则执行业务,并删除 redis 中的 token
4. 如果校验失败,说明 redis 中已经没有对应的 token,则表示重复操作,直接返回指定的结果给客户端

注意:

1. 对 redis 中是否存在 token 以及删除的代码逻辑建议用 Lua 脚本实现,保证原子性
2. 全局唯一 ID 可以用snowflake 雪花算法,美团 Leaf 算法百度的 uid-generator、美团的 Leaf 去生成

需要讨论的点:
1、是否全局拦截处理,所有Post请求的接口url都进行处理? 还是只处理产品提供的url?
2、后续新增的url是否自动添加到这个规则里面;
3、这个功能是否需要表维护,做成配置化;

3.2 数据库:实现主要是利用数据库表中主键唯一约束+唯一索引(后端处理)

通常数据库实现主要是利用数据库表中主键唯一约束+唯一索引的特性,如果主键唯一或者设置了复合唯一索引,在”插入“数据的时候就是幂等性操作。该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。
实现:

 alter table test add
 UNIQUE KEY `udx_uid_aid` (`userid`,`act_id`) USING BTREE

注意:
加了唯一索引之后,第一次请求数据可以插入成功。但后面的相同请求,插入数据时会报Duplicate entry ‘002’ for key 'order.etc_code异常,表示唯一索引有冲突。
虽说抛异常对数据来说没有影响,不会造成错误数据。但是为了保证接口幂等性,我们需要对该异常进行捕获,然后返回成功。
如果是java程序需要捕获:DuplicateKeyException异常,如果使用了spring框架还需要捕获:MySQLIntegrityConstraintViolationException异常。

3.3 数据库:悲观锁、乐观锁(后端处理)

悲观锁:认为别人每次去拿数据都会修改这条数据,所以每次拿数据的时候,都会使数据处于锁定状态。(该方案影响接口性能)
实现:

 select * from test where userid = 123 and act_id = 'spring' for update;

这里并没有使用主键 id 是查询,首先我们并不知道这条记录 id 值,所以我们通过 uid+aid 组合的唯一建作为锁表行记录条件,一定要使用主键或者唯一建,不然会将整张表都被锁住,那么其他的用户就无法操作了。

乐观锁:一般表字段增加版本号控制 version,即为数据增加一个版本标识、一般是通过为数据库表数据增加一个数字类型的“version”字段来实现。
实现:

 update user_account set amount=amount+100,version=version+1
 where id=123 and version=1;

更新数据的同时 version+1,然后判断本次 update 操作的影响行数,如果大于 0,则说明本次更新成功,如果等于 0,则说明本次更新没有让数据变更。当并发请求过来时,只需要拿到 select 的版本号,进行更新操作即可(where 可带上主键 id),保证幂等。推荐使用

3.4 分布式锁(后端处理)

Redis或Zookeeper
【面试】幂等性问题的思考和总结,防重、幂等,常用解决方案,解决方式_第2张图片

Redis主要有三种方式实现redis的分布式锁:
1. setnx命令
2. set命令
3. Redission框架

具体步骤:

1. 用户通过浏览器发起请求,服务端会收集数据,并且生成订单号code作为唯一业务字段。
2. 使用redis的set命令,将该订单code设置到redis中,同时设置超时时间。(set和expire要保证一起执行,避免中间出现问题导致死锁)
3. 判断是否设置成功,如果设置成功,说明是第一次请求,则进行数据操作。
4. 如果设置失败,说明是重复请求,则直接返回成功或请求处理中。 需要特别注意的是:分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费redis的存储空间,需要根据实际业务情况而定。也会对性能产生影响。这里就需要权衡性能和数据一致性的问题了。

传送门:解决重复请求导致数据出现重复问题,幂等性实现基于Redis,附代码述

3.5 前端优化:提交后load

-------------欢迎各位留言交流,如有不正确的地方,请予以指正。【Q:981233589】

你可能感兴趣的:(Java常见面试题,缓存技术,分布式,后端,幂等性,防重,架构)