概念
接口幂等性是指对同一操作的一次请求或多次请求返回的结果是一致的,不会因为多次请求就产生不一样的结果,比如数据库的select操作,可以看成是幂等性的,而插入和更新操作,则要保证重复提交造成的数据重复或者数据不正确的问题。
比如新增操作,假设名称不能重复,那么往往就要保证在新增过程中使数据库不要出现同名的数据,这是典型的防重操作。
比如更新操作,假设每次更新数值加一,那么就要保证在新增过程中不会因为重复请求而导致数值增加错乱,要保证幂等性,如果只是update 表 set is_enable = 0或1这种固定的值,就没关系,这种操作是幂等性的。
防重,顾名思义,防止数据库产生重复数据,对返回的结果是没什么要求的,所以如果是幂等性处理,在数据防重的基础上,还要把重复提交的请求返回的结果和第一次提交的请求返回的结果保持一致,比如统一返回操作成功的提示,但只有第一次是真正执行了。
适用场景
1.前端如果忘了做遮罩层,快速点击多次新增的情况下,数据库极易出现重复数据;
2.微服务间调用接口超时的时候,如果配置了重试机制,也很容易出现重复请求的情况;
3.在使用rocketMq消息队列在消费消息的时候,有时也会同时有重复消费的情况,比如下面两种情况:
① 消息生产者没有收到消息队列收到消息的应答,重试机制使得重复产生消息。
比如网络故障导致应答消息丢失或者消息太多 ,应答消息传回受到阻塞,生产者等待超时。
②消息已经到达消息队列,但发送给消费者的时候,没有收到来自消费者的回复消息,或者消息中间件更改消息状态出现问题。
常用的解决方案
数据新增的时候通常先从数据库select确认数据不存在后再插入,但是这样依然无法避免多次连击的情况(实测,只要够快),所以思路大体分成两个方向,一个是用数据库方式解决,一个是非数据库的方式。
数据库方向可分为:悲观锁、乐观锁、加唯一索引、建防重表(本质上还是唯一索引),加状态字段,更新完改变状态(本质上和乐观锁很像)
非数据库方向可分为:借用第三方缓存解决,比如redis或者结合前端用token.
先说一下我觉得这里面最适合的方式,用数据库解决会加重数据库的性能负担,而token的方案,要请求两次,而且要前端配合(既然都要前端配合了,不如直接让前端加个遮罩层来的快速方便)所以优先考虑用redis做分布式锁的方式,下面也会稍微介绍下每个方式的原理。
redis分布式锁
比如新增用户的接口,请求开始时,先把不能重复的字段比如用户名放到redis缓存里,可以用set或者setnx都行,设置比如两秒的过期时间(可根据业务自行设置),然后再select数据库里有没这个用户,如果没有则插入,插入成功之后删除redis里的缓存,返回操作成功。此时,如果有其他重复的请求进来,redis里已经有缓存的情况下,则直接返回操作成功就好了,或者返回数据已存在的提示,但是不会再往下执行。
这种方法应该是最简单的,而且效率最高的方法了。原理和数据库的分布式锁差不多,只是放在了redis里不会影响数据库的性能。但是关键最后一定要删除redis里的缓存。
token方案
需要让前端先请求后台生成token的接口,然后前端请求的时候header里带上这个token,后台拿到token之后放入redis里,设置缓存过期时间,再去数据库select看数据存不存在,不存在则新增,成功之后删除redis里的token,这种方案,要求前端那边请求两次,一次拿token,一次业务请求带上token,而且快速点击时,多次请求带的都是同一个token, 这个原理上还是分布式锁,只是可以全局配置,并不关心具体是什么字段,但是要请求方在请求之前先拿到唯一的token,如果连拿token这个请求都是重复的话就不太容易判断了。
数据库悲观锁
利用select for update锁住数据库中的一行数据,更新完之后别的请求才能更新这条数据,比如:
select * from user where id=1 for update;
这样再更新这一行的数值做加减运算的时候就不怕用其他并发请求过来引发数据错乱问题了,比如:
update user set like_num = like_num+1 where id = 1;
但是这种需要注意,如果是mysql,存储引擎必须用支持事物的innodb,这里id字段一定要是主键或者唯一索引,不然会锁住整张表。
数据库乐观锁
给要操作的行加个版本字段,每次更新前先查出这一行数据的版本,更新的时候指定这一行数据和版本,并且版本id+1,比如:
update user set like_num = like_num+1,version = version+1
where id = 1 and version = 1;
这样即便有重复请求,但是更新之后version变成了2,那么再更新version=1的必然不会生效。
以上两种数据库操作并不会解决新增数据重复的问题,能解决更新数据数值计算错乱的问题。
加唯一索引
这个基本是数据库版的分布式锁,有效解决数据重复问题,当插入重复字段数据时,会抛异常,不会插入成功。
建防重表
这种是把上面的唯一索引单独建一张表,这样就能在需要防重的时候先插入防重表来防重,不需要防重的时候又能存储这个字段的重复数据,更加灵活一点,这个思路把这个防重表放到redis里就是redis分布式锁了,其实思路差不多,只是一个在数据库,一个借助第三方缓存。
加状态字段
比如活动待审核-审核中-已审核-执行-结束-删除等流程操作的时候,每次更新都会造成状态的改变,这个就像是上面加了版本字段一个原理,更新的时候指定状态,更新完之后状态也改变了,这样重复的请求过来就不会更新成功,比如:
update activity set status = 2 where status = 1 and id = 1;
总结
综上,感觉redis缓存分布式锁的方式是效率最高的,推荐这个。
不同业务需求可以结合使用,会更香。
比如转账场景,先把订单id放入redis缓存做分布式锁,然后用数据库乐观锁加个版本字段控制,会更加保险一点。