幂等性的概念

对于同一操作发起的请求(一次或者多次请求),任意多次执行对资源本身产生的影响均与一次执行产生的影响相同,不会因为多次相同操作而产生副作用。

比如一个用户注册,点击“注册”,由于某些原因(比如服务器负载大),长时间转圈圈,你多次点击“注册”,这多次请求都是完全相同的,应该只插入一条用户记录,而不是点多次“注册”每次都插入一条用户记录。

比如下单购买,点击“提交”,卡住了,什么破手机|网络,疯狂点“提交”,这多次请求完全相同,应该只产生一个订单,而不是每次点击都产生一个订单。

http请求方式(restful)

在其他条件不变的情况下(资源没有被其它操作修改):

get  查询,相同的查询操作,比如select username where id=1  from tb_user;执行多次,查询到的结果都是相同的,get是幂等的

post  插入,相同的插入操作,比如insert into tb_order (…) values (…);  执行多次,虽然可能会因为主键已存在、某个字段的值要唯一等原因插入失败,但每次都会尝试插入一条新纪录,如果满足约束,每次都会插入一条记录,结果可能不同(产生了新纪录),post不是幂等的

put  更新,相同的更新操作,比如update tb_user username=’chy’,age=20 where id=1;  不管执行多少次,id=1的这条记录,name都是chy,age都是20,结果都是相同的,put是幂等的

delete  删除,相同的删除操作,比如delete from tb_user where id=1 ,不管执行多少次,结果都是删除了id=1的记录,结果相同,delete是幂等的

消息消费者与幂等性

消息可能会被重复投递、消费,比如第一次投递的消息还在queue中,为确保消息投递的可靠性,延时又投递了一次消息,消费者应确保同一消息只进行一次消费。


幂等常见的实现方式

核心思想:

使用唯一的业务单号来标识一个业务,如果业务单号相同,就认为是同一笔业务,只执行一次。(去掉重复的业务)

存储执行过的业务的业务单号,先根据业务单号查询这笔业务是否已经执行过了,如果没执行过,才执行。

如果系统是单线程的,查询单号、执行这笔业务2个过程都不用加锁;如果过多个线程并发访问执行过的业务单号(有读有写),就需要给执行过的业务单号加锁(幂等是查询时加锁),执行过程往往也需要给共享数据加锁。

业务单号可以用流水号,也可以唯一id+指纹码,总之要能唯一标识一笔业务。

比如order的id,只用id是可以唯一标识一个订单,但不能标识是这个order的增改删查哪个业务,也不能标识是这个order的第几次修改操作。需要再加一个指纹码,比如时间戳、业务规则什么的。

1、数据库主键去重

单独用一张tb_order_ handled来存储执行过的订单的业务单号,使用列no存储业务单号(主键、unique约束),外键order_id关联order表的id,

执行业务时先select count(*) from tb_order_handled where no=xxx; 根据业务单号查询这笔业务是否已执行,数量为1表示有这条记录,这笔业务已处理过,不再处理;为0表示没处理过,就处理这笔业务。

这种方式的缺点是增加了数据库IO次数,数据库性能很容易成为系统性能的瓶颈,往往需要分库分表把业务单号所在的表拆分出来,来提升数据库性能。

 如果这个服务只有数据库更新操作的话,也可以使用数据库的乐观锁(版本号机制)。

2、分布式锁(推荐)

把处理过的业务单号作为键值对的key,存储在redis上,处理业务时先查询redis上有没有这个key,有就说明处理过了,不再处理,没有就说明这笔业务没有处理过,开始处理。

简单、且性能高,推荐。

3、缓冲区

把一段时间内要处理的业务都放到缓冲区中,先去掉重复的业务,再执行。

缺点:处理有时延、不能马上返回处理结果。不太常用。