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, 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这种行为,不管执行多少次都是幂等的,你在进行互联网支付的时候,即使系统卡顿,你提交多次,也只支付一次。
要做到幂等性,从接口设计上来说不设计任何非幂等的操作即可。特别在类似支付宝,银行,互联网金融公司等涉及的网上资金系统,既要高效,数据也要准确,不能出现多扣款,多打款,产生金钱交易不一致等问题。
以上就是从架构思维角度分析高并发下幂等性解决方案的详细内容,更多关于高并发下幂等性架构思维解决方案的资料请关注编程网其它相关文章!