ToB企服应用市场:ToB评测及商务社交产业平台

标题: 高并发下数据幂等题目的9种办理方案 [打印本页]

作者: 八卦阵    时间: 2024-5-15 03:29
标题: 高并发下数据幂等题目的9种办理方案
置顶阐明

严酷来说,所谓人云亦云的接口幂等性,大部分场景是要求接口防重或数据幂等,而不是接口幂等,许多人都搞混了。
举例:后端做了付出防重,用户对单一订单重复付出,再次付出不是提示付出成功(接口幂等是要求多次请求返回的结果一致),而是提示请勿重复付出。
许多时候是防重是包管MySQL表数据的幂等,而不是接口幂等。

接口幂等与接口防重

常规方案

前端实现方案(多用户防重)

耳熟能详的防抖:用户点击按钮后,可将按钮置灰几秒钟,一方面提示用户不能点了,一方面只让接口请求了一次,减轻服务器的压力。
验证码方案(常见于抢购的流量削峰)(多用户防重)

秒杀场景需要极致的性能优化,秒杀开始时的抢购按钮点击后,添加验证码功能,不同用户输入速度不一样。
一方面用于流量削峰,防止服务端瞬时负载过大挂掉(报502等)。
一方面可以防止用户狂点,影响接口幂等性,只要点按钮就蹦出来验证码。
但是这里小心有业务逻辑安全毛病,验证码是否正确或被绕过(黑客直接请求下单接口),都需要与卑鄙的业务逻辑保持线性关联。
低并发时,数据库的判断题目(单用户防重)

低并发也不要鄙视,有时就会阴沟里翻船。
我之前写过,邮政银行的内部员工营销项目(用的PostgreSQL数据库),当时思量到请求量不大,于是就没用redis去抗。
暗昧逻辑如下:表中查不到部分数据就新增,查到数据就提示,当时的业务场景还加不了唯一索引。
但是出现了相同数据的情况。
厥后一推理,是同一个人狂点(存在表中的create_at字段时间相同),此时多个请求都没有查询到表中有这个数据,就是所谓的趁PgSQL不注意,于是同一组数据都举行了新增操作,出现了bug。
也很好办理:
在用户写操作成功逻辑代码区的卑鄙中,添加,用redis的setex命令,将模块名拼接用户id作为key,设置3秒逾期,1作为value,用不上value,以是随便尝试。
上游代码:只要检测到有值,则给提示。
由于3秒的时间,充足数据库的insert操作了,还不用手动删除这个key。
伪代码如下:
  1. if(Redis::exists('模块名:' . $user_id)) {
  2.         return '操作频繁,请勿重复操作';
  3. }
  4. if(判断表中是否存在数据sql) {
  5.         return '您已提交,请明天再来';
  6. }
  7. 写操作SQL,将数据入库操作...
  8. if(写操作SQL执行失败) {
  9.         return '操作失败,请稍后重试';
  10. }
  11. Redis::setex('模块名:' . $user_id, 3, 1);
  12. return '操作成功';
复制代码
基于请求头数据(Token)的前置判断方案(单用户防重)

这种方式,适用于快速办理并全局办理幂等性的项目,高明手段。
但是覆盖率太广,需要根据请求的url,添加黑白名单的策略,就是说哪些接口要防重,哪些接口不能防重。
  1. if(当前请求的接口需要防重) {
  2.         $server = $_SERVER;
  3.         $user_temp_key = md5($server['REMOTE_ADDR'] . $server['User-Agent'] . $server['HTTP_RAND_STR']);
  4.         $user_temp_value = md5($_POST['post_data']);
  5.        
  6.         $cache = Redis::get($user_temp_key)
  7.         if($cache && ($cache === $user_temp_value)) {
  8.                 return '操作频繁';
  9.         }
  10.        
  11.         Redis::setex($user_temp_key, 3, $user_temp_value);
  12. }
复制代码
数据库唯一索引兜底方案(多用户防重)

添加唯一索引做兜底,就算并发绕过了业务逻辑,但使用会在唯一索引那边报错,然后返回给用户此次操作失败,从而包管接口幂等。
缺点是有些场景不能加唯一索引。
状态机判断方案(单用户防重)

比方订单状态,可能是1手动取消订单、2被动取消订单、3待付出,4待发货,5已发货,6代签收,7待评价,5已评价。
状态机的更新,假如不是递增的、不连续的、或者不变,也有可能是并发过来,或者是黑客攻击。
也可以在这一步做一些验证。
在付出回调等场景,根据订单状态的判断,在防止重复改状态,或者防止变更为不符合事务发展规律的状态时,很 重要。
高并发的方案

MySQL 可重复读的隔告别引发的幻读题目

场景:有些操作需要insert的事务,请求A中的事务a还未提交,此时又过来一个请求B,也就有了事务b,两者算是相同的数据举行insert,表中添加了唯一索引。
分析:为了包管防重,事务b insert时需要先查询有没有相同的数据,假如没有再举行插入,此时事务a还没有提交,事务b也就查询不到数据(能查到就是脏读,MySQL RR的隔离级别不会出现),于是举行了inset操作,结果导致事务b被壅闭(受事务a的行级X锁倾轧),等事务a提交后,事务b插入失败。
幻读:同一个事务里前后查询两次相同范围的数据,后一次查询查询到了前一次看不到的东西,这叫幻读。MySQL的机制,select没办法直接幻读,只能通过insert 插入相同的数据,达到唯一索引冲突的错误来证明。
办理:幻读的题目可以通过间隙锁或临键锁去壅闭,但是无法办理唯一约束冲突的报错题目。
唯一约束冲突的题目,看业务也是一项,假如重复是小概率事件,可以忽略。
假如概率挺大,只管不要让MySQL频仍报错,添加一个redis组件,在上次事务提交成功后,缓存提交数据的md5的值,与这次提交数据的md5的做个对比,假如一致,阐明有重复,制止了并发情况下,卑鄙唯一约束冲突的报错题目。
用空间换时间的方式。这样可以把题目引到上游,减轻MySQL服务器的压力,和报错数量。提升性
能。
可按照以下伪代码思路去优化(注意是优化,不是办理)
  1. $post_data = 'md5加密后的接口数据';
  2. $cache_data = Redis::get('key');
  3. if(($cache_data != null) && ($post_data === $cache_data) ) {
  4.         return '请勿重复提交';
  5. }
  6. 查询是否存在的防重提交SQL... //这一步是数据库防重的兜底策略。
  7. if(有重复) {
  8.         return '请勿重复提交';
  9. }
  10. 事务sql...
  11. if(事务回滚) {
  12.         return '操作失败,请稍后重试';
  13. }
  14. if(事务提交) {
  15.         Redis::setex('key', 3, 'md5加密提交的数据'); //3秒后过期,不用考虑占空间和维护问题。
  16. }
  17. return '操作成功';
复制代码
扩展:MySQL事务(4种事务隔离级别、脏写、脏读、不可重复读、幻读、当前读、快照读、MVCC、事务指标监控)
分布式锁(多用户防重)

分布式锁对于PHP而言,不常用。用的相对没有Java的多,并且PHP实现分布式锁缺少了一些机制,显得鸡肋。
用分布式锁,也可以办理上面的题目,但是会降低性能。
除非由于重复插入的报错非常多,否则不保举用。
但是有几点要注意:
悲观锁(多用户防重)

请阅读之前写过的文章:
MySQL锁(读锁、共享锁、写锁、S锁、排它锁、独占锁、X锁、表锁、意向锁、自增锁、MDL锁、RL锁、GL锁、NKL锁、插入意向锁、间隙锁、页锁、悲观锁、乐观锁、隐式锁、显示锁、全局锁、死锁)
乐观锁(多用户)

请阅读之前写过的文章。
MySQL乐观锁与悲观锁

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4