幂等性设计,及案例分析

news2024/11/26 7:36:39

一、redis锁处理幂等性失效

在这里插入图片描述

上面代码中,锁起不了作用;
——count方法,和insert方法在同一事务中,事务中包含锁,锁没有作用,锁的范围内,事务没提交,但释放锁后,事务提交前,被另一线程抢了执行权后,因为事务还没提交,另一线程拿到的count还是0。

以上代码问题:

  1. 对事物的理解使用有问题,幂等设计bug;
  2. redis锁使用有问题(单独案例讲述);

mysql默认事务级别——可重复读;
锁加错位置了,锁应该加在这个事务方法的外面;
正例:
在这里插入图片描述

stop the world:
学会用stop the world注释代码。

1.1 扩展:

事务在生产实践中经常犯的错误:

  • 事务范围:应该加入事务的代码未加入到事务中

1.1.1 图是另一个真实生产当中的事故-仅供参考:

在这里插入图片描述

  IdGenerator 是一个生成唯一标识符的工具类。它通常用于生成数据库表中的主键值,例如AUTO_INCREMENT 字段。

  • 事务大小:事务过大,是否有必要拆解小事务(如何优化),拆解后一致性问题。

传播范围(异常标注):

  • 多线程中不可传播;
  • 多个方法内如果异常被捕获将要被标记为异常事务,不可以再次提交(虽然不影响数据,但是有报错信息);

二、Transaction rolled back bacause it has been marked as rollback-only问题原因复盘

2.1 复盘

在这里插入图片描述

在这里插入图片描述

错误原因:

提交了一个被标记为异常的事务,会报这个错。

解决方法:

  • a处try-catch代码去掉;
  • 或者,b处@Transactional注解去掉;

无论是哪种解决方法,具体看业务。

三、mysql死锁场景

  • 问题1:jvm如果死锁了,java进程还在吗?——一直锁着。
  • 问题2:mysql如果死锁了,其他连接还能正常运行吗?——死锁一段时间后会自动释放,可配置;

3.1 mysql死锁复盘

在这里插入图片描述
在 MySQL 中,FOR UPDATE 子句用于在读取数据时锁定该记录,以防止其他事务同时更新或删除该记录。当多个事务试图同时锁定同一记录时,可能会导致死锁。
下面是一个可能导致死锁的场景:

假设有两个事务 T1 和 T2,它们都试图更新同一行数据。

  • 事务 T1 执行以下操作:
    • 读取一行数据并加上 FOR UPDATE 锁。
    • 等待一段时间(例如,进行一些计算或等待其他资源)。
  • 事务 T2 执行以下操作:
    • 读取同一行数据并加上 FOR UPDATE 锁。
    • 试图更新该记录,但由于 T1 已经锁定了该记录,因此事务 T2 被阻塞。 现在,事务 T1 等待一>段时间后准备更新记录,但由于事务 T2 已经锁定了该记录,因此事务 T1 也被阻塞。

这就形成了一个死锁,因为两个事务都在等待对方释放锁,而它们都无法继续执行下去。

为了避免死锁,可以采取以下措施:

  • 尽量减少锁定的时间,以避免其他事务长时间等待。
  • 按照相同的顺序访问数据,以避免冲突。

mysql死锁时间长好还是短好?
——短的话,不好控制长事务;长的话,发生死锁时,时间等待太久;

四、幂等性设计方法

4.1 幂等性设计:

  1. 有时我们在填写某些 form表单 时,保存按钮不小心快速点了两次,表中竟然产生了两条重复的数据,只是id不一样;
  2. 我们在项目中为了解决 接口超时 问题,通常会引入了 重试机制 。第一次请求接口超时了,请求方没能及时获取返回结果(此时有可能已经成功了),为了避免返回错误的结果(这种情况不可能直接返回失败吧?),于是会对该请求重试几次,这样也会产生重复的数据;
  3. mq消费者在读取消息时,有时候会读取到 重复消息 ,如果处理不好,也会产生重复的数据;

这些都是幂等性问题。

接口幂等性: 是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

这类问题多发于接口的:

  • insert 操作,这种情况下多次请求,可能会产生重复数据;
  • update 操作,如果只是单纯的更新数据,比如: update user set status=1 where id=1 ,是没有问题的。如果还有计算,比如: update user set status=status+1where id=1 ,这种情况下多次请求,可能会导致数据错误;

那么我们要如何保证接口幂等性?请往下看。

4.1.1 insert前先select

 通常情况下,在保存数据的接口中,我们为了防止产生重复数据,一般会在 insert 前,先根据 name 或 code 字段 select 一下数据。如果该数据已存在,则执行 update 操作,如果不存在,才执行 insert 操作。
在这里插入图片描述
 该方案可能是我们平时在防止产生重复数据时,使用最多的方案。但是该方案不适用于并发场景,在并发场景中,要配合其他方案一起使用,否则同样会产生重复数据。

4.1.2 加悲观锁

4.1.2.1 支付场景

 支付场景在加减库存场景中,用户A的账号余额有150元,想转出100元,正常情况下用户A的余额只剩50元。一般情况下,sql是这样的:

update user amount = amount-100 where id=123;

 如果出现多次相同的请求,可能会导致用户A的余额变成负数。这种情况,用户A来可能要哭了。于此同时,系统开发人员可能也要哭了,因为这是很严重的系统bug。
 为了解决这个问题,可以加悲观锁,将用户A的那行数据锁住,在同一时刻只允许一个请求获得
锁,更新数据,其他的请求则等待。

通常情况下通过如下sql锁住单行数据:

select * from user id=123 for update;

条件:数据库引擎为innoDB

操作位于事务中
具体流程如下:
在这里插入图片描述
具体步骤:

  1. 多个请求同时根据id查询用户信息。
  2. 判断余额是否不足100,如果余额不足,则直接返回余额不足。
  3. 如果余额充足,则通过for update再次查询用户信息,并且尝试获取锁。
  4. 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会。
  5. 第一个请求获取到锁之后,判断余额是否不足100,如果余额足够,则进行update操作。
  6. 如果余额不足,说明是重复请求,则直接返回成功。
4.1.2.1 操作库场景
select* from stock_info where goods_id=12312 and storage_id=1 for update;

具体流程:

a:单件货品操作流程:

在这里插入图片描述

b:(同一个goodsId)多个单件货品,批量操作出库流程:

在这里插入图片描述
具体步骤:

  1. 多个请求同时根据goodsId和storageId操作货品的上下架,或者其他渠道订单批量下架操作;
  2. 判断当前货品是否有仓库货品;
  3. 如果货品库存充足,则通过for update再次查询货品库存信息,并且尝试获取锁;
  4. 只有第一个请求能获取到行锁,其余没有获取锁的请求,则等待下一次获取锁的机会;
  5. 第一个请求获取到锁之后,进行货品单件明细状态变更,成功后操作,则进行update操作加减库存;
  6. 如果库存不足或者单件不满足操作,则直接返回成功或者幂等状态。

 需要特别注意的是:如果使用的是mysql数据库,存储引擎必须用innodb,因为它才支持事 务。此外,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。

 悲观锁需要在同一个事务操作过程中锁住一行数据,如果事务耗时比较长,会造成大量的请求等待,影响接口性能。此外,每次请求接口很难保证都有相同的返回值,所以不适合幂等性设计场景,但是在防重场景中是可以的使用的。在这里顺便说一下, 防重设计幂等设计 ,其实是有区别的。防重设计主要为了避免产生重复数据,对接口返回没有太多要求。而幂等设计除了避免产生重复数据之外,还要求每次请求都返回一样的结果。

4.1.3 加乐观锁

 既然悲观锁有性能问题,为了提升接口性能,我们可以使用乐观锁。需要在表中增加一个timestamp 或者 version 字段,这里以 version 字段为例。
在更新数据之前先查询一下数据:

select id,amount,version from user id=123;

中间就省略了,相信大家也知道。直接贴出sql中的乐观锁代码了:

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

需要注意的是,如果影响行数为0:

 该 update 操作不会真正更新数据,最终sql的执行结果影响行数是 0 ,因为 version 已经变成 2了, where
中的 version=1 肯定无法满足条件。但为了保证接口幂等性,接口可以直接返回成功,因为 version
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

在这里插入图片描述
具体步骤:

  1. 先根据id查询用户信息,包含version字段;
  2. 根据id和version字段值作为where条件的参数,更新用户信息,同时version+1;
  3. 判断操作影响行数,如果影响1行,则说明是一次请求,可以做其他数据操作;
  4. 如果影响0行,说明是重复请求,则直接返回成功;

4.1.4 加唯一索引

 常规的创建唯一索引,和唯一联合索引的思路就不写了。

4.1.4.1 软删除可能引发的问题:

 在很多业务场景中,都使用“软删除”即使用flag或is_deleted等字段表示记录是否被删除,这种方式能很好地保存“历史记录”,但由于”历史记录”的存在,导致无法在表上建立唯一索引,需要通过程序来控制”数据唯一性”,其中一种程序实现逻辑就是“先尝试更新,更新失败则插入”,该方式在高并发下死锁频发。(select for update ;为什么?你能复现么?如何避免?)

 尽管可以通过程序来控制”数据唯一性”,但仍建议使用数据库级别的唯一约束来确保数据在表级别的”唯一”,对于”硬删除”方式,直接在唯一索引列上建立为唯一索引即可,对于”软删除”方式,可以通过 复合索引 方式来处理。

 假设当前有订单相关的表tb_order_worker,表中有order_id字段需要唯一约束,使用is_delete字段来标识记录是否被”软删除”,is_delete=1时表示记录被删除,is_delete=0时表示记录未被删除,需要控制满足is_delete=0时的记录中order_id唯一,如果对(order_id,is_delete)的建唯一索引,那么当同一订单被多次”软删除”时就会出现唯一索引冲突的问题。

解决方式一:

 提升is_delete列的取值范围,当is_delete=0时表示记录有效,当is_delete>0时表示记录被删除,在删除记录时将is_delete值设置为不同数值,只要确保相同order_id的记录使用不同数值即可(很多表都使用自增主键,可以取自增主键的值来作为is_delete值)。

解决方式二:

 新增列order_rid来保持方式一中is_delete的原有取值范围,当is_delete时设置order_rid=0,当is_delete=1时设置order_rid为任意非0值,只要确保相同order_id的记录使用不同值即可(同样建议参照自增主键值来设置),然后对(order_id,yn,order_rid)建唯一索引。

4.1.4.2 唯一索引和普通索引的区别?
4.1.4.2.1 查询
select * from t_user where id_card =1000;
  • 对于普通索引来说,查找到满足条件的第一个记录(1,1000)后,需要查找下一个记录,直到碰到第一个不满足id_card=1000条件的 记录;
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索

 性能差距微乎其微,因为mysql 数据是按照数据页为单位的,也就是说,当读取一条数据的时候,会将当前数据所在页都读入到内存,普通索引无非多了一次判断是否等于 的操作,相当于指针的寻找和一次计算,当然,如果该页码上,id_card=1000是最后一个数据,那么就需要取下一个页了,但是这种概率并不大。

 总结说,查询上,普通索引和唯一索引性能是没什么差异的

4.1.4.2.2 更新

 当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致 性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。在下次查询 需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证 这个数据逻辑的正确性。

这个change buffer通常被称为InnoDB的写缓冲?

 在MySQL5.5之前,叫插入缓冲(insert buffer),只针对insert做了优化;现在对delete和update也有效,叫做写缓冲(change buffer)。 它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进 行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(buffer changes),等未来数据被读取时,再将数据合并(merge)恢复到缓冲池中的技术。

写缓冲的目的是降低写操作的磁盘IO,提升数据库性能:

  对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入 (1,1000)这个记录,就要先判 断现在表中是否已经存在id_card=1000的记录,而这必须要将数据 页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用change buffer了。 因此,唯一索引的更新就不能使用change buffer,实际上也只有普通索引可以使用。

接着分析InnoDB更新流程:
处理流程如下:

  • 对于唯一索引来说,找到999和1001之间的位置,判断到没有冲突,插入这个值,语句执行结 束;
  • 对于普通索引来说,找到999和1001之间的位置,插入这个值,语句执行结束。

这样看来,普通索引和唯一索引对更新语句性能影响的差别,只是一个判断,只会耗费微小的 CPU时间。

真正影响性能的是第二种情况是,这个记录要更新的目标页不在内存中。处理流程如下:

  • 对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
  • 对于普通索引来说,则是将更新记录在change buffer,语句执行就结束了。
4.1.4.2.3 总结

 将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。change buffer因为减少了随机磁盘访问, 所以对更新性能的提升是会很明显的。

 因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好。

 这种 业务模型常见的就是账单类、日志类的系统。

 反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在change buffer,但之 后由于马上要访问这个数据页,会立即触发merge过程。这样随机访问IO的次数不会减少,反而增加了change buffer的维 护代价。所以,对于这种业务模式来说,change buffer反而起到了副作用。

 redo log主要节省的是随机写磁盘的IO消耗(转成 顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。

4.1.4.2.4 Change buffer为什么只对非唯一普通索引页有效
  • 主键索引,唯一索引
    实际上对于【唯一索引】的更新,插入操作都会先判断当前操作是否违反唯一性约束,而这个操作就必须要将索引页读取到内存中,此时既然已经读取到内存了,那直接更新即可,没有需要在用Change buffer了。

  • 非唯一普通索引
    不需要判断当前操作是否违反唯一性约束,也就不需要将数据页读取到内存,因此可以直接使用 change buffer 更新。

基于此,Change buffer只有对普通索引可以使用,对唯一索引的更新无法生效

change buffer参考文章:MySQL十七:Change Buffer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1161048.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云安全与容器安全: 探讨在云环境和容器化应用中如何保护数据和工作负载的安全。

在当今数字化时代,云计算和容器化应用已经成为了企业业务的主要组成部分。这两项技术的普及,极大地提高了开发和部署的效率,但也带来了新的安全挑战。在本文中,我们将探讨云安全和容器安全的重要性,以及如何有效地保护…

WordPress外链页面安全跳转插件

老白博客我参照csdn和腾讯云的外链跳转页面,写了一个WordPress外链安全跳转插件:给网站所有第三方链接添加nofollow标签和重定向功能,提高网站安全性。插件包括两个样式,由于涉及到的css不太一样,所以分别写了两个版本…

Spring MVC (Next-1)

1.Restful请求 restFul是符合rest架构风格的网络API接口,完全承认Http是用于标识资源。restFul URL是面向资源的,可以唯一标识和定位资源。 对于该URL标识的资源做何种操作是由Http方法决定的。 rest请求方法有4种,包括get,post,put,delete.分别对应获取…

低代码可视化逻辑编排工具:JNPF

目录 Intro 一、是什么? 提供自动化的解决方案 二、为什么受欢迎? JNPF自身特点——安全、方便、高效、低耗 对于企业,更“安全” 成本“最低”,效率“最高” 三、JNPF开发平台功能展示 技术介绍 参考地址 近几年,随着…

Tomcat下载地址(详细)

Apache Tomcat - Apache Tomcat 8 Software Downloadshttps://tomcat.apache.org/download-80.cgi2.找到Archives 3.选择下载的把版本 4.选择具体下载那个版本 5. 6.一般选择tar.gz结尾的压缩包

飞桨国际化应用案例:挪威广告企业Adevinta应用PaddleOCR提质增效

Adevinta,位于挪威奥斯陆的跨国在线分类广告公司,以其全球市场的图像处理API为特色。Adevinta的主要使命是构建全球买家和卖家之间的桥梁,其在线市场运营覆盖11个国家,拥有众多备受信任的品牌,如荷兰的marktplaats、德…

Stream 流对象的创建与各方法

Stream 流对象的创建与各方法 目录 1.0 Stream 流的说明 2.0 Stream 流对象的创建 2.1 对于 Collection 系列集合创建 Stream 流对象的方式 2.2 对于 Map 系列集合创建 Stream 流对象的方式 2.3 对于数组创建 Stream 流对象的方式 3.0 Stream 流的中间方法 3.1 Stream 流的 …

Jupyter notebook如何加载torch环境

默认你已经安装了anaconda 和 pytorch 环境。 1,必须要以管理员身份打开 Anaconda prompt终端, 2,进入pytorch环境中: conda activate pytorch_393,安装必要插件: (1)conda inst…

【力扣】2003. 每棵子树内缺失的最小基因值

【力扣】2003. 每棵子树内缺失的最小基因值 文章目录 【力扣】2003. 每棵子树内缺失的最小基因值1. 题目介绍2. 思路3. 解题代码4. Danger参考 1. 题目介绍 有一棵根节点为 0 的 家族树 ,总共包含 n 个节点,节点编号为 0 到 n - 1 。 给你一个下标从 0…

国密SM算法及实现加密和解密

一 引入pom <dependency><groupId>com.antherd</groupId><artifactId>sm-crypto</artifactId><version>0.3.2</version></dependency> 二 代码实现 package com.example.ytyproject.component;import com.antherd.smcrypto.…

[架构之路-250/创业之路-81]:目标系统 - 纵向分层 - 企业信息化的呈现形态:常见企业信息化软件系统 - 企业内的数据与数据库

目录 一、数据概述 1.1 数据 1.2 企业信息系统的数据 1.3 大数据 1.4 数据与程序的分离思想 1.5 数据与程序的分离做法 1.6 数据库的基本概念 1.7 企业数据来源 1.8 企业数据架构 二、常见的数据库类型 2.1 数据库分类 2.1 数据库类型 2.2 常见的数据库类型、应用…

在前端实现小铃铛上展示消息

点击铃铛显示如下消息框&#xff1a; 如果点击消息&#xff0c;可以实现消息从列表中移除,并从铃铛总数上进行扣减对应的已读消息数。 关于以上功能的实现方式&#xff1a; <!-- 铃铛位置 --><i class"el-icon-bell" click"showPopover true"&…

torch_geometric,scatter,sparse, cluster的安装失败

首先&#xff0c;对于自己的电脑环境是 已将安装3.9版本的python&#xff0c;成功安装11.6版本的cuda和1.31.1版本的torch。 现在想要安装torch_geometric&#xff0c; -需要先安装scatter&#xff0c;sparse&#xff0c; cluster。 直接安装失败&#xff0c;报错如下&…

小程序https证书

小程序通常需要与服务器进行数据交换&#xff0c;包括用户登录信息、个人资料、支付信息等敏感数据。如果不使用HTTPS&#xff0c;这些数据将以明文的方式在网络上传输&#xff0c;容易被恶意攻击者截获和窃取。HTTPS通过数据加密来解决这个问题&#xff0c;确保数据在传输过程…

make工具的介绍,包含的显示/隐晦规则/变量定义/文件指示,使用,.PHONY的介绍+原理

目录 make--自动化构建工具 引入 介绍 包含 显式规则 隐晦规则 变量定义 文件指示 注释 使用 test:test.c .PHONY 介绍 作用 示例 原理 示例 介绍 make--自动化构建工具 引入 在软件开发过程中&#xff0c;通常需要编译、链接和构建大量的源代码文件如果全…

FPGA 如何 固化程序到 FLASH中

1、导出Hardware 2、导出bit文件 3、打开SDK 4、 点击Ok 5、创建工程 6、 输入工程名称&#xff1a;guhua 7、选择 Zynq FSBL 8、单击 guhua、然后点击 build 点击&#xff1a;build all 9、 右键之后&#xff0c;点击&#xff1a;Creat Boot Image 10、点击 Cr…

栈及其栈的模拟实现和使用

1. 栈(Stack) 1.1 概念 栈 &#xff1a;一种特殊的线性表&#xff0c;其 只允许在固定的一端进行插入和删除元素操作 。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO &#xff08; Last In First Out &#xff09;的原则…

【计算机网络】计算机网络中的基本概念

文章目录 局域网LAN基于网线直连基于集线器组建基于交换机组建基于交换机和路由器组建 广域网WANIP地址端口号协议为什么要有协议知名协议的默认端口 五元组协议分层TCP/IP五层模型封装和分用 网络互连就是将多台计算机连接在一起&#xff0c;完成数据共享。数据共享本质是网络…

echarts实现圆形进度图

echarts实现圆形进度图 效果图 话不多说&#xff0c;代码如下 option {title: {text: 本月功率因数,textStyle: {color: #666666,fontSize: 14},subtext: 0.95,subtextStyle: {color: #161616,fontSize:30,fontWeight:700},itemGap: 15, // 主副标题距离left: center,top:…

竞赛知识点11【线段树】

文章目录 一、概念二、基本操作2.1、建树2.2、区间询问操作2.3、单点修改2.4、区间修改一、概念 线段树是用一种树状结构来存储一个连续区间的信息的数据结构。 它主要用于处理一段连续区间的插入,查找,统计,查询等操作。 复杂度: 设区间长度是 n n n,所有操作的复杂度是 l…