久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

造了一個(gè) Redis 分布鎖的輪子,,沒想到還學(xué)到這么多東西,!

 自由裸奔者 2020-06-30

上篇文章可能舉得例子有點(diǎn)不恰當(dāng),導(dǎo)致有些小伙伴沒看懂為什么余額會變負(fù),。

這次我們舉得實(shí)際一點(diǎn),,還是上篇文章 account 表,假設(shè) id=1,,balance=1000,,不過這次我們扣款 1000,兩個(gè)事務(wù)的時(shí)序圖如下:

這次使用兩個(gè)命令窗口真實(shí)執(zhí)行一把:

注意事務(wù) 2,,③處查詢到  id=1,,balance=1000,但是實(shí)際上由于此時(shí)事務(wù) 1 已經(jīng)提交,,最新結(jié)果如②處所示 id=1,,balance=900

本來 Java 代碼層會做一層余額判斷:

if (balance - amount < 0) {
  throw new XXException('余額不足,,扣減失敗');
}

但是此時(shí)由于 ③ 處使用快照讀,,讀到是個(gè)舊值,未讀到最新值,,導(dǎo)致這層校驗(yàn)失效,,從而代碼繼續(xù)往下運(yùn)行,執(zhí)行了數(shù)據(jù)更新,。

更新語句又采用如下寫法:

UPDATE account set balance=balance-1000 WHERE id =1;

這條更新語句又必須是在這條記錄的最新值的基礎(chǔ)做更新,,更新語句執(zhí)行結(jié)束,這條記錄就變成了 id=1,,balance=-1000,。

之前有朋友疑惑 t12 更新之后,再次進(jìn)行快照讀,結(jié)果會是多少,。

上圖執(zhí)行結(jié)果 ④ 可以看到結(jié)果為 id=1,balance=-1000,可以看到已經(jīng)查詢最新的結(jié)果記錄,。

這行數(shù)據(jù)最新版本由于是事務(wù) 2 自己更新的,自身事務(wù)更新永遠(yuǎn)對自己可見,。

另外這次問題上本質(zhì)上因?yàn)?Java 層與數(shù)據(jù)庫層數(shù)據(jù)不一致導(dǎo)致,,有的朋友留言提出,可以在更新余額時(shí)加一層判斷:

UPDATE account set balance=balance-1000 WHERE id =1 and balance>0;

然后更新完成,,Java 層判斷更新有效行數(shù)是否大于 0,。這種做法確實(shí)能規(guī)避這個(gè)問題。

最后這位朋友留言總結(jié)的挺好,,粘貼一下:

手?jǐn)]分布式鎖

現(xiàn)在切回正文,,這篇文章本來是準(zhǔn)備寫下 Mysql 查詢左匹配的問題,但是還沒研究出來,。那就先寫下最近在鼓搗一個(gè)東西,使用 Redis 實(shí)現(xiàn)可重入分布鎖,。

看到這里,,有的朋友可能會提出來使用 redisson 不香嗎,為什么還要自己實(shí)現(xiàn),?

哎,,redisson 真的很香,但是現(xiàn)有項(xiàng)目中沒辦法使用,,只好自己手?jǐn)]一個(gè)可重入的分布式鎖了,。

雖然用不了 redisson,但是我可以研究其源碼,,最后實(shí)現(xiàn)的可重入分布鎖參考了 redisson 實(shí)現(xiàn)方式,。

分布式鎖

分布式鎖特性就要在于排他性,同一時(shí)間內(nèi)多個(gè)調(diào)用方加鎖競爭,,只能有一個(gè)調(diào)用方加鎖成功,。

Redis 由于內(nèi)部單線程的執(zhí)行,內(nèi)部按照請求先后順序執(zhí)行,,沒有并發(fā)沖突,,所以只會有一個(gè)調(diào)用方才會成功獲取鎖。

而且 Redis 基于內(nèi)存操作,,加解鎖速度性能高,,另外我們還可以使用集群部署增強(qiáng) Redis 可用性。

加鎖

使用 Redis 實(shí)現(xiàn)一個(gè)簡單的分布式鎖,,非常簡單,,可以直接使用 SETNX 命令。

SETNX 是『SET if Not eXists』,如果不存在,,才會設(shè)置,,使用方法如下:

不過直接使用 SETNX 有一個(gè)缺陷,我們沒辦法對其設(shè)置過期時(shí)間,,如果加鎖客戶端宕機(jī)了,,這就導(dǎo)致這把鎖獲取不了了。

有的同學(xué)可能會提出,,執(zhí)行 SETNX 之后,,再執(zhí)行 EXPIRE 命令,主動設(shè)置過期時(shí)間,,偽碼如下:

var result = setnx lock 'client'
if(result==1){
    // 有效期 30 s
    expire lock 30
}

不過這樣還是存在缺陷,,加鎖代碼并不能原子執(zhí)行,如果調(diào)用加鎖語句,,還沒來得及設(shè)置過期時(shí)間,,應(yīng)用就宕機(jī)了,還是會存在鎖過期不了的問題,。

不過這個(gè)問題在 Redis 2.6.12 版本 就可以被完美解決,。這個(gè)版本增強(qiáng)了 SET 命令,可以通過帶上 NX,EX 命令原子執(zhí)行加鎖操作,,解決上述問題,。參數(shù)含義如下:

  • EX second  :設(shè)置鍵的過期時(shí)間,單位為秒
  • NX 當(dāng)鍵不存在時(shí),,進(jìn)行設(shè)置操作,,等同與 SETNX 操作

使用 SET 命令實(shí)現(xiàn)分布式鎖只需要一行代碼:

SET lock_name anystring NX EX lock_time

解鎖

解鎖相比加鎖過程,就顯得非常簡單,,只要調(diào)用 DEL 命令刪除鎖即可:

DEL lock_name

不過這種方式卻存在一個(gè)缺陷,,可能會發(fā)生錯解鎖問題。

假設(shè)應(yīng)用 1 加鎖成功,,鎖超時(shí)時(shí)間為 30s,。由于應(yīng)用 1 業(yè)務(wù)邏輯執(zhí)行時(shí)間過長,30 s 之后,,鎖過期自動釋放,。

這時(shí)應(yīng)用 2 接著加鎖,加鎖成功,,執(zhí)行業(yè)務(wù)邏輯,。這個(gè)期間,應(yīng)用 1 終于執(zhí)行結(jié)束,,使用 DEL 成功釋放鎖,。

這樣就導(dǎo)致了應(yīng)用 1 錯誤釋放應(yīng)用 2 的鎖,,另外鎖被釋放之后,其他應(yīng)用可能再次加鎖成功,,這就可能導(dǎo)致業(yè)務(wù)重復(fù)執(zhí)行,。

為了使鎖不被錯誤釋放,我們需要在加鎖時(shí)設(shè)置隨機(jī)字符串,,比如 UUID,。

SET lock_name uuid NX EX lock_time

釋放鎖時(shí),需要提前獲取當(dāng)前鎖存儲的值,,然后與加鎖時(shí)的 uuid 做比較,,偽代碼如下:

var value= get lock_name
if value == uuid
 // 釋放鎖成功
else
 // 釋放鎖失敗

上述代碼我們不能通過 Java 代碼運(yùn)行,因?yàn)闊o法保證上述代碼原子化執(zhí)行。

幸好 Redis 2.6.0 增加執(zhí)行 Lua 腳本的功能,,lua 代碼可以運(yùn)行在 Redis 服務(wù)器的上下文中,,并且整個(gè)操作將會被當(dāng)成一個(gè)整體執(zhí)行,中間不會被其他命令插入,。

這就保證了腳本將會以原子性的方式執(zhí)行,,當(dāng)某個(gè)腳本正在運(yùn)行的時(shí)候,不會有其他腳本或 Redis 命令被執(zhí)行,。在其他的別的客戶端看來,,執(zhí)行腳本的效果,要么是不可見的,,要么就是已完成的。

EVAL 與 EVALSHA

EVAL

Redis 可以使用 EVAL 執(zhí)行 LUA 腳本,,而我們可以在 LUA 腳本中執(zhí)行判斷求值邏輯,。EVAL 執(zhí)行方式如下:

EVAL script numkeys key [key ...] arg [arg ...]

numkeys 參數(shù)用于鍵名參數(shù),即后面 key 數(shù)組的個(gè)數(shù),。

key [key ...] 代表需要在腳本中用到的所有 Redis key,,在 Lua 腳本使用使用數(shù)組的方式訪問 key,類似如下 KEYS[1] ,, KEYS[2],。注意 Lua 數(shù)組起始位置與 Java 不同,Lua 數(shù)組是從 1 開始,。

命令最后,,是一些附加參數(shù),可以用來當(dāng)做 Redis Key 值存儲的 Value 值,,使用方式如 KEYS 變量一樣,,類似如下:ARGV[1]ARGV[2] ,。

用一個(gè)簡單例子運(yùn)行一下 EVAL 命令:

eval 'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2],ARGV[3]}' 2 key1 key2 first second third

運(yùn)行效果如下:

可以看到 KEYSARGVS內(nèi)部數(shù)組可以不一致,。

在 Lua 腳本可以使用下面兩個(gè)函數(shù)執(zhí)行 Redis 命令:

  • redis.call()
  • redis.pcall()

兩個(gè)函數(shù)作用法與作用完全一致,,只不過對于錯誤的處理方式不一致,感興趣的小伙伴可以具體點(diǎn)擊以下鏈接,,查看錯誤處理一章,。

http://doc./script/eval.html

下面我們統(tǒng)一在 Lua 腳本中使用 redis.call(),執(zhí)行以下命令:

eval 'return redis.call('set',KEYS[1],ARGV[1])' 1 foo 樓下小黑哥

運(yùn)行效果如下:

EVALSHA

EVAL 命令每次執(zhí)行時(shí)都需要發(fā)送 Lua 腳本,,但是 Redis 并不會每次都會重新編譯腳本,。

當(dāng) Redis 第一次收到 Lua 腳本時(shí),首先將會對 Lua 腳本進(jìn)行  sha1 獲取簽名值,,然后內(nèi)部將會對其緩存起來,。后續(xù)執(zhí)行時(shí),直接通過 sha1 計(jì)算過后簽名值查找已經(jīng)編譯過的腳本,,加快執(zhí)行速度,。

雖然 Redis 內(nèi)部已經(jīng)優(yōu)化執(zhí)行的速度,但是每次都需要發(fā)送腳本,,還是有網(wǎng)絡(luò)傳輸?shù)某杀?,如果腳本很大,這其中花在網(wǎng)絡(luò)傳輸?shù)臅r(shí)間就會相應(yīng)的增加,。

所以 Redis 又實(shí)現(xiàn)了 EVALSHA 命令,,原理與 EVAL 一致。只不過 EVALSHA 只需要傳入腳本經(jīng)過 sha1計(jì)算過后的簽名值即可,,這樣大大的減少了傳輸?shù)淖止?jié)大小,,減少了網(wǎng)絡(luò)耗時(shí)。

EVALSHA命令如下:

evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 foo 樓下小黑哥

運(yùn)行效果如下:

SCRIPT FLUSH 命令用來清除所有 Lua 腳本緩存,。

可以看到,,如果之前未執(zhí)行過 EVAL命令,直接執(zhí)行 EVALSHA 將會報(bào)錯,。

優(yōu)化執(zhí)行 EVAL

我們可以結(jié)合使用 EVALEVALSHA,,優(yōu)化程序。下面就不寫偽碼了,,以 Jedis 為例,,優(yōu)化代碼如下:

//連接本地的 Redis 服務(wù)
Jedis jedis = new Jedis('localhost', 6379);
jedis.auth('1234qwer');

System.out.println('服務(wù)正在運(yùn)行: '   jedis.ping());

String lua_script = 'return redis.call('set',KEYS[1],ARGV[1])';
String lua_sha1 = DigestUtils.sha1DigestAsHex(lua_script);

try {
    Object evalsha = jedis.evalsha(lua_sha1, Lists.newArrayList('foo'), Lists.newArrayList('樓下小黑哥'));
} catch (Exception e) {
    Throwable current = e;
    while (current != null) {
        String exMessage = current.getMessage();
        // 包含 NOSCRIPT,代表該 lua 腳本從未被執(zhí)行,,需要先執(zhí)行 eval 命令
        if (exMessage != null && exMessage.contains('NOSCRIPT')) {
            Object eval = jedis.eval(lua_script, Lists.newArrayList('foo'), Lists.newArrayList('樓下小黑哥'));
            break;
        }

    }
}
String foo = jedis.get('foo');
System.out.println(foo);

上面的代碼看起來還是很復(fù)雜吧,,不過這是使用原生 jedis 的情況下。如果我們使用 Spring Boot 的話,,那就沒這么麻煩了,。Spring 組件執(zhí)行的 Eval 方法內(nèi)部就包含上述代碼的邏輯。

不過需要注意的是,,如果 Spring-Boot 使用 Jedis 作為連接客戶端,并且使用Redis  Cluster 集群模式,,需要使用  2.1.9 以上版本的spring-boot-starter-data-redis,不然執(zhí)行過程中將會拋出:

org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.

詳細(xì)情況可以參考這個(gè)修復(fù)的 IssueAdd support for scripting commands with Jedis Cluster

優(yōu)化分布式鎖

講完 Redis 執(zhí)行 LUA 腳本的相關(guān)命令,,我們來看下如何優(yōu)化上面的分布式鎖,使其無法釋放其他應(yīng)用加的鎖,。

以下代碼基于 spring-boot  2.2.7.RELEASE 版本,,Redis 底層連接使用 Jedis。

加鎖的 Redis 命令如下:

SET lock_name uuid NX EX lock_time

加鎖代碼如下:

/**
 * 非阻塞式加鎖,,若鎖存在,,直接返回
 *
 * @param lockName  鎖名稱
 * @param request   唯一標(biāo)識,防止其他應(yīng)用/線程解鎖,,可以使用 UUID 生成
 * @param leaseTime 超時(shí)時(shí)間
 * @param unit      時(shí)間單位
 * @return
 */
public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    // 注意該方法是在 spring-boot-starter-data-redis 2.1 版本新增加的,,若是之前版本 可以執(zhí)行下面的方法
    return stringRedisTemplate.opsForValue().setIfAbsent(lockName, request, leaseTime, unit);
}

由于setIfAbsent方法是在 spring-boot-starter-data-redis 2.1 版本新增加,之前版本無法設(shè)置超時(shí)時(shí)間,。如果使用之前的版本的,,需要如下方法:

/**
 * 適用于 spring-boot-starter-data-redis 2.1 之前的版本
 *
 * @param lockName
 * @param request
 * @param leaseTime
 * @param unit
 * @return
 */
public Boolean doOldTryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
    Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
        RedisSerializer valueSerializer = stringRedisTemplate.getValueSerializer();
        RedisSerializer keySerializer = stringRedisTemplate.getKeySerializer();

        Boolean innerResult = connection.set(keySerializer.serialize(lockName),
                valueSerializer.serialize(request),
                Expiration.from(leaseTime, unit),
                RedisStringCommands.SetOption.SET_IF_ABSENT
        );
        return innerResult;
    });
    return result;
}

解鎖需要使用 Lua 腳本:

-- 解鎖代碼
-- 首先判斷傳入的唯一標(biāo)識是否與現(xiàn)有標(biāo)識一致
-- 如果一致,釋放這個(gè)鎖,,否則直接返回
if redis.call('get', KEYS[1]) == ARGV[1] then
   return redis.call('del', KEYS[1])
else
   return 0
end

這段腳本將會判斷傳入的唯一標(biāo)識是否與 Redis 存儲的標(biāo)示一致,,如果一直,釋放該鎖,,否則立刻返回,。

釋放鎖的方法如下:

/**
 * 解鎖
 * 如果傳入應(yīng)用標(biāo)識與之前加鎖一致,解鎖成功
 * 否則直接返回
 * @param lockName 鎖
 * @param request 唯一標(biāo)識
 * @return
 */
public Boolean unlock(String lockName, String request) {
    DefaultRedisScript<Boolean> unlockScript = new DefaultRedisScript<>();
    unlockScript.setLocation(new ClassPathResource('simple_unlock.lua'));
    unlockScript.setResultType(Boolean.class);
    return stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
}

由于公號外鏈無法直接跳轉(zhuǎn),,關(guān)注『程序通事』,,回復(fù)分布式鎖獲取源代碼。

Redis 分布式鎖的缺陷

無法重入

由于上述加鎖命令使用了 SETNX ,,一旦鍵存在就無法再設(shè)置成功,,這就導(dǎo)致后續(xù)同一線程內(nèi)繼續(xù)加鎖,將會加鎖失敗,。

如果想將 Redis 分布式鎖改造成可重入的分布式鎖,有兩種方案:

  • 本地應(yīng)用使用 ThreadLocal 進(jìn)行重入次數(shù)計(jì)數(shù),,加鎖時(shí)加 1,,解鎖時(shí)減 1,當(dāng)計(jì)數(shù)變?yōu)?0 釋放鎖
  • 第二種,,使用 Redis Hash 表存儲可重入次數(shù),,使用 Lua 腳本加鎖/解鎖

第一種方案可以參考這篇文章分布式鎖的實(shí)現(xiàn)之 redis 篇。第二個(gè)解決方案,,下一篇文章就會具體來聊聊,,敬請期待。

鎖超時(shí)釋放

假設(shè)線程 A 加鎖成功,,鎖超時(shí)時(shí)間為 30s,。由于線程 A 內(nèi)部業(yè)務(wù)邏輯執(zhí)行時(shí)間過長,,30s 之后鎖過期自動釋放。

此時(shí)線程 B 成功獲取到鎖,,進(jìn)入執(zhí)行內(nèi)部業(yè)務(wù)邏輯,。此時(shí)線程 A 還在執(zhí)行執(zhí)行業(yè)務(wù),而線程 B 又進(jìn)入執(zhí)行這段業(yè)務(wù)邏輯,,這就導(dǎo)致業(yè)務(wù)邏輯重復(fù)被執(zhí)行,。

這個(gè)問題我覺得,一般由于鎖的超時(shí)時(shí)間設(shè)置不當(dāng)引起,,可以評估下業(yè)務(wù)邏輯執(zhí)行時(shí)間,,在這基礎(chǔ)上再延長一下超時(shí)時(shí)間。

如果超時(shí)時(shí)間設(shè)置合理,,但是業(yè)務(wù)邏輯還有偶發(fā)的超時(shí),,個(gè)人覺得需要排查下業(yè)務(wù)執(zhí)行過長的問題。

如果說一定要做到業(yè)務(wù)執(zhí)行期間,,鎖只能被一個(gè)線程占有的,,那就需要增加一個(gè)守護(hù)線程,定時(shí)為即將的過期的但未釋放的鎖增加有效時(shí)間,。

加鎖成功后,,同時(shí)創(chuàng)建一個(gè)守護(hù)線程。守護(hù)線程將會定時(shí)查看鎖是否即將到期,,如果鎖即將過期,,那就執(zhí)行 EXPIRE 等命令重新設(shè)置過期時(shí)間。

說實(shí)話,,如果要這么做,,真的挺復(fù)雜的,感興趣的話可以參考下  redisson watchdog 實(shí)現(xiàn)方式,。

Redis 分布式鎖集群問題

為了保證生產(chǎn)高可用,,一般我們會采用主從部署方式。采用這種方式,,我們可以將讀寫分離,,主節(jié)點(diǎn)提供寫服務(wù),從節(jié)點(diǎn)提供讀服務(wù),。

Redis 主從之間數(shù)據(jù)同步采用異步復(fù)制方式,,主節(jié)點(diǎn)寫入成功后,立刻返回給客戶端,,然后異步復(fù)制給從節(jié)點(diǎn),。

如果數(shù)據(jù)寫入主節(jié)點(diǎn)成功,但是還未復(fù)制給從節(jié)點(diǎn),。此時(shí)主節(jié)點(diǎn)掛了,,從節(jié)點(diǎn)立刻被提升為主節(jié)點(diǎn),。

這種情況下,還未同步的數(shù)據(jù)就丟失了,,其他線程又可以被加鎖了,。

針對這種情況, Redis 官方提出一種  RedLock 的算法,,需要有 N 個(gè)Redis 主從節(jié)點(diǎn),,解決該問題,詳情參考:

https:///topics/distlock,。

這個(gè)算法自己實(shí)現(xiàn)還是很復(fù)雜的,,幸好 redisson 已經(jīng)實(shí)現(xiàn)的 RedLock,詳情參考:redisson redlock

總結(jié)

本來這篇文章是想寫 Redis 可重入分布式鎖的,,可是沒想到寫分布式鎖的實(shí)現(xiàn)方案就已經(jīng)寫了這么多,,再寫下去,文章可能就很長,,所以拆分成兩篇來寫,。

嘿嘿,這不下星期不用想些什么了,,真是個(gè)小機(jī)靈鬼~

好了,,幫大家再次總結(jié)一下本文內(nèi)容。

簡單的 Redis 分布式鎖的實(shí)現(xiàn)方式還是很簡單的,,我們可以直接用 SETNX/DEL  命令實(shí)現(xiàn)加解鎖,。

不過這種實(shí)現(xiàn)方式不夠健壯,可能存在應(yīng)用宕機(jī),,鎖就無法被釋放的問題,。

所以我們接著引入以下命令以及 Lua 腳本增強(qiáng) Redis 分布式鎖。

SET lock_name anystring NX EX lock_time

最后 Redis 分布鎖還是存在一些缺陷,,在這里提出一些解決方案,,感興趣同學(xué)可以自己實(shí)現(xiàn)一下。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多