一对一聊天平台源码,实现幂等的8种方案
一对一聊天平台源码,实现幂等的8种方案
在一对一聊天平台源码开发时,幂等设计的基本流程都是类似的,我们简简单单来过一下幂等实现的8中方案
一、select+insert+主键/唯一索引冲突
在一对一聊天平台源码开发中,为了实现交易接口幂等,我是这样实现的:
交易请求过来,我会先根据请求的唯一流水号 bizSeq字段,先select一下数据库的流水表
如果数据已经存在,就拦截是重复请求,直接返回成功;
如果数据不存在,就执行insert插入,如果insert成功,则直接返回成功,如果insert产生主键冲突异常,则捕获异常,接着直接返回成功。
流程图如下
伪代码如下:
/** * 幂等处理 */ Rsp idempotent(Request req){ Object requestRecord =selectByBizSeq(bizSeq); if(requestRecord !=null){ //拦截是重复请求 log.info("重复请求,直接返回成功,流水号:{}",bizSeq); return rsp; } try{ insert(req); }catch(DuplicateKeyException e){ //拦截是重复请求,直接返回成功 log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq); return rsp; } //正常处理请求 dealRequest(req); return rsp; }
为什么前面已经select查询了,还需要try…catch…捕获重复异常呢?
是因为高并发场景下,两个请求去select的时候,可能都没查到,然后都走到insert的地方啦。
当然,用唯一索引代替数据库主键也是可以的哈,都是全局唯一的ID即可。
二、直接insert + 主键/唯一索引冲突
在方案一中,都会先查一下流水表的交易请求,判断是否存在,然后不存在再插入请求记录。如果一对一聊天平台源码中重复请求的概率比较低的话,我们可以直接插入请求,利用主键/唯一索引冲突,去判断是重复请求。
流程图如下:
伪代码如下:
/** * 幂等处理 */ Rsp idempotent(Request req){ try{ insert(req); }catch(DuplicateKeyException e){ //拦截是重复请求,直接返回成功 log.info("主键冲突,是重复请求,直接返回成功,流水号:{}",bizSeq); return rsp; } //正常处理请求 dealRequest(req); return rsp; }
温馨提示 :
大家别搞混哈,防重和幂等设计其实是有区别的。防重主要为了避免产生重复数据,把重复请求拦截下来即可。而幂等设计除了拦截已经处理的请求,还要求每次相同的请求都返回一样的结果。不过呢,很多时候,它们的处理可以是类似,只是返回响应不一样。
三、状态机幂等
一对一聊天平台源码的很多业务表,都是有状态的,比如转账流水表,就会有0-待处理,1-处理中、2-成功、3-失败状态。转账流水更新的时候,都会涉及流水状态更新,即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等,一起来看下它是怎么实现的。
比如转账成功后,把处理中的转账流水更新为成功状态,SQL这么写:
update transfr_flow set status=2 where biz_seq=‘666’ and status=1;
简要流程图如下:
伪代码实现如下:
Rsp idempotentTransfer(Request req){ String bizSeq = req.getBizSeq(); int rows= "update transfr_flow set status=2 where biz_seq=#{bizSeq} and status=2;" if(rows==1){ log.info(“更新成功,可以处理该请求”); //其他业务逻辑处理 return rsp; }else if(rows==0){ log.info(“更新不成功,不处理该请求”); //不处理,直接返回 return rsp; } log.warn("数据异常") return rsp: }
状态机是怎么实现幂等的呢?
第1次请求来时,bizSeq流水号是 666 ,该流水的状态是处理中,值是 1 ,要更新为2-成功的状态 ,所以该update语句可以正常更新数据,sql执行结果的影响行数是1,流水状态最后变成了2。
第2请求也过来了,如果它的流水号还是 666 ,因为该流水状态已经2-成功的状态 了,所以更新结果是0,不会再处理业务逻辑,接口直接返回成功。
四、抽取防重表
方案一和二,都是建立在一对一聊天平台源码业务流水表上bizSeq的唯一性上。很多时候,我们业务表唯一流水号希望后端系统生成,又或者我们希望防重功能与业务表分隔开来,这时候我们可以单独搞个防重表。当然防重表也是利用主键/索引的唯一性,如果插入防重表冲突即直接返回成功,如果插入成功,即去处理请求。
五、 token令牌
token 令牌方案一般包括两个请求阶段:
客户端请求申请获取token,服务端生成token返回
客户端带着token请求,服务端校验token
流程图如下:
一对一聊天平台源码客户端发起请求,申请获取token。
服务端生成全局唯一的token,保存到redis中(一般会设置一个过期时间),然后返回给客户端。
客户端带着token,发起请求。
服务端去redis确认token是否存在,一般用 redis.del(token) 的方式,如果存在会删除成功,即处理业务逻辑,如果删除失败不处理业务逻辑,直接返回结果。
六、 悲观锁(如select for update)
什么是悲观锁?
通俗点讲就是很悲观,每次去操作数据时,都觉得别人中途会修改,所以每次在拿数据的时候都会上锁。官方点讲就是,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
在一对一聊天平台源码中悲观锁如何控制幂等的呢?就是加锁呀,一般配合事务来实现。
举个更新订单的业务场景:
假设先查出订单,如果查到的是处理中状态,就处理完业务,再然后更新订单状态为完成。如果查到订单,并且是不是处理中的状态,则直接返回
整体的伪代码如下:
begin; # 1.开始事务 select * from order where order_id='666' # 查询订单,判断状态 if(status !=处理中){ //非处理中状态,直接返回; return ; } ## 处理业务逻辑 update order set status='完成' where order_id='666' # 更新完成 commit; # 5.提交事务
这种场景是非原子操作的,在高并发环境下,可能会造成一个业务被执行两次的问题:
当一个请求A在执行中时,而另一个请求B也开始状态判断的操作。因为请求A还未来得及更改状态,所以请求B也能执行成功,这就导致一个业务被执行了两次。
可以使用数据库悲观锁(select …for update)解决这个问题.
begin; # 1.开始事务 select * from order where order_id='666' for update # 查询订单,判断状态,锁住这条记录 if(status !=处理中){ //非处理中状态,直接返回; return ; } ## 处理业务逻辑 update order set status='完成' where order_id='666' # 更新完成 commit; # 5.提交事务
这里面order_id需要是索引或主键哈,要锁住这条记录就好,如果不是索引或者主键,会锁表的!
悲观锁在同一事务操作过程中,锁住了一行数据。别的请求过来只能等待,如果当前事务耗时比较长,就很影响接口性能。所以一般不建议用悲观锁做这个事情。
七、乐观锁
悲观锁有性能问题,可以试下乐观锁。
什么是乐观锁?
乐观锁在操作一对一聊天平台源码中的数据时,则非常乐观,认为别人不会同时在修改数据,因此乐观锁不会上锁。只是在执行更新的时候判断一下,在此期间别人是否修改了数据。
怎样实现乐观锁呢?
就是给表的加多一列version版本号,每次更新记录version都升级一下(version=version+1)。具体流程就是先查出当前的版本号version,然后去更新修改数据时,确认下是不是刚刚查出的版本号,如果是才执行更新
比如,我们更新前,先查下数据,查出的版本号是version =1
select order_id,version from order where order_id='666';
然后使用version =1 和订单Id一起作为条件,再去更新
update order set version = version +1,status='P' where order_id='666' and version =1
最后更新成功,才可以处理业务逻辑,如果更新失败,默认为重复请求,直接返回。
流程图如下:
为什么版本号建议自增的呢?
因为乐观锁存在ABA的问题,如果version版本一直是自增的就不会出现ABA的情况啦。
八、分布式锁
在一对一聊天平台源码中分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。执行流程如下图所示:
在一对一聊天平台源码中分布式锁可以使用Redis,也可以使用ZooKeeper,不过还是Redis相对好点,因为较轻量级。
Redis分布式锁,可以使用命令SET EX PX NX + 唯一流水号实现,分布式锁的key必须为业务的唯一标识哈
Redis执行设置key的动作时,要设置过期时间哈,这个过期时间不能太短,太短拦截不了重复请求,也不能设置太长,会占存储空间。
以上是一对一聊天平台源码,实现幂等的8种方案, 更多内容欢迎关注之后的文章