高并发下数据幂等问题的9种解决方案

这篇具有很好参考价值的文章主要介绍了高并发下数据幂等问题的9种解决方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

置顶说明

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

接口幂等与接口防重

  • 接口幂等:对于一个接口进行多次请求,服务器响应的结果一致。
  • 接口防重:对于一个接口进行多次请求,服务器不会产生额外的副作用,或产生业务逻辑意料之外的情况。

常规方案

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

耳熟能详的防抖:用户点击按钮后,可将按钮置灰几秒钟,一方面提示用户不能点了,一方面只让接口请求了一次,减轻服务器的压力。

验证码方案(常见于抢购的流量削峰)(多用户防重)

秒杀场景需要极致的性能优化,秒杀开始时的抢购按钮点击后,添加验证码功能,不同用户输入速度不一样。
一方面用于流量削峰,防止服务端瞬时负载过大挂掉(报502等)。
一方面可以防止用户狂点,影响接口幂等性,只要点按钮就蹦出来验证码。

但是这里小心有业务逻辑安全漏洞,验证码是否正确或被绕过(黑客直接请求下单接口),都需要与下游的业务逻辑保持线性关联。

低并发时,数据库的判断问题(单用户防重)

低并发也不要小看,有时就会阴沟里翻船。

我之前写过,邮政银行的内部员工营销项目(用的PostgreSQL数据库),当时考虑到请求量不大,于是就没用redis去抗。
模糊逻辑如下:表中查不到部分数据就新增,查到数据就提示,当时的业务场景还加不了唯一索引。
但是出现了相同数据的情况。
后来一推理,是同一个人狂点(存在表中的create_at字段时间相同),此时多个请求都没有查询到表中有这个数据,就是所谓的趁PgSQL不注意,于是同一组数据都进行了新增操作,出现了bug。

也很好解决:
在用户写操作成功逻辑代码区的下游中,添加,用redis的setex命令,将模块名拼接用户id作为key,设置3秒过期,1作为value,用不上value,所以随便尝试。
上游代码:只要检测到有值,则给提示。

因为3秒的时间,足够数据库的insert操作了,还不用手动删除这个key。
伪代码如下:

if(Redis::exists('模块名:' . $user_id)) {
	return '操作频繁,请勿重复操作';
}

if(判断表中是否存在数据sql) {
	return '您已提交,请明天再来';
}

写操作SQL,将数据入库操作...

if(写操作SQL执行失败) {
	return '操作失败,请稍后重试';
}

Redis::setex('模块名:' . $user_id, 3, 1);

return '操作成功';

基于请求头数据(Token)的前置判断方案(单用户防重)

这种方式,适用于快速解决并全局解决幂等性的项目,高明手段。
但是覆盖率太广,需要根据请求的url,添加黑白名单的策略,就是说哪些接口要防重,哪些接口不能防重。

  • 在不用登录的业务场景下防重:
    请求头可以获取,客户端的IP、UA数据,两者结合,基本可以区分不同用户,但是有误差。
    在提交接口时,让前端生成一个随机字符串,并保存到LocalStorage,跟随接口提交,后端无需验证字符串,但3者一结合,基本能确定是一个用户。
    将3者拼接后计算hash散列值(省空间)作为redis的key
    将用户提交的数据的hash散列值(省空间)作为key对应的value。
    将redis过期时间设置为3秒。
    若用户重复提交就能检测出这个值,并且3秒过期,不用维护这块的数据。
    伪代码如下

if(当前请求的接口需要防重) {
	$server = $_SERVER;
	$user_temp_key = md5($server['REMOTE_ADDR'] . $server['User-Agent'] . $server['HTTP_RAND_STR']);
	$user_temp_value = md5($_POST['post_data']);
	
	$cache = Redis::get($user_temp_key)
	if($cache && ($cache === $user_temp_value)) {
		return '操作频繁';
	}
	
	Redis::setex($user_temp_key, 3, $user_temp_value);
}
  • 在用登录的业务场景下防重:
    有用户的令牌,接口能获取到,在项目中的前置中间件添加类似以上的逻辑,把redis的key换成token即可,也是一种方案。

数据库唯一索引兜底方案(多用户防重)

添加唯一索引做兜底,就算并发绕过了业务逻辑,但使用会在唯一索引那里报错,然后返回给用户此次操作失败,从而保证接口幂等。
缺点是有些场景不能加唯一索引。

状态机判断方案(单用户防重)

例如订单状态,可能是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服务器的压力,和报错数量。提升性
能。

可按照以下伪代码思路去优化(注意是优化,不是解决)

$post_data = 'md5加密后的接口数据';
$cache_data = Redis::get('key');

if(($cache_data != null) && ($post_data === $cache_data) ) {
	return '请勿重复提交';
}

查询是否存在的防重提交SQL... //这一步是数据库防重的兜底策略。
if(有重复) {
	return '请勿重复提交';
}



事务sql...

if(事务回滚) {
	return '操作失败,请稍后重试';
}

if(事务提交) {
	Redis::setex('key', 3, 'md5加密提交的数据'); //3秒后过期,不用考虑占空间和维护问题。
}

return '操作成功';

扩展:MySQL事务(4种事务隔离级别、脏写、脏读、不可重复读、幻读、当前读、快照读、MVCC、事务指标监控)

分布式锁(多用户防重)

分布式锁对于PHP而言,不常用。用的相对没有Java的多,并且PHP实现分布式锁缺少了一些机制,显得鸡肋。
用分布式锁,也可以解决上面的问题,但是会降低性能。
除非因为重复插入的报错非常多,否则不推荐用。

但是有几点要注意:

  • 一定要在事务提交后在释放分布式锁,如果在否则事务提交前释放,那其它请求就可以拿到分布式锁,进而提交事务,仍旧可能遇到上面一样的问题。
  • 不要让事务包含分布式锁,否则事务因为分布式锁的阻塞,而阻塞当前事务。其它事务过来也会因为这个问题,阻塞到那里占用MySQL连接资源。应当反过来,分布式锁包含事务。
  • 不要锁错对象了,分布式锁锁的是事务,是查询。

悲观锁(多用户防重)

  • 原理:就是同一时间,MySQL只允许一个写请求改某个部位的数据。
  • 补充:加X锁获得最新的数据,防止被改动,然后去更新它,其它的请求被阻塞(等待)。
  • 注意:加锁最好根据主键或者唯一索引列,避免锁住更多的数据,要降低锁的粒度。并且有性能问题。

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

乐观锁(多用户)

  • 原理:先查询出版本号,并将版本号作为where条件,联合其它where条件去更新(并更新版本字段),如果受影响函数为0,就说明数据没被改动,需要再次查询后更新,如此往复,直到有重试次数的干预,或者受影响行数>0。
  • 补充:如果select语句,查不到数据,可能是数据被删除了,后面的update也就没必要执行了。
  • 注意:谨防乐观锁的ABA问题。至于update受影响行数为0,是否正常返回或者重试,看业务。

请阅读之前写过的文章。
MySQL乐观锁与悲观锁文章来源地址https://www.toymoban.com/news/detail-842698.html

到了这里,关于高并发下数据幂等问题的9种解决方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • mysql高并发下主键自增打来的问题

    在一般情况下,在新增领域对象后,都需要获取对应的主键值。使用应用层来维护主键,在一定程度上有利于程序性能的优化和应用移植性的提高。在采用数据库自增主键的方案里,如果JDBC驱动不能绑定新增记录对应的主键,就需要手工执行查询语句以获取对应的主键值,对

    2024年02月13日
    浏览(9)
  • 08 - 网络通信优化之IO模型:如何解决高并发下IO瓶颈?

    08 - 网络通信优化之IO模型:如何解决高并发下IO瓶颈?

    提到 Java I/O,相信你一定不陌生。你可能使用 I/O 操作读写文件,也可能使用它实现 Socket 的信息传输…这些都是我们在系统中最常遇到的和 I/O 有关的操作。 我们都知道,I/O 的速度要比内存速度慢,尤其是在现在这个大数据时代背景下,I/O 的性能问题更是尤为突出,I/O 读写

    2024年02月12日
    浏览(16)
  • 高并发下缓存失效问题(穿透、雪崩、击穿),以及本地锁、Redis分布锁、Redisson锁、SpringCache使用

    高并发下缓存失效问题(穿透、雪崩、击穿),以及本地锁、Redis分布锁、Redisson锁、SpringCache使用

    说明 :以不存在的数据攻击,数据库压力增加导致崩溃 风险 :利用不存在数据攻击,数据库瞬时压力增大,导致崩溃 解决 :设置不存在数据为 null 值 与 短暂过期时间 布隆过滤器 使用案例: redission布隆过滤器解决缓存穿透问题,定时刷新bloomFilter中的数据 说明 :设置缓

    2024年02月08日
    浏览(14)
  • 深入理解高并发下的MySQL与Redis缓存一致性问题(增删改查数据缓存的一致性、Canal、分布式系统CAP定理、BASE理论、强、弱一致性、顺序、线性、因果、最终一致性)

    一些小型项目,或极少有并发的项目,这些策略在无并发情况下,不会有什么问题。 读数据策略:有缓存则读缓存,然后接口返回。没有缓存,查询出数据,载入缓存,然后接口返回。 写数据策略:数据发生了变动,先删除缓存,再更新数据,等下次读取的时候载入缓存,

    2024年03月20日
    浏览(18)
  • Redis 高并发下的性能优化技术

    Redis是一款高性能的键值存储数据库。与传统的数据库相比,Redis拥有更高的读写速度以及更丰富的数据结构支持。 尽管Redis拥有出色的性能表现,但在高并发场景下其仍存在着一些性能问题需要我们注意。本文将围绕这些问题进行分析,并提供一些解决方案。 Redis采用单进程

    2024年02月10日
    浏览(8)
  • 模拟高并发下RabbitMQ的削峰作用

    模拟高并发下RabbitMQ的削峰作用

            在并发量很高的时候,服务端处理不过来客户端发的请求,这个时候可以使用消息队列,实现削峰。原理就是请求先打到队列上,服务端从队列里取出消息进行处理,处理不过来的消息就堆积在消息队列里等待。 可以模拟一下这个过程:         发送方把10万条

    2024年02月11日
    浏览(11)
  • 高并发下的缓存击穿、雪崩、穿透和分布式锁(三)

    概念: 去查询缓存和数据库都不存在的数据,然后大量请求不存在的数据,导致数据库压力过大崩溃。 解决方案: 把不存在的数据null存入缓存,并给个短期的过期时间。 概念: 缓存采用相同的过期时间,然后在某一时刻会同时过期,然后请求全部访问数据库,导致数据库

    2024年02月07日
    浏览(11)
  • 短信发送+实现高并发下高可用(HTTP连接池+异步)

    依赖注入 application.properties SmsComponent SmsConfig RestTemplateConfig(使用HTTP协议请求) 测试 异步配置 使用异步

    2024年02月09日
    浏览(7)
  • 【HBZ分享】高并发下Redis+Nginx+Lua+Canal架构体系设计

    设计简单,可以支持普通并发现的大部分需求,但如果并发太高,该方案依然无法支撑。 瓶颈卡在tomcat的并发量低 通过lua直连redis nginx判断是否从nginx中直接获取缓存,如果需要获取,则通过lua直接去redis读取,并返回,整个过程不需要经历应用程序这层,所以性能很高,单

    2024年02月12日
    浏览(35)
  • Kafka数据重复问题解决方案

    通常,消息消费时候都会设置一定重试次数来避免网络波动造成的影响,同时带来副作用是可能出现消息重复。 幂等性指: 幂等性使用示例: 为了更好理解,需要了解下Kafka幂等机制 这种设计针对解决了两个问题: 那什么时候该使用幂等: 事务使用示例:分为生产端 和

    2024年02月07日
    浏览(10)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包