幂等性相关的讨论

# 幂等性相关的讨论 ## 什么是幂等性? ### 官方解释 > 接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的, 不会因为多次点击而产生了副作用; ### 个人理解 > 比如发送了一个A请求, A请求触发了B操作, 当发生网络延迟时, 用户会不自觉的频繁发送A请求, 但是这些A请求不再触发B操作, 这就是接口幂等性 > 这类比数学就像是1^N次方, 无论多少个1相乘, 结果都是1, 即如果有多少个A请求, 都只会执行一次B操作 ## 存在幂等性问题的场景 ### 1) 网络延迟 > 比如订单支付请求, 当发生网络延迟的时候, 用户极大可能发生多次点击的行为, 这就会让这个请求带来的操作执行多次, 发生幂等性问题 ### 2) 人为破坏 > 即对于当前项目而言, 可以不断刷新当前页, 点击订单支付, 这和1类似, 也会有幂等性问题 ### 3) 后端调用 > 当网络延迟比较高的时候, Feign调用会不断的重试, 这也会导致同一个操作执行多次, 也会存在幂等性问题 ## 涉及幂等性的操作-以数据库为例 ### 具有天然幂等性的操作 ```SQL SELECT * FROM TABLE_ANME WHERE FIELD = ?; ``` > 无论查询多少次, 不会改变数据库中的数据, 具有天然的幂等性 ```SQL DELETE FROM TABLE_NAME WHERE FIELD = ?; ``` > 如果进行条件删除, 一旦发生了这次删除, 没有其他操作打扰的前提下, 也是具有幂等性的, 因为, 这次删了, 后面不可能存在符合条件的数据了 ```SQL INSERT INTO TABLE_NAME(FIELD...) VALUES(VALUE...) ``` > 如果字段中存在唯一字段或主键字段, 一定具有天然幂等性, 因为成功插入一次后, 因为不可重复的特性, 不可能插入第二条, 因此具有天然幂等性 ```SQL UPDATE TABLE_NAME SET FIELD = 1 WHERE FIELD = ? ``` > 如果按照某个条件把字段设置为1或者某个值, 无论执行多少次, 最后的结果都是1或某个值, 具有天然幂等性 ### 不具有天然幂等性的操作 ```SQL INSERT INTO TABLE_NAME(FIELD...) VALUES(VALUE...) ``` > 如果字段里面没有唯一性键列或主键列, 重复的数据可以插入多次, 不具有幂等性 ```SQL UPDATE TABLE_NAME SET FIELD = FIELD + 1 WHERE FIELD = ? ``` > 该更新操作每一次都会使字段++, 如果多次调用, 结果最终不是固定的值, 不具有幂等性 ## 解决幂等性问题 ### Token机制 #### 机制介绍 > 服务器会预先将防重令牌响应给浏览器, 在浏览器真正的发送请求的时候, 会发生如下的过程 > 1. 请求携带防重令牌参数 > 2. 浏览器携带的防重令牌和后端的防重令牌比较 > 3. 若令牌匹配, 则进行执行, 最后再删除令牌 > 4. 若令牌不匹配, 则不执行 #### 服务器的令牌存储到哪? > 直接抛结论: 令牌应该存储到Redis里面 > 根据令牌的性质可知, 令牌属于时效性很强的数据, 因此, 令牌不需要被持久化, 存储到内存即可, 但是, 令牌不允许存储到本地内存, 如果令牌存储到服务器内存, 有一个很严重问题, 即, 如果是节点A响应并存储令牌, 负载均衡到节点B比较令牌, 这样就会导致一个操作都完成不了 > 为了让分布式场景下能够正常的执行, 令牌应该存储到Redis中 #### 令牌的删除要点 ##### 操作后删除令牌? > 说在前面: 操作后删除令牌是错误的, 是危险的 > 因为, 当我们比较令牌成功并执行的时候, 我们需要等到执行完才能删除令牌, 那么, 如果执行的时间比较长, 那么在这段时间内, 其他请求都可以匹配成功令牌, 该操作会被执行多次, 仍然会存在幂等性问题, 因此, **不可以使用后置删除令牌** > 也可能突然闪退, 不能成功删除令牌 ###### 操作前删除令牌 > 说在前面: 操作前删除令牌是不完全正确的 > 因为我们成功比较完令牌后, 之后是要删除令牌的, 但是在这个时间段内, 可能其他线程页经历了比较成功并删除令牌的操作, 这会导致同一时间内多个线程比较成功, 也会导致幂等性问题 > 前置删除还有一个问题, 就是删除令牌后可能系统崩溃, 导致无法操作的问题出现 > **而该幂等性问题的本质原因是, 获取令牌, 比较令牌, 删除令牌没有保证他们的原子性, 因此, 保证原子性可以解决幂等性问题** ##### 总结 > 结合前面的两者, 我们宁愿什么都不干也不要干错, 即宁愿不扣钱也不愿意扣多, 因此, 我们需要前置删除, 加上原子性操作, 幂等性问题可以根本解决 ### 数据库怎么解决幂等性问题 #### 悲观锁 > 悲观锁其实并不可以解决幂等性问题, 因为悲观锁只限制了并发, 并不能对版本进行校验, 也不能判断该操作是否重复了, 除非进行双端检锁, 太麻烦了 > 悲观锁不适合用于幂等性问题的解决 #### 乐观锁 > 乐观锁非常的适合使用幂等性问题, 因为乐观锁的实现是通过版本号来实现的, 即因为多个请求想要修改的时候, 刚开始的版本号都是一样的, 一旦发生改变, 版本号就会更新, 后面的操作都会比较失败, 更新失败, 因此乐观锁非常适合作为解决幂等性问题 #### 主键列或唯一性键列 > 对于这些字段, 插入操作都只能插入一次, 因为它们是唯一的, 后面的均为失败, 因此完美解决幂等性问题 ### MD5机制 #### 机制介绍 > 比如一个请求需要操作数据A, 在操作之前会计算该数据的MD5值, 并存储到Redis的set中, 当下一个请求的时候, 会检查操作的数据的MD5是否在set中存在, 如果存在则说明该数据操作过了, 不再操作, 否则操作, 解决了幂等性问题, 利用了MD5的强抗干扰性 > 针对结果上的数据 ### 防重表机制 > 当我们进行业务操作的时候, 我们插入一个唯一性索引字段, 如果插入成功则继续进行, 否则不操作, 在当前项目中可以采用订单号 > 针对参数上的数据 ### 唯一ID机制 > 为每一个请求都赋值一个唯一ID, 如果该请求执行过了, 将ID存储起来, 如果多次发送该请求, ID已存在, 不会再被执行 > 针对请求, 很想防重令牌