幂等性
什么叫幂等性?就是当一操作多次执行所产生的影响与一次执行的影响相同。
接口幂等性,就是当接口被多次调用所产生的结果与一次调用的结果相同。
应用层http协议也分幂等性
GET:该方法用于获取资源,不会改变资源,属于幂等。
DELETE:该方法用于删除资源,但资源只能删一次,(删多次的结果和删一次一样)所以属于幂等。
POST:该方法既可以创建资源又可更新资源,但重点在于其对应的不是资源本身,而是资源的定位(URL)。例如:POST http://www.tieba.baidu.com/xxx 像在贴吧发帖一样,两次相同的POST请求会在服务端创建两份资源,而它们是具有不同的URI。所以属于非幂等。
PUT:该方法既可以创建资源又可更新资源,但重点在于其对应的是资源本身(URI),而且前端提供的得是一个完整的资源对象(更新整个对象,就是一个对象里的所有属性都有值),会起到覆盖的作用。例如:PUT http://www.tieba.baidu.com/xxx/1234567 的意思是若没有该1234567资源则创建,若有则更新该1234567资源,
PATCH:该方法用来更新局部资源(只更新一个对象里的某个属性),无法保证幂等性,若局部更新的是x=x+param的话就不是幂等,或是x=param的话就是幂等,所以属于非幂等。
URL可以在客户端确定,那么就使用PUT,如果是在服务端确定,那么就使用POST,比如说很多资源使用数据库自增主键作为标识信息,而创建的资源的标识信息到底是什么只能由服务端提供,这个时候就必须使用POST。
所以在设计接口的时候应当注意其幂等性,尤其是在分布式系统
比如像点赞的需求:
当用户点赞时,将答案的赞数量+1。------若在该接口里的业务逻辑直接将赞+1的话将会使该接口称为非幂等,因为多次点击赞的话,该接口执行多次,赞就会多次+1。
我们可以将其更改为如下:
建立一张点赞表(字段分别有用户、所点赞的答案),当用户点赞时,查询表中该用户是否已经点赞过相关答案,若未点赞过,则将插入一条记录,若已点赞过,则直接返回。
若要统计赞的数量,则可直接使用count来统计。
在sql中需注意哪些是幂等操作,哪些是非幂等操作?
①查询操作(select)
select就是天然的幂等,不管查询一次还是多次,在数据不变的情况下,查询结果都一样。
②删除操作(delete)
delete也是幂等,删除一次和删除多次都是将数据删除,虽然返回的结果可能不一样,但在不考虑返回结果的情况下,删除操作也满足幂等。
③更新操作(update)
update操作有的幂等,有的非幂等。如下
update table set a=10 where a=5 ——幂等
update table set a=a+1 where a=5 ——非幂等
④新增操作(insert)
insert操作也有幂等和非幂等。如下
当设置有主键或唯一键: insert into table (a,b,c) value (1,2,3) 主键与唯一键都是不能插入相同的数据,会报错,所以是—— 幂等
当没有主键也没有唯一键: insert into table (a,b,c) value (1,2,3) 没有主键与唯一键,所以可以插入相同的数据,所以是——非幂等
一般我们使用的数据库表都是需要主键和唯一键的,所以insert通常都是非幂等的,所以有insert操作需要注意重复消费(幂等的问题)的问题。
下面将介绍实现幂等性的方式。
①使用token机制
客户端先向服务器端请求一个全局唯一的token(由服务器生成),将token放入redis(主要运用了setnx)或者jvm内存,客户端再向接口请求的时候携带该token一起请求,服务器会对token做校验保证接口的幂等操作。
【
需注意:
1、redis用删除操作来判断token,删除成功代表token校验通过
2、如果使用select + delete的方式来校验token的话会存在并发问题,不能这样用
3、这里使用的token和jwt或单点登录使用的token不一样,不可以混淆,这里使用的token是用来防止重复消费,而jwt与单点登录的token是用来保持其登录状态。
】
②通过状态标识与使用幂等的sql语句
在设计的时候最好只支持状态的单向改变(不可逆的),像 待支付(0),支付中(1),支付成功(2),支付失败(3),订单超时关闭(4) 这些状态用起来,再加上幂等sql语句,如下:
update order set orderStatus=1 where orderId=‘xxx’ and orderStatus = 0
③使用Post/Redirect/Get 简称PRG设计模式
该方法主要用于避免重复提交form内容的情况,像用户刷新提交响应页面等。
当表单通过POST请求提交的时候,用户在服务端返回响应期间如果重复刷新了响应页面,将会导致原始POST过的内容重复提交,造成重复数据提交。
【注意,这里说的提交表单不是用 ajax 异步请求把数据传到服务器,而是原生的 HTML form 的 submit,不要混淆该模式的使用场景,否则会对该模式产生困惑】
PRG模式通过响应页面Header返回HTTP状态码进行页面跳转替代响应页面跳转过程。使用303或302状态码,具体描述如下。
用户通过POST表单提交, 服务器收到form提交的 Post 请求后,并不是直接返回一个2XX的结果页面,而是返回一个3XX的重定向页面(Redirct),定向到正确的结果页面(Get)。
但是,PRG设计模式并不能适用所有的表单重复提交情况,如以下几种情况:
- 如果用户返回表单页面,重新提交表单的情况
- 用户在服务器端响应到达之前,多次点击提交按钮的时候。(可通过JavaScript控制提交按钮点击次数)
- 由于服务器响应缓慢,用户刷新提交POST请求造成的多次POST请求
- 恶意用户避开客户端预防多次提交手段,进行重复提交请求
④在Session中存放特殊标志
在服务器端,生产一个唯一的ID,将其放入session中,并将其置入表单的隐藏字段中,再将表单页面发送给浏览器。当用户输入信息点击提交,在服务器端就将获取到表单中隐藏字段的值,将其与session中的唯一ID比较,若不相等表示重复提交,若相等就是首次提交,将处理本次请求,再将session中的唯一ID删除。【该方法只针对于单体应用】
⑤乐观锁实现幂等
如果更新已有数据,可以进行加锁更新,也可以使用乐观锁。
-
在设计表结构的时候使用,通过字段version来做乐观锁,这样既能保证幂等,又能保证效率。version字段在更新业务数据时得自增。
(1) 查询数据,得到version版本号;【查询虽然会有并发问题,但是接下来第二步的更新解决了这个问题】
(2) 通过version版本号去更新,版本号匹配就更新,版本号不匹配就不更新;
update table set xxx=xxx-x, version = version + 1 where id=xxx and version =1; -
也可以采用update with condition(更新带条件)(升级版),来实现乐观锁,通过version或者其他条件来实现乐观锁。
version:update table set quality = quality - #subQuaity# ,version = version +1 where id = xxx and version = #version#
其他条件:update table set quality = quality - #sybQuality# where quality - #sybQuality# >= 0
⑥防重表实现幂等性
顾名思义,就是需要一个表,能防止数据重复的表,就叫防重表。该表使用唯一主键去做防重表的唯一索引,比如使用订单号orderNum做为防重表的唯一索引,每次请求会根据订单号去向防重表中插入一条数据,第一次请求查询订单支付状态,订单没有支付,进行支付操作,支付前先向防重表中插入该支付的订单号,插入成功说明可以支付,无论支付成功与否,支付操作执行完后更新订单状态为相对应的状态(成功,失败,或者超时等其他状态),然后就可以将防重表中的数据删除。在此期间重复的请求的订单会因为表中唯一索引而插入失败,直到第一次的请求操作完成(不论成败),所以防重表就像是加上了一把锁一样。
【在这里,可能有小伙伴会问,要是第二个重复请求因为网络延迟等到第一个请求执行完并删除防重表的数据后才到达呢?不就插入成功了吗?
答:所以说这得看业务需求或者配合其他实现幂等的方法一起使用,实际业务中也都是相互配合使用的,不会单单只用一种方法。
若像上面的例子的话,那么在支付逻辑前查询一下就能判断是否已支付了。因为第一个请求是在加锁情况下执行支付逻辑的,所以等到第二个重复请求迟到能够插入防重表数据的时候,证明第一个请求已经执行完了。
】
⑦分布式锁保证幂等性【一般并发的问题我们才会想到锁,然后幂等性也可以用到锁】
在进入方法时,先去获取锁,假如获取到锁,就继续后面的流程。假如没有获取到锁,就等待锁的释放知道获取到锁,当执行完方法时,释放锁,还有,锁要设置超时时间,防止意外宕机或者其他原因导致锁没有释放,该方法可以解决分布式系统的幂等性。
常用的分布式锁实现方案分别有redis和zookeeper。
使用分布式锁类似于防重表,将防重并发放到缓存中,会较为高效,同一时间只能完成一次支付请求。
⑧缓冲队列