我们微服务接口愈来愈庞大,那这里大概讲述一下我个人对于各种接口设计的一些看法和要点。
1、接口是否需要做幂等
如果是查询或者删除这些接口是不需要做幂等的,一般删除是根据ID来删除,第二次删除如果内容不存在,那么也相当于删除成功。
如果是新增或者修改操作,一般来说都会做幂等的,而做密等有如下两种。
1)业务逻辑上幂等
比如注册接口,这种根据用户填入信息,比如手机号这种来注册的,逻辑上一般都是如果发现表中有值,都会直接返回注册成功,不管用户调用多少次,所以这种业务逻辑上已经可以保证幂等了的。不管前端调用你的接口超时或者调用成功后本地操作失败造成客户端本地事务回滚,第二次调用后你直接返回成功,所以不会有数据上的问题。
2)接口调用流水号来保证幂等
有些接口比如权益领取,客户端调用权益领取接口,如果此时超时了,或者调用成功后本地业务逻辑操作失败了那怎么办,权益在接口那边是已经领取成功了的,如果再次发起可能会领取第二次,这种情况下靠业务逻辑来保证幂等就比较麻烦了,此时我们需要在接口上加上一个额外的接口调用流水号,比如如下输入参数:
{
"cstm_no":"会员号",
"award_id":"要领取的权益ID",
"call_sn":"接口调用流水号"
}
上面的接口调用流水号通常来说跟客户端的业务有关联,如果客户端调用接口成功后业务逻辑出错回滚,或者超时,第二次应该还是用相同流水号来,这样微服务业务处理逻辑就会根据流水号去查询该流水是否已经存在,存在则直接返回,不存在则做流水操作。
3)接口调用流水号来做异步解耦
还有一种业务逻辑是这样的,客户端或者第三方调用你的接口后,为了防止超时导致服务没返回,客户端会直接根据接口调用流水号来循环间隔的调用你提供的另一个接口来查询结果,比如支付宝的支付,因为支付逻辑耗时比较长,所以你调用完支付后,可以等着支付宝回调你的接口来告诉你结果,也可以你自己根据流水号去查询结果。又比如上面说的注册,如果我的注册逻辑比较复杂,如下流程:
获取接口输入信息
校验输入参数是否正确
调用身份验证接口校验用户是否满足注册条件
初始化用户相关信息
其中用身份验证接口和初始化用户相关信息耗时较长,如果客户端或者第三方调用我们的接口的话超时的概率很大,并且这个注册接口耗时时间较长,会较长时间占据一个http线程,对微服务的性能影响较大。此时我们通常会对该接口进行解耦。将”调用身份验证接口校验用户是否满足注册条件”和”初始化用户相关信息”这两部逻辑用消息队列比如RocketMQ让消费程序来执行,本注册接口就直接返回成功。最终注册结果需要通过另一个接口来查询,如下图:
那么客户端的调用逻辑将会变成途中所说的
- 1、调用注册接口,传入接口调用流失
{
"call_sn":"接口调用流水UUID等",
"userinfo":"用户注册信息"
}
- 2、不去管接口是否调用成功,因为可能超时,直接调用结果查询接口
{
"call_sn":"接口调用流水UUID等"
}
去根据流水号查询注册接口调用的结果,并且这里是轮询调用,比如两秒钟调用一次,总共调用10次,如果结果为生成就都叫做处理中。
查询接口可以通用出来,直接根据call_sn去redis查询结果,如果查询不到则代表处理中,如果查询得到则直接返回,让客户端自己去处理即可。
那么微服务的处理逻辑也很简单
- 1、获取接口输入信息,登记消息给消费程序。
{
"call_sn":"接口调用流水UUID等",
"userinfo":"用户注册信息"
}
消费程序的处理逻辑也很简单
- 1、做注册操作
- 2、把结果放入redis,key为call_sn.(或者加一些前缀)
一般来说,某个接口有做异步解耦的话,一定要传入接口调用流水,然后根据接口调用流水来查询本次的操作结果。
2、怎么保证接口同一个用户并发访问。
1)场景1:单个用户并发注册
比如我们有一个活动报名表,活动开始后用户首先要根据手机号注册,那么正常处理逻辑如下
1、根据用户手机号查询报名表查看用户是否已经注册
2、若用户已注册则返回用户注册成功
3、若用户未注册则做注册逻辑操作
伪代码如下
//1、获取用户报名信息
UserInfo userInfo = getUserInfoForUpdate(phone,activity_code)
if(userInfo==null){
//2、做注册操作
...
}else{
//3、用户已注册
return userInfo
}
正常逻辑是没有问题的,但是如果单个用户并发进来,在到第一步的时候两个线程都没有查询到userInfo,然后这里都跑去注册。
解决办法:这种情况下,通常我们可以子啊phone和activity_code加上唯一索引即可,如果用户并发进来,刚好同时查询用户信息都为空(正常情况下页面有做token或者防止重复点击的操作),那么会触发唯一索引报错,这样保证了业务数据的一致性。
2)场景2:单个用户并发登记
比如我们有一个玩游戏的活动,规定一个用户一天只能完两次,也就是我们的提交分数接口处理逻辑要做如下操作。
1、用户提交分数
2、查看用户今天已经提交分数的次数
3、若没有超过2次,则允许提交
4、若超过超过2次,则不允许提交
正常流程是没问题的,并且第2步骤那里用户是行锁查询用户的当天提交分数记录,但是如果用户是第一次提交,四五个线程并发进来呢?
此时第2步骤因为一条记录都没有,所以都可以通过,然后用户就登记了四五条记录,违反了活动规则。
这种类似的情况还有很多,比如活动规定一个用户只能获取两个奖品,但是如果用户并发进来领取呢?活动规定以恶搞用户只能抢购两个商品,但是用户并发进来抢购呢?这种情况下行锁统计用户已经做的操作就没有太大意义了,那么该怎么解决呢?
解决方案如下:
方案1、借用redis的自增自减操作来防止用户并发
我们可以用redis的incr自增原子操作,如果incr后操过了规定则直接返回失败,然后回滚redis。并且设置该key的失效时间未几秒钟,反正只要数据库中有记录了的话
就不会出现都查询到是空,一条记录都不存在导致行锁失败的情况,几秒钟失效也可以不用浪费redis的内存。
方案2、借用数据库行锁来防止用户并发
奇怪,上面不是说数据库行锁在没有一条记录的情况下会失败么,这里我说的不是所没有记录的那个表,而是直接锁用户已经有记录的表,这里最好的就是报名表,报名表在场景一里可以保证一定有一条记录,所以这里可以行锁用户的记录,这样子后面统计用户的提交分数次数都不需要进行锁表了。这种情况会遇到的问题就是如果这个用户还有别的业务场景也都锁用户的报名表的话,会导致都等待。
上面两种方案都可以,具体采取哪一种,得看不同的项目架构是怎样的,如果没有引入redis组件,那肯定直接用第二种了。
3)场景3:多个用户并发登记
比如我们有一个奖品,一天只能派发10个,那么也会面临场景2的问题,如果是多个用户并发进来,此时表中一条记录都没有怎么办,又不可能全表锁,那么解决方案就只能够借用方案1来计数了,并且失效时间应该未活动期间内都有效,也许还有用户想直接用java的同步锁,这种方案在高并发的情况下是特别不可取的,不说对性能的影响极其大,如果是多个节点,那么同步锁只能锁JVM的特性就完全没有意义了。