溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

高并發(fā)核心技術(shù) - 冪等性與分布式鎖

發(fā)布時(shí)間:2020-06-06 19:41:45 來(lái)源:網(wǎng)絡(luò) 閱讀:525 作者:Java_老男孩 欄目:編程語(yǔ)言

高并發(fā)核心技術(shù)之 - 冪等性

1. 什么是冪等性

冪等性就是指:一個(gè)冪等操作任其執(zhí)行多次所產(chǎn)生的影響均與一次執(zhí)行的影響相同。
用數(shù)學(xué)的概念表達(dá)是這樣的: f(f(x)) = f(x).
就像 nx1 = n 一樣, x1 就是一個(gè)冪等操作。無(wú)論是乘以多少次結(jié)果都一樣。

2. 常見(jiàn)的冪等性問(wèn)題

冪等性問(wèn)題經(jīng)常會(huì)是由網(wǎng)絡(luò)問(wèn)題引起的,還有重復(fù)操作引起的。

場(chǎng)景一:比如點(diǎn)贊功能,一個(gè)用戶(hù)只能對(duì)同一片文章點(diǎn)贊一次,重復(fù)點(diǎn)贊提示已經(jīng)點(diǎn)過(guò)贊了。

示例代碼:

    public void like(Article article,User user) {
    //檢查是否點(diǎn)過(guò)贊
    if (checkIsLike(article,user)) {
    //點(diǎn)過(guò)贊了
    throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
    //保存點(diǎn)贊
    saveLike(article,user);
}
}
</pre>

看上去好像沒(méi)有什么問(wèn)題,保存點(diǎn)贊之前已經(jīng)檢查過(guò)是否點(diǎn)贊了,理論上同一個(gè)人不會(huì)對(duì)同一篇文章重復(fù)點(diǎn)贊。但實(shí)際不是這樣的。因?yàn)榫W(wǎng)絡(luò)請(qǐng)求不是排隊(duì)進(jìn)來(lái)的,而是一窩蜂涌進(jìn)來(lái)的。

某些時(shí)候,用戶(hù)網(wǎng)絡(luò)不好,可能很短的時(shí)間內(nèi)點(diǎn)擊了多次,由于網(wǎng)絡(luò)傳輸問(wèn)題,這些請(qǐng)求可能會(huì)同時(shí)來(lái)到我們的服務(wù)器。

  • 第一個(gè)請(qǐng)求 checkIsLike() 返回 false , 正在執(zhí)行 saveLike() 操作,還沒(méi)來(lái)的及提交事務(wù)
  • 第二個(gè)請(qǐng)求過(guò)來(lái)了 ,checkIsLike() 返回 也是 false , 并去 執(zhí)行了 saveLike() 操作

這樣子,就造成了一個(gè)用戶(hù)同時(shí)對(duì)一篇文章進(jìn)行了多次點(diǎn)贊操作。

這就是典型的冪等性問(wèn)題, 操作了一次和操作了兩次結(jié)果不一樣,因?yàn)槟愣帱c(diǎn)了一次贊,按照冪等性原則 不管你點(diǎn)擊了多少次結(jié)果都一樣,只點(diǎn)了一次贊。

很多場(chǎng)景都是這樣造成的,比如用戶(hù)重復(fù)下單,重復(fù)評(píng)論,重復(fù)提交表單等。

那怎么解決呢?
假設(shè)網(wǎng)絡(luò)的請(qǐng)求是排隊(duì)進(jìn)來(lái)的就不會(huì)出現(xiàn)這個(gè)問(wèn)題了。

于是我們可以改成這樣:

public synchronized void like(Article article,User user) {
    //檢查是否點(diǎn)過(guò)贊
if (checkIsLike(article,user)) {
    //點(diǎn)過(guò)贊了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
    //保存點(diǎn)贊
saveLike(article,user);
}
}
</pre>

synchronized 同步鎖 這樣我們的請(qǐng)求就會(huì)乖乖的排隊(duì)進(jìn)來(lái)了。

PS :這樣做是效率比較低的做法,不建議這么做,只是舉例子,synchronized 也不適合分布式集群場(chǎng)景。

場(chǎng)景二 : 第三方回調(diào)

我們系統(tǒng)經(jīng)常需要和第三方系統(tǒng)打交道,比如微信充值,支付寶充值什么的,微信和支付寶常常會(huì)以回調(diào)你的接口通知你支付結(jié)果。為了保證你能收到回調(diào),往往可能會(huì)回調(diào)多次。

有時(shí)候我們也為了保證數(shù)據(jù)的準(zhǔn)確性會(huì)有個(gè)定時(shí)器去查詢(xún)支付結(jié)果未知的流水,并執(zhí)行響應(yīng)的處理。
如果定時(shí)器的輪訓(xùn)和回調(diào)剛好是在同時(shí)進(jìn)行,這可能又出BUG了,又進(jìn)行了兩次重復(fù)操作。

那么問(wèn)題來(lái)了
假設(shè)我是一個(gè)充值操作, 回調(diào)回來(lái)的時(shí)候 ,會(huì)做業(yè)務(wù)處理,成功了給用戶(hù)賬戶(hù)加錢(qián)。這是后就要保證冪等性了, 假設(shè)微信同一筆交易給你回調(diào)了兩次,如果你給用戶(hù)充值了兩次,這顯然不合理(我是老板肯定扣你工資),所以要保證 不管微信回調(diào)你多少次 ,同一筆交易你只能給用戶(hù)充一次錢(qián)。這就冪等性。

解決冪等性問(wèn)題方案

  • synchronized
    適合單機(jī)應(yīng)用,不追求性能 ,不追求并發(fā)。
  • 分布式鎖
    但是往往我們的應(yīng)用是分布式的集群,并且很講究性能,并發(fā),所以我們需要用到 分布式鎖 來(lái)解決這個(gè)問(wèn)題。

Redis 分布式鎖:

/**
* setNx
*
*  @param key
*  @param value
*  @return
*/
public Boolean setNx(String key,Object value) {
    return redisTemplate.opsForValue().setIfAbsent(key,value);
}
/**
*  @param key 鎖
*  @param waitTime 等待時(shí)間  毫秒
*  @param expireTime 超時(shí)時(shí)間  毫秒
*  @return
*/
public Boolean lock(String key,Long waitTime,Long expireTime) {
    String vlaue =  UUIDUtil.mongoObjectId();
    Boolean flag = setNx(key,vlaue);
    //嘗試獲取鎖  成功返回
if (flag) {
    redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
    return flag;
}
else {
    //失敗
//現(xiàn)在時(shí)間
long newTime =  System.currentTimeMillis();
    //等待過(guò)期時(shí)間
long loseTime = newTime + waitTime;
    //不斷嘗試獲取鎖成功返回
while (System.currentTimeMillis()  < loseTime) {
    Boolean testFlag = setNx(key,vlaue);
    if (testFlag) {
    redisTemplate.expire(key,expireTime,TimeUnit.MILLISECONDS);
    return testFlag;
}
//休眠100毫秒
try {
    Thread.sleep(100);
}
catch (InterruptedException e) {
    e.printStackTrace();
}
}}return false;}/**
*  @param key
*  @return
*/
public Boolean lock(String key) {
    return lock(key,1000L,60  *  1000L);
}
/**
*  @param key
*/
public void unLock(String key) {
    remove(key);
}
</pre>

利用Redis 分布式鎖 我們的代碼可以改成這樣:

public void like(Article article,User user) {
    String key =  "key:like"  + article.getId()  +  ":"  + user.getUserId();
    //  等待鎖的時(shí)間  0  ,  過(guò)期時(shí)間  一分鐘防止死鎖
boolean flag = redisService.lock(key,0,60  *  1000L);
    if(!flag) {
    //獲取鎖失敗  說(shuō)明前面的請(qǐng)求已經(jīng)獲取了鎖
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
//檢查是否點(diǎn)過(guò)贊
if (checkIsLike(article,user)) {
    //點(diǎn)過(guò)贊了
throw new ApiException(CodeEnums.SYSTEM_ERR);
}
else {
    //保存點(diǎn)贊
saveLike(article,user);
}
//刪除鎖
redisService.unLock(key);
}
</pre>

key 的設(shè)計(jì)也很講究:
數(shù)據(jù)不沖突的兩個(gè)業(yè)務(wù)場(chǎng)景,key不能沖突,不同人的key也不一樣,不同的文章Key也不一樣。
根據(jù)場(chǎng)景業(yè)務(wù)設(shè)定。

一個(gè)原則: 盡可能的縮小key的范圍。?這樣才能增強(qiáng)我們的并發(fā)。

首先我們先獲取鎖,獲取鎖成功 執(zhí)行完操作,保存數(shù)據(jù) ,刪除鎖。獲取不到鎖返回失敗。設(shè)置過(guò)期時(shí)間是為了防止‘死鎖’,比如機(jī)器獲取到了 鎖,沒(méi)有設(shè)置過(guò)期時(shí)間,但是他死機(jī)了,沒(méi)有刪除釋放鎖。

  • 版本號(hào)控制
    CAS 算法: CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。這個(gè)比較繁雜,有興趣的同學(xué)可以去看看。
向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI