幂等性设计,及案例分析

这篇具有很好参考价值的文章主要介绍了幂等性设计,及案例分析。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、redis锁处理幂等性失效

幂等性设计,及案例分析,java,mysql

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

以上代码问题:

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

mysql默认事务级别——可重复读;
锁加错位置了,锁应该加在这个事务方法的外面;
正例:
幂等性设计,及案例分析,java,mysql

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

1.1 扩展:

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

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

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

幂等性设计,及案例分析,java,mysql

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

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

传播范围(异常标注):

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

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

2.1 复盘

幂等性设计,及案例分析,java,mysql

幂等性设计,及案例分析,java,mysql

错误原因:

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

解决方法:

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

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

三、mysql死锁场景

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

3.1 mysql死锁复盘

幂等性设计,及案例分析,java,mysql
在 MySQL 中,FOR UPDATE 子句用于在读取数据时锁定该记录,以防止其他事务同时更新或删除该记录。当多个事务试图同时锁定同一记录时,可能会导致死锁。
下面是一个可能导致死锁的场景:

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

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

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

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

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

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

四、手动模拟死锁

幂等性设计,及案例分析,java,mysql

mysql默认级别,可重复读(rr)。阿里设置的级别:读已提交(rc);

rr会有间隙锁出现死锁的可能性更大;

五、幂等性设计方法

5.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 ,这种情况下多次请求,可能会导致数据错误;

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

5.1.1 insert前先select

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

5.1.2 加悲观锁

5.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

操作位于事务中
具体流程如下:
幂等性设计,及案例分析,java,mysql
具体步骤:

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

具体流程:

a:单件货品操作流程:

幂等性设计,及案例分析,java,mysql

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

幂等性设计,及案例分析,java,mysql
具体步骤:

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

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

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

5.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
值已经修改了,那么前面必定已经成功过一次,后面都是重复的请求。

幂等性设计,及案例分析,java,mysql
具体步骤:

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

5.1.4 加唯一索引

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

5.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)建唯一索引。

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

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

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

5.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,语句执行就结束了。
5.1.4.2.3 总结

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

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

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

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

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

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

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

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

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

六、mysql 一页大约能存多少个索引

MySQL 中,一页的大小通常是 16KB,其中大约能存 1000 个索引。

  在 MySQL 中,索引的大小取决于多种因素,例如索引的数据类型、索引列的数量、数据行的大小等等。通常情况下,一个 B 树索引的大小可以通过以下公式来估算:
 索引大小 = 索引列数 * 每个列的平均字节数 + 每个索引节点的开销其中,每个索引节点的开销通常是固定的,大约为 16 字节。
例如,如果一个索引有 4 个列,每个列的平均字节数为 10 字节,那么该索引的大小大约为:
4 * 10 + 16 = 56 字节


 需要注意的是,这只是一个粗略的估算值,实际的索引大小可能会因为数据的分布、数据行的大小等因素而有所不同。此外,不>同类型的索引(如 B 树索引、哈希索引等)的大小也会有所不同。在实际应用中,为了获得最佳的性能和存储效率,需要根据>具体情况进行调整和优化。文章来源地址https://www.toymoban.com/news/detail-738113.html

到了这里,关于幂等性设计,及案例分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • Java设计模式之结构型-组合模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 4.1、基本实现 4.2、菜单遍历  五、总结  组合模式(Composite Pattern)又叫部分-整体模式,它通过将对象组合成树形结构来表示“整体-部分”的层次关系,允许用户统一单个对象和组合对象的处理逻辑。 角色 描述

    2024年02月16日
    浏览(44)
  • Java设计模式之结构型-桥接模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 4.1、支付方式 4.2、支付渠道  五、总结 桥接模式(Bridge Pattern)是一种结构型设计模式,其主要目的是“将抽象部分与实现部分分离,使它们都可以独立地变化”。 桥接模式的核心思想是把抽象(abstraction)与实现

    2024年02月13日
    浏览(36)
  • Java设计模式之创建型-单例模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 4.1、饿汉模式 4.2、懒汉模式(线程不安全) 4.3、懒汉模式(线程安全) 4.4、双重检索模式 4.5、静态内部类 4.6、枚举  五、总结 单例模式确保一个类只有一个实例,提供一个全局访问点。一般实现方式是把构造函

    2024年02月13日
    浏览(32)
  • Java设计模式之创建型-建造者模式(UML类图+案例分析)

    目录 一、基本概念 二、UML类图 三、角色设计  四、案例分析 五、总结 建造者模式是一种创建型设计模式,它使我们将一个复杂对象的构建步骤分离出来,使得同样的构建过程可以创建不同的表示。该模式的目的是将构建复杂对象的过程抽象化,从而减少代码的重复和复杂

    2024年02月15日
    浏览(32)
  • Java设计模式之行为型-迭代器模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 五、总结 迭代器模式是一种常用的设计模式,它主要用于遍历集合对象,提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 举个简单的比喻,聚合对象像一个存放苹果的篮子,迭代

    2024年02月16日
    浏览(31)
  • Java设计模式之结构型-享元模式(UML类图+案例分析)

    目录 一、基本概念 二、UML类图 三、角色设计 四、案例分析 4.1、基本实现 4.2、游戏角色 五、总结 享元模式是一种结构型设计模式,主要用于减少创建大量相似对象所占用的内存,它通过共享技术来有效支持大量细粒度的对象。 角色 描述 抽象享元角色 定义出对象的外部状

    2024年02月16日
    浏览(39)
  • Java设计模式之行为型-备忘录模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 五、总结  备忘录模式是一种行为型设计模式,它允许保存一个对象的内部状态到一个备忘录对象中,这样就可以在需要的时候恢复这个对象的状态了,同时又不违反封装性原则。 这个模式的核心就是备忘录对象,

    2024年02月16日
    浏览(31)
  • Java设计模式之行为型-访问者模式(UML类图+案例分析)

    目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 五、总结 访问者模式是一种对象行为型设计模式,它能够在不修改已有对象结构的前提下,为对象结构中的每个对象提供新的操作。 访问者模式的主要作用是把对元素对象的操作抽象出来封装到访问者类中,这样就

    2024年02月16日
    浏览(40)
  • Java设计模式之结构型-装饰器模式(UML类图+案例分析)

    目录 一、基本概念 二、UML类图 三、角色设计 四、代码实现 案例一 案例二  五、总结  装饰器模式是指不必在改变原有的类和不使用继承的情况下,动态扩展一个对象的功能。 角色 描述 抽象构件 是一个接口或者抽象类,定义我们最核心的对象 具体构件 抽象构件的实现,

    2024年02月11日
    浏览(27)
  • 幂等性设计与实现

    幂等性(Idempotence) 是一个在计算机科学中使用的术语。当某个操作无论进行一次或多次都产生相同的结果,我们就说这个操作是幂等的。 例如,删除文件的操作就是幂等的,因为无论你尝试删除一次还是两次,结果都是文件被删除。相对地,计数器增加操作就不是幂等的,

    2024年02月16日
    浏览(28)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包