从架构思维角度分析高并发下幂等性解决方案

目录
  • 1 背景
  • 2 幂等性概念
  • 3 幂等性问题的常见解决方案
    • 3.1 查询操作和删除操作
    • 3.2 使用唯一索引 或者唯一组合索引
    • 3.3 token机制
    • 3.4 悲观锁
    • 3.5 乐观锁
    • 3.6 分布式锁
    • 3.7  select + insert
    • 3.8 状态机幂等
    • 3.9 保证Api接口的幂等性
  • 4 会议室的解决方案
  • 5 总结

1 背景

我们的云办公系统有一个会议预定模块,每个月最后一个工作日的下午三点,会启动对下个月会议室的可用预定。

公司的 会议室大约200个,但是需求量远不止于此,所以会形成会议室抢订的场面(抢订大军为行政助理、人事助理、开发经理、产品运营等对会议室有刚性需求的人)。

程序团队,经常会接到投诉,A同学和B同学抢了同一个会议室, 前端页面显示为两个占位图片,从数据库看,是插入了两条同一个会议位置的数据,这两条数据的发起人员分别是A和B。

这就牵扯出一个数学与计算机学概念: 幂等。

在计算机系统操作中,有很多种行为,需要保证无论执行多少次,都应该产生一样的效果或返回一样的结果。

比如:

1、前端重复点击提交表单选中的数据,在后台应该只能有一个数据录入到数据库;

2、发送同一个消息,也应该只发一次,用户不会收到多条一样的数据;

3、创建业务订单,一次业务请求只能创建一个,如果程序没有保证幂等,创建出多条订单数据,就混乱了。

4、在高并**况下,对于单一的数据,不可以多次使用,比如一张确定位置的电影票,不会被多次预订成功。同理的,同一时间的一个会议室信息,不会被多次预订。

etc.很多重要的场景都需要幂等的特性来支持。

2 幂等性概念

幂等(idempotent)是一个数学与计算机学概念,常见于抽象代数中。

在我们的开发过程中,保证幂等性就是保证你的程序的无论执行多少次,影响均与第一次执行的影响是一致的,产生的结果也是一样的。

而幂等函数(幂等方法),是指使用相同的参数结构重复执行,产生相同的结果的函数,重复执行幂等函数不会影响系统的状态或者造成改变。

例如,"getUserName(String uCode)" 和 "delUser(String uCode)" 函数就是典型的幂等函数,而更复杂的幂等保证是类似 高并发场景下的订单号(流水号)或者 秒杀场景下的唯一有效数据 等。

所以,幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

3 幂等性问题的常见解决方案

3.1 查询操作和删除操作

查询一次和查询多次,在数据不变的情况下,查询结果是一样的,所以严格来说, select是天然的幂等操作。

删除也是一样的, 对于单条数据来说,删除一次和删除多次都是把数据删除,影响和结果都是一样(当然,程序上 的执行的返回结果可能会不一样,比如操作数据库的时候,删除的数据不存在,返回0,正常删除成功,返回1) 。

1 -- 用户库查询某个身份证号的用户名
2 select user_name from t_user where id_no ='xxx';
3
4 -- 用户库删除某个身份证号的用户
5 delete from t_user where id_no ='xxx';

3.2 使用唯一索引 或者唯一组合索引

避免插入同样信息的脏数据。

比如:中秋节到了,淘宝上线某款**版的月饼,每个用户都只能购买一盒月饼,如何防止用户被创建多条月饼订单数据,可以给月饼销售表中的用户ID加唯一索引( 不允许被索引的数据列包含重复的值),

保证一个用户只能创建成功一条月饼订单记录。

1 CREATE UNIQUE INDEX uni_user_userid ON t_user(userid);

唯一索引或唯一组合索引来防止新增数据出现脏数据(当表存在唯一索引,并发执行时,先进入的执行成功,后进入的会执行失败,说明该数据已经存在了,返回结果即可)。如下图所示。

回到我们上面的哪个会议室预订,也可以是一样的方式,可以用会议室编号(该编号具有唯一标识)作为唯一索引,但是他的实际情况更复杂。

3.3 token机制

防止页面重复提交而导致的数据重复

业务现象: 页面的数据只能被提交一次,或者提交多次的结果是一致的,不会产生多余的脏数据。

产生的原因: 由于系统卡顿导致的重复点击或网络重发,还有就是nginx重发等情况,导致的数据被重复提交;

解决方法:

  • 集群环境采用token加redis(redis单线程的,处理需要排队);
  • 单JVM环境:采用token加redis或token加jvm内存。

处理步骤:

  • 数据提交前要向服务的申请token,token放到redis或jvm内存,token需要设置有效时间,一般我们一个请求从request到respond时间是很短的,所以有效时间可以设置短一点;
  • 提交后后台校验token,同时删除token,返回执行结果。token特点:一次有效性,用完即删,可以限流执行。

流程如下,注意:redis要用删除操作来判断token,删除成功代表token校验通过;

3.4 悲观锁

获取数据的时候加锁获取。 select * from t_name where id='xxx' for update;

注意:这边的id字段一定是主键或者唯一索引,不然会导致锁表。悲观锁使用时一般会配合事务一起使用,数据锁定时间可能会很长,根据实际情况选用。

3.5 乐观锁

乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高,适用于多读少写的类型,并发大的情况。

乐观锁的实现方式多种多样,可以通过version或者其他状态条件:

1. 通过版本号实现  update t_name set name=#{name},version=version+1 where version=#{version};

2. 通过条件限制  update t_name set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0

使用版本号的方式执行过程如下图:

这边需要注意: 乐观锁的更新操作,如果加上主键或者唯一索引来作为条件, 更新时锁的是行,否则更新时会锁表,性能效率差很多。所以上面两个sql改成下面两个会好很多。

1 update t_name set name=#name#,version=version+1 where id=#id# and version=#version#;
2 update t_name set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0;

3.6 分布式锁

如果是分布是系统,构建全局唯一索引比较困难,不同的链路业务可能分布在不同的数据库表中,所以唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),

在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,完成业务操作之后,释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。

关键点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。

3.7  select + insert

并发不高的后台系统,或者一些简单的执行任务,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。

但是同样有问题,核心高并发流程不便使用这种方法。因为他本质上还是两个步骤,中间还有执行间隙的,在超高并发的情况还是会造成数据不一致的情况,这对于核心业务就是灾难了。

3.8 状态机幂等

在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,

这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。

注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助

3.9 保证Api接口的幂等性

如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号 ,source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。

关键点:核心业务功能,对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,

先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。为了幂等友好,最好先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,而实际是已经处理过了。

4 会议室的解决方案

将每天的会议预定按照半个小时1位做48位占用位符预算,建立缓存机制,进行高效率的占位判断,并反写到预定表;启动额外调度服务做最终的预定持久化;

采用唯一联合索引保障高并发下的幂等性策略。将会议室ID、时间段、日期,建立唯一组合索引,防止新增脏数据,保证不会有两条一样的会议室预定记录插入

1 CREATE UNIQUE CLUSTERED INDEX [ClusteredIndex_A9_MeetingReser] ON A9_MeetingReser
2 (
3 [timespan] ASC,
4 [roomid] ASC,
5 [sdate] ASC
6 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

执行会议预订的事务脚本,如下,当数据库中存在一样的会议室信息时,会返回错误(被占用)的状态值。

1  BEGIN TRAN T_Add;
2  DECLARE @code INT; DECLARE @occupyMeeing TABLE ( sMeetCode INT );
3  DECLARE @resutlTable TABLE ( lType TINYINT,/*返回类型0为失败类型,1为成功类型*/ resutlValue NVARCHAR(60)/*返回的信息*/ );
4  -- Todo 业务逻辑 写入数据库操作,即会议号和占用的时间段标识为联合索引,不可重复插入,重复插入报错
5  IF @@ERROR!=0 goto w_err;
6  COMMIT TRAN T_Add ;
7  goto w_end   w_err:
8  ROLLBACK TRAN T_Add ;
9  w_end:  SELECT * FROM @resutlTable;

原来从预定到判断占用到写库会耗时0.5~1s,优化后整个流程执行性能提升到50ms左右,避免了会议室预定冲突的情况。

结果:根据会议室预定记录的统计,优化发布之后再未发生过预定冲突的问题。免除了会议管理员与预定人员沟通协调会议室的成本,解决了长期困扰他们的问题。

5 总结

幂等本质上与系统是否分布式、高并发,业务执行频率高不高,没有直接的关系。关键是程序的操作过程是不是幂等的。

典型的幂等操作就是:把某个变量设置为1这种行为,不管执行多少次都是幂等的,你在进行互联网支付的时候,即使系统卡顿,你提交多次,也只支付一次。

要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。特别在类似支付宝,银行,互联网金融公司等涉及的网上资金系统,既要高效,数据也要准确,不能出现多扣款,多打款,产生金钱交易不一致等问题。

以上就是从架构思维角度分析高并发下幂等性解决方案的详细内容,更多关于高并发下幂等性架构思维解决方案的资料请关注我们其它相关文章!

(0)

相关推荐

  • 浅谈java接口的幂等性及解决方案

    目录 一.什么情况下需要幂等 二.幂等性解决方案 2.1 token机制(令牌) 2.2 各种锁机制 2.3 各种唯一约束 2.4 防重表 2.5 全局请求唯一id 总结 一.什么情况下需要幂等 用户多次点击按钮 用户页面回退再次提交 微服务相互调用,由于网络问题,导致请求失败,feign触发重试机制 二.幂等性解决方案 2.1 token机制(令牌) 在加载页面详情时候,服务器会顺便生成一个token一起返回给前端,服务端同时也在Redis中保存这个token数据,前端并不展示这个token,

  • 高并发系统的限流详解及实现

    在开发高并发系统时有三把利器用来保护系统:缓存.降级和限流.本文结合作者的一些经验介绍限流的相关概念.算法和常规的实现方式. 缓存 缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪.使用缓存不单单能够提升系统访问速度.提高并发访问量,也是保护数据库.保护系统的有效方式.大型网站一般主要是"读",缓存的使用很容易被想到.在大型"写"系统中,缓存也常常扮演者非常重要的角色.比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及

  • 高并发系统数据幂等的解决方案

    前言 在系统开发过程中,经常遇到数据重复插入.重复更新.消息重发发送等等问题,因为应用系统的复杂逻辑以及网络交互存在的不确定性,会导致这一重复现象,但是有些逻辑是需要有幂等特性的,否则造成的后果会比较严重,例如订单重复创建,这时候带来的问题可是非同一般啊. 什么是系统的幂等性 幂等是数据中得一个概念,表示N次变换和1次变换的结果相同. 高并发的系统如何保证幂等性? 1.查询 查询的API,可以说是天然的幂等性,因为你查询一次和查询两次,对于系统来讲,没有任何数据的变更,所以,查询一次和查询多次一

  • Java 浅谈 高并发 处理方案详解

    目录 高性能开发十大必须掌握的核心技术 I/O优化:零拷贝技术 I/O优化:多路复用技术 线程池技术 无锁编程技术 进程间通信技术 Scale-out(横向拓展) 缓存 异步 高性能.高可用.高拓展 解决方案 高性能的实践方案 高可用的实践方案 高扩展的实践方案 总结 高性能开发十大必须掌握的核心技术 我们循序渐进,从内存.磁盘I/O.网络I/O.CPU.缓存.架构.算法等多层次递进,串联起高性能开发十大必须掌握的核心技术. - I/O优化:零拷贝技术 - I/O优化:多路复用技术 - 线程池技

  • 从架构思维角度分析高并发下幂等性解决方案

    目录 1 背景 2 幂等性概念 3 幂等性问题的常见解决方案 3.1 查询操作和删除操作 3.2 使用唯一索引 或者唯一组合索引 3.3 token机制 3.4 悲观锁 3.5 乐观锁 3.6 分布式锁 3.7  select + insert 3.8 状态机幂等 3.9 保证Api接口的幂等性 4 会议室的解决方案 5 总结 1 背景 我们的云办公系统有一个会议预定模块,每个月最后一个工作日的下午三点,会启动对下个月会议室的可用预定. 公司的 会议室大约200个,但是需求量远不止于此,所以会形

  • 从架构思维角度分析分布式锁方案

    目录 1 介绍 2 关于分布式锁 3 分布式锁的实现方案 3.1  基于数据库实现 3.1.1 乐观锁的实现方式 3.1.2 悲观锁的实现方式 3.1.3 数据库锁的优缺点 3.2基于Redis实现 3.2.1 基于缓存实现分布式锁 3.2.2缓存实现分布式锁的优缺点 3.3 基于Zookeeper实现 3.3.1 实现过程 3.3.2 zk实现分布式锁的优缺点 3.4 三种方案的对比总结 1 介绍 前面的文章我们介绍了分布式系统和它的CAP原理:一致性(Consistency).可用性(Ava

  • 总结高并发下Nginx性能如何优化

    目录 特点 优势 安装和命令 配置文件 代理模式和配置反向代理 正向代理(forward proxy) : 反向代理(reverse proxy)︰ 透明代理∶ 动静分离 日志管理 日志格式 日志切割 高并发架构分析 什么是高并发? 如何提升系统的并发能力? 三种方式实现 限制连接流 限制请求流(限速) 后台服务限制 安全配置 Nginx优化 Nginx压缩 我们终将在,没有黑暗的地方相见. ~乔治<1984> Nginx同Apache一样都是一种WEB服务器.基于REST架构风格,以统一资源

  • 架构思维之缓存雪崩的灾难复盘

    目录 1 真实案例 1.1 背景 1.2 问题处理 2 缓存雪崩 2.1 概念 2.2 解决方案分析 2.2.1 缓存集群+数据库集群 2.2.2 适当的限流.降级 2.2.3 随机过期时间 2.2.4 缓存预热 3 缓存穿透 3.1 概念 3.2 解决方案分析 3.2.1 缓存空值 3.2.2 BloomFilter 3.2.3 两种方案的选择判断 4 缓存击穿 4.1 概念 4.2 解决方案 4.2.1 锁的方式 4.2.2 空初始值 1 真实案例 云办公系统用户实时信息查询功能优化发布之后

  • PHP+Redis 消息队列 实现高并发下注册人数统计的实例

    前言 现在越来越多的网站开始注重统计和用户行为分析,作为网站经常使用的功能,如何让统计性能更加高,这也是我们需要考虑的事情.本篇通过Redis来优化统计功能(以注册人数统计为例). 传统的统计功能都是直接操作数据库把数据插入表中.这样做,对数据库的性能消耗就会比较大. 思路: 这里我们用到了redis的队列,注册的时候先添加到队列,然后在处理的时候出队,并且把人数添加redis里. 代码: <?php //register.php $redis = new Redis(); $redis->c

  • 深入浅析Random类在高并发下的缺陷及JUC对其的优化

    Random可以说是每个开发都知道,而且都用的很6的类,如果你说,你没有用过Random,也不知道Random是什么鬼,那么你也不会来到这个技术类型的社区,也看不到我的博客了.但并不是每个人都知道Random的原理,知道Random在高并发下的缺陷的人应该更少.这篇,我就来分析下Random类在并发下的缺陷以及JUC对其的优化. Random的原理及缺陷 public static void main(String[] args) { Random random = new Random();

  • 高并发下restTemplate的错误分析方式

    目录 高并发下restTemplate的错误分析 1. 问题现象和分析 2. 问题解决 使用restTemplate出现的异常 高并发下restTemplate的错误分析 1. 问题现象和分析 org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection 此问题很明显是连接等待超时,而且是从连接池中获取的连接. 那么就有一个很诧异的问题,这里哪来的连接池呢?然后我去跟踪restTemplat

  • BlockingQueue队列处理高并发下的日志

    目录 前言 what阻塞队列? 1.声明存储固定消息的队列 2.消息入队 3.消息出队被消费 前言 当系统流量负载比较高时,业务日志的写入操作也要纳入系统性能考量之内,如若处理不当,将影响系统的正常业务操作,之前写过一篇<spring boot通过MQ消费log4j2的日志>的博文,采用了RabbitMQ消息中间件来存储抗高并发下的日志,因为引入了中间件,操作使用起来可能没那么简便,今天分享使用多线程消费阻塞队列的方式来处理我们的海量日志 what阻塞队列? 阻塞队列(BlockingQueu

  • java高并发下CopyOnWriteArrayList替代ArrayList

    目录 一.ArrayList线程不安全 二.解决ArrayList线程不安全的方案 1.使用Vector类 2.使用Collections类 3.使用CopyOnWriteArrayList类 三.CopyOnWriteArrayList 1.简介 2.主要方法源码分析 1.初始化 2.添加元素 3.获取指定位置元素 4.修改指定元素 5.删除元素 6.弱一致性的迭代器 四.总结 一.ArrayList线程不安全 在Java的集合框架中,想必大家对ArrayList肯定不陌生,单线程的情况下使用

  • php结合redis高并发下发帖、发微博的实现方法

    发帖.发微博.点赞.评论等这些操作很频繁的动作如果并发量小,直接入库是最简单的 但是并发量一大,数据库肯定扛不住,这时可采取延迟发布:先将发布动作保存在队列里,后台进程循环获取再入库 模拟发布微博先进入redis队列 weibo_redis.php <?php //此处需要安装phpredis扩展 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis->auth("php001"); //连接

随机推荐