概念
接口冪等性是指對(duì)同一操作的一次請(qǐng)求或多次請(qǐng)求返回的結(jié)果是一致的,不會(huì)因?yàn)槎啻握?qǐng)求就產(chǎn)生不一樣的結(jié)果,比如數(shù)據(jù)庫(kù)的select操作,可以看成是冪等性的,而插入和更新操作,則要保證重復(fù)提交造成的數(shù)據(jù)重復(fù)或者數(shù)據(jù)不正確的問(wèn)題。
比如新增操作,假設(shè)名稱不能重復(fù),那么往往就要保證在新增過(guò)程中使數(shù)據(jù)庫(kù)不要出現(xiàn)同名的數(shù)據(jù),這是典型的防重操作。
比如更新操作,假設(shè)每次更新數(shù)值加一,那么就要保證在新增過(guò)程中不會(huì)因?yàn)橹貜?fù)請(qǐng)求而導(dǎo)致數(shù)值增加錯(cuò)亂,要保證冪等性,如果只是update 表 set is_enable = 0或1這種固定的值,就沒(méi)關(guān)系,這種操作是冪等性的。
防重,顧名思義,防止數(shù)據(jù)庫(kù)產(chǎn)生重復(fù)數(shù)據(jù),對(duì)返回的結(jié)果是沒(méi)什么要求的,所以如果是冪等性處理,在數(shù)據(jù)防重的基礎(chǔ)上,還要把重復(fù)提交的請(qǐng)求返回的結(jié)果和第一次提交的請(qǐng)求返回的結(jié)果保持一致,比如統(tǒng)一返回操作成功的提示,但只有第一次是真正執(zhí)行了。
適用場(chǎng)景
1.前端如果忘了做遮罩層,快速點(diǎn)擊多次新增的情況下,數(shù)據(jù)庫(kù)極易出現(xiàn)重復(fù)數(shù)據(jù);
2.微服務(wù)間調(diào)用接口超時(shí)的時(shí)候,如果配置了重試機(jī)制,也很容易出現(xiàn)重復(fù)請(qǐng)求的情況;
3.在使用rocketMq消息隊(duì)列在消費(fèi)消息的時(shí)候,有時(shí)也會(huì)同時(shí)有重復(fù)消費(fèi)的情況,比如下面兩種情況:
① 消息生產(chǎn)者沒(méi)有收到消息隊(duì)列收到消息的應(yīng)答,重試機(jī)制使得重復(fù)產(chǎn)生消息。
比如網(wǎng)絡(luò)故障導(dǎo)致應(yīng)答消息丟失或者消息太多 ,應(yīng)答消息傳回受到阻塞,生產(chǎn)者等待超時(shí)。
②消息已經(jīng)到達(dá)消息隊(duì)列,但發(fā)送給消費(fèi)者的時(shí)候,沒(méi)有收到來(lái)自消費(fèi)者的回復(fù)消息,或者消息中間件更改消息狀態(tài)出現(xiàn)問(wèn)題。
常用的解決方案
數(shù)據(jù)新增的時(shí)候通常先從數(shù)據(jù)庫(kù)select確認(rèn)數(shù)據(jù)不存在后再插入,但是這樣依然無(wú)法避免多次連擊的情況(實(shí)測(cè),只要夠快),所以思路大體分成兩個(gè)方向,一個(gè)是用數(shù)據(jù)庫(kù)方式解決,一個(gè)是非數(shù)據(jù)庫(kù)的方式。
數(shù)據(jù)庫(kù)方向可分為:悲觀鎖、樂(lè)觀鎖、加唯一索引、建防重表(本質(zhì)上還是唯一索引),加狀態(tài)字段,更新完改變狀態(tài)(本質(zhì)上和樂(lè)觀鎖很像)
非數(shù)據(jù)庫(kù)方向可分為:借用第三方緩存解決,比如redis或者結(jié)合前端用token.
先說(shuō)一下我覺得這里面最適合的方式,用數(shù)據(jù)庫(kù)解決會(huì)加重?cái)?shù)據(jù)庫(kù)的性能負(fù)擔(dān),而token的方案,要請(qǐng)求兩次,而且要前端配合(既然都要前端配合了,不如直接讓前端加個(gè)遮罩層來(lái)的快速方便)所以優(yōu)先考慮用redis做分布式鎖的方式,下面也會(huì)稍微介紹下每個(gè)方式的原理。
redis分布式鎖
比如新增用戶的接口,請(qǐng)求開始時(shí),先把不能重復(fù)的字段比如用戶名放到redis緩存里,可以用set或者setnx都行,設(shè)置比如兩秒的過(guò)期時(shí)間(可根據(jù)業(yè)務(wù)自行設(shè)置),然后再select數(shù)據(jù)庫(kù)里有沒(méi)這個(gè)用戶,如果沒(méi)有則插入,插入成功之后刪除redis里的緩存,返回操作成功。此時(shí),如果有其他重復(fù)的請(qǐng)求進(jìn)來(lái),redis里已經(jīng)有緩存的情況下,則直接返回操作成功就好了,或者返回?cái)?shù)據(jù)已存在的提示,但是不會(huì)再往下執(zhí)行。
這種方法應(yīng)該是最簡(jiǎn)單的,而且效率最高的方法了。原理和數(shù)據(jù)庫(kù)的分布式鎖差不多,只是放在了redis里不會(huì)影響數(shù)據(jù)庫(kù)的性能。但是關(guān)鍵最后一定要?jiǎng)h除redis里的緩存。
token方案
需要讓前端先請(qǐng)求后臺(tái)生成token的接口,然后前端請(qǐng)求的時(shí)候header里帶上這個(gè)token,后臺(tái)拿到token之后放入redis里,設(shè)置緩存過(guò)期時(shí)間,再去數(shù)據(jù)庫(kù)select看數(shù)據(jù)存不存在,不存在則新增,成功之后刪除redis里的token,這種方案,要求前端那邊請(qǐng)求兩次,一次拿token,一次業(yè)務(wù)請(qǐng)求帶上token,而且快速點(diǎn)擊時(shí),多次請(qǐng)求帶的都是同一個(gè)token, 這個(gè)原理上還是分布式鎖,只是可以全局配置,并不關(guān)心具體是什么字段,但是要請(qǐng)求方在請(qǐng)求之前先拿到唯一的token,如果連拿token這個(gè)請(qǐng)求都是重復(fù)的話就不太容易判斷了。
數(shù)據(jù)庫(kù)悲觀鎖
利用select for update鎖住數(shù)據(jù)庫(kù)中的一行數(shù)據(jù),更新完之后別的請(qǐng)求才能更新這條數(shù)據(jù),比如:
select * from user where id=1 for update;
這樣再更新這一行的數(shù)值做加減運(yùn)算的時(shí)候就不怕用其他并發(fā)請(qǐng)求過(guò)來(lái)引發(fā)數(shù)據(jù)錯(cuò)亂問(wèn)題了,比如:
update user set like_num = like_num+1 where id = 1;
但是這種需要注意,如果是mysql,存儲(chǔ)引擎必須用支持事物的innodb,這里id字段一定要是主鍵或者唯一索引,不然會(huì)鎖住整張表。
數(shù)據(jù)庫(kù)樂(lè)觀鎖
給要操作的行加個(gè)版本字段,每次更新前先查出這一行數(shù)據(jù)的版本,更新的時(shí)候指定這一行數(shù)據(jù)和版本,并且版本id+1,比如:
update user set like_num = like_num+1,version = version+1
where id = 1 and version = 1;
這樣即便有重復(fù)請(qǐng)求,但是更新之后version變成了2,那么再更新version=1的必然不會(huì)生效。
以上兩種數(shù)據(jù)庫(kù)操作并不會(huì)解決新增數(shù)據(jù)重復(fù)的問(wèn)題,能解決更新數(shù)據(jù)數(shù)值計(jì)算錯(cuò)亂的問(wèn)題。
加唯一索引
這個(gè)基本是數(shù)據(jù)庫(kù)版的分布式鎖,有效解決數(shù)據(jù)重復(fù)問(wèn)題,當(dāng)插入重復(fù)字段數(shù)據(jù)時(shí),會(huì)拋異常,不會(huì)插入成功。
建防重表
這種是把上面的唯一索引單獨(dú)建一張表,這樣就能在需要防重的時(shí)候先插入防重表來(lái)防重,不需要防重的時(shí)候又能存儲(chǔ)這個(gè)字段的重復(fù)數(shù)據(jù),更加靈活一點(diǎn),這個(gè)思路把這個(gè)防重表放到redis里就是redis分布式鎖了,其實(shí)思路差不多,只是一個(gè)在數(shù)據(jù)庫(kù),一個(gè)借助第三方緩存。
加狀態(tài)字段
比如活動(dòng)待審核-審核中-已審核-執(zhí)行-結(jié)束-刪除等流程操作的時(shí)候,每次更新都會(huì)造成狀態(tài)的改變,這個(gè)就像是上面加了版本字段一個(gè)原理,更新的時(shí)候指定狀態(tài),更新完之后狀態(tài)也改變了,這樣重復(fù)的請(qǐng)求過(guò)來(lái)就不會(huì)更新成功,比如:
update activity set status = 2 where status = 1 and id = 1;
總結(jié)
綜上,感覺redis緩存分布式鎖的方式是效率最高的,推薦這個(gè)。
不同業(yè)務(wù)需求可以結(jié)合使用,會(huì)更香。
比如轉(zhuǎn)賬場(chǎng)景,先把訂單id放入redis緩存做分布式鎖,然后用數(shù)據(jù)庫(kù)樂(lè)觀鎖加個(gè)版本字段控制,會(huì)更加保險(xiǎn)一點(diǎn)。