redis 解決并發(fā) java
1、為什么要使用分布式鎖
如果在一個(gè)分布式系統(tǒng)中,我們從數(shù)據(jù)庫(kù)中讀取一個(gè)數(shù)據(jù),然后修改保存,這種情況很容易遇到并發(fā)問(wèn)題。因?yàn)樽x取和更新保存不是一個(gè)原子操作,在并發(fā)時(shí)就會(huì)導(dǎo)致數(shù)據(jù)的不正確。這種場(chǎng)景其實(shí)并不少見(jiàn),比如電商秒殺活動(dòng),庫(kù)存數(shù)量的更新就會(huì)遇到。如果是單機(jī)應(yīng)用,直接使用本地鎖就可以避免。如果是分布式應(yīng)用,本地鎖派不上用場(chǎng),這時(shí)就需要引入分布式鎖來(lái)解決。
由此可見(jiàn)分布式鎖的目的其實(shí)很簡(jiǎn)單,就是為了保證多臺(tái)服務(wù)器在執(zhí)行某一段代碼時(shí)保證只有一臺(tái)服務(wù)器執(zhí)行。
2、為了保證分布式鎖的可用性,至少要確保鎖的實(shí)現(xiàn)要同時(shí)滿足以下幾點(diǎn):
互斥性。在任何時(shí)刻,保證只有一個(gè)客戶端持有鎖。
不能出現(xiàn)死鎖。如果在一個(gè)客戶端持有鎖的期間,這個(gè)客戶端崩潰了,也要保證后續(xù)的其他客戶端可以上鎖。
保證上鎖和解鎖都是同一個(gè)客戶端。
3、一般來(lái)說(shuō),實(shí)現(xiàn)分布式鎖的方式有以下幾種:
使用MySQL,基于唯一索引。
使用ZooKeeper,基于臨時(shí)有序節(jié)點(diǎn)。
使用Redis,基于set命令(2.6.12 版本開始)。
本篇文章主要講解Redis的實(shí)現(xiàn)方式。
4、用到的redis命令
鎖的實(shí)現(xiàn)主要基于redis的SET命令(SET詳細(xì)解釋參考這里),我們來(lái)看SET的解釋:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
將字符串值 value 關(guān)聯(lián)到 key 。
如果 key 已經(jīng)持有其他值, SET 就覆寫舊值,無(wú)視類型。
對(duì)于某個(gè)原本帶有生存時(shí)間(TTL)的鍵來(lái)說(shuō), 當(dāng) SET 命令成功在這個(gè)鍵上執(zhí)行時(shí), 這個(gè)鍵原有的 TTL 將被清除。
可選參數(shù)
從 Redis 2.6.12 版本開始, SET 命令的行為可以通過(guò)一系列參數(shù)來(lái)修改:
EX second :設(shè)置鍵的過(guò)期時(shí)間為 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :設(shè)置鍵的過(guò)期時(shí)間為 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在鍵不存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在鍵已經(jīng)存在時(shí),才對(duì)鍵進(jìn)行設(shè)置操作。
加鎖:使用SET key value [PX milliseconds] [NX]命令,如果key不存在,設(shè)置value,并設(shè)置過(guò)期時(shí)間(加鎖成功)。如果已經(jīng)存在lock(也就是有客戶端持有鎖了),則設(shè)置失敗(加鎖失敗)。
解鎖:使用del命令,通過(guò)刪除鍵值釋放鎖。釋放鎖之后,其他客戶端可以通過(guò)set命令進(jìn)行加鎖。
5、上面第二項(xiàng),說(shuō)了分布式鎖,要考慮的問(wèn)題,下面講解一下
5.1、互斥性。在任何時(shí)刻,保證只有一個(gè)客戶端持有鎖
redis命令是原子性的,只要客戶端調(diào)用redis的命令SET key value [PX milliseconds] [NX] 執(zhí)行成功,就算加鎖成功了
5.2、不能出現(xiàn)死鎖。如果在一個(gè)客戶端持有鎖的期間,這個(gè)客戶端崩潰了,也要保證后續(xù)的其他客戶端可以上鎖。
set命令px設(shè)置了過(guò)期時(shí)間,key過(guò)期失效了,就能避免死鎖了
5.3保證上鎖和解鎖都是同一個(gè)客戶端。
釋放鎖(刪除key)的時(shí)候,只要確保是當(dāng)前客戶端設(shè)置的value才去刪除key即可,采用lua腳本來(lái)實(shí)現(xiàn)
在Redis中,執(zhí)行Lua語(yǔ)言是原子性,也就是說(shuō)Redis執(zhí)行Lua的時(shí)候是不會(huì)被中斷的,具備原子性,這個(gè)特性有助于Redis對(duì)并發(fā)數(shù)據(jù)一致性的支持。
6、java代碼實(shí)現(xiàn)
先把需要的jar包引入
? ? ? ?<dependency> ? ? ? ? ? ?<groupId>redis.clients</groupId> ? ? ? ? ? ?<artifactId>jedis</artifactId> ? ? ? ? ? ?<version>2.9.3</version> ? ? ? ?</dependency>
加鎖設(shè)置參數(shù)的實(shí)體類
import lombok.Data;//加鎖設(shè)置的參數(shù)
redis分布式具體代碼實(shí)現(xiàn)
import lombok.extern.slf4j.Slf4j;import redis.clients.jedis.Jedis;import java.util.Collections;import java.util.UUID;/**
* redis分布式鎖
*/
redis分布式鎖使用
import lombok.extern.slf4j.Slf4j;
這是代碼在執(zhí)行過(guò)程中,通過(guò)redis可視化工具看到的效果,可以參考一下~

控制臺(tái)日志打印結(jié)果
15:02:28.569 [main] INFO com.test.test - 下面測(cè)試兩個(gè)線程同時(shí),搶占鎖的結(jié)果15:02:28.645 [我是線程2] INFO com.test.test - 加鎖結(jié)果:true15:02:30.618 [我是線程1] INFO com.test.test - 加鎖結(jié)果:false15:02:30.620 [我是線程1] INFO com.test.test - 釋放鎖結(jié)果:false15:02:33.652 [我是線程2] INFO com.test.test - 釋放鎖結(jié)果:true15:02:48.614 [main] INFO com.test.test - -----------------我是一條分割線----------------15:02:48.614 [main] INFO com.test.test -
15:02:48.614 [main] INFO com.test.test -
15:02:48.614 [main] INFO com.test.test -
15:02:48.614 [main] INFO com.test.test - 下面是測(cè)試 ?一個(gè)線程獲取鎖成功后,由于業(yè)務(wù)執(zhí)行時(shí)間超過(guò)了設(shè)置持有鎖的時(shí)間,是否會(huì)把其他線程持有的鎖給釋放掉15:02:48.616 [我是線程3] INFO com.test.test - 加鎖結(jié)果:true15:02:50.645 [我是線程4] INFO com.test.test - 加鎖結(jié)果:true15:02:55.647 [我是線程4] INFO com.test.test - 釋放鎖結(jié)果:true15:02:58.621 [我是線程3] INFO com.test.test - 釋放鎖結(jié)果:false
可以看到多個(gè)線程競(jìng)爭(zhēng)一把鎖的時(shí)候,保證了只有一個(gè)線程持有鎖
分割線下面的日志也能看出,一個(gè)線程持有了鎖,由于處理業(yè)務(wù)代碼時(shí)間,超過(guò)了設(shè)置持有鎖的時(shí)間,通過(guò)lua腳本釋放鎖的時(shí)候,也不會(huì)把其他線程持有的鎖給釋放掉,保證了安全釋放了鎖
7、分布式鎖 實(shí)際使用中需要注意的一些問(wèn)題
假設(shè)有這樣一個(gè)場(chǎng)景: 有一個(gè)修改訂單狀態(tài)的接口,訂單狀態(tài)修改為失敗,就不允許在修改為其他狀態(tài)了;
在單臺(tái)機(jī)器上,在代碼方法上加了synchronized來(lái)做并發(fā)控制,由于代碼邏輯比較復(fù)雜,現(xiàn)在它的TPS是1,一秒就只能處理一個(gè)訂單。
后面對(duì)這個(gè)系統(tǒng)做集群,部署了一百臺(tái),那么這個(gè)接口性能就提升了100倍了。
但是synchronized是進(jìn)程級(jí)別的鎖,在集群環(huán)境下synchronized沒(méi)辦法控制其他服務(wù)器下線程并發(fā)訪問(wèn) 臨界代碼了,后面就采用了分布式鎖來(lái)做并發(fā)控制。
7.1、那么使用分布鎖要注意什么了?
7.1.1、鎖粒度
如果分布式鎖的key 設(shè)置的是?redisLock:updateOrderStatus?相當(dāng)于集群下對(duì)這個(gè)接口加了相同的一把大鎖,按照上面那個(gè)場(chǎng)景TPS就變成1了,集群部署就浪費(fèi)了。
7.1.2、那么如何控制鎖粒度了?
平常我們修改訂單的時(shí)候都有訂單號(hào),那么分布式的key可以設(shè)置為:redisLock:updateOrderStatus:{orderCode}?,{orderCode}執(zhí)行的時(shí)候動(dòng)態(tài)的替換成訂單編號(hào),那么鎖粒度就控制到這條訂單了,就跟數(shù)據(jù)庫(kù)從表鎖 變成了行鎖一樣,接口支持更高的并發(fā)了。
7.1.3、獲取鎖時(shí)間
如果時(shí)間設(shè)置的太長(zhǎng):用戶就會(huì)等待太久才能得到響應(yīng)結(jié)果
太短:頻繁獲取鎖失敗,用戶體驗(yàn)性也不好
只能按照不同的業(yè)務(wù)代碼,由開發(fā)人員來(lái)衡量設(shè)置多長(zhǎng)的時(shí)間
7.1.4、持有鎖時(shí)間:
如果鎖粒度比較小,時(shí)間可以設(shè)置長(zhǎng)一點(diǎn),就算執(zhí)行比較慢,影響面比較小可以接受
7.1.5、難道每次想使用分布式鎖的時(shí)候都需要下面流程一樣,在編碼一次?有什么辦法能優(yōu)化嗎?
1、先創(chuàng)建一個(gè) 分布式鎖對(duì)象;RedisLock redisLock = new RedisLock(lockParam);
2、加鎖;Boolean lockFlag = redisLock.lock();
3、finally 解鎖;redisLock.unlock();
鏈接:https://www.dianjilingqu.com/628686.html