redis和redission分布式鎖原理及區(qū)別說明
redis和redission分布式鎖原理及區(qū)別
我最近做租車項(xiàng)目,在處理分布式時用到分布式鎖,我發(fā)現(xiàn)很多同事都在網(wǎng)上找分布式鎖的資料,但是看的資料都不是很全,所以在這里我談?wù)勛约旱姆植际芥i理解。
結(jié)合我的其中某一業(yè)務(wù)需求:多個用戶在同一個區(qū)域內(nèi)發(fā)現(xiàn)只有一輛可租的車,最終結(jié)果肯定只有一位用戶租車成功,這就產(chǎn)生了多線程(多個用戶)搶同一資源的問題。
1、有的同伴想到了synchronized關(guān)鍵字鎖
暫且拋開性能問題,項(xiàng)目為了高可用,都會做集群部署,那么synchronized就失去了加鎖的意義,這里多嘴解釋一下:

2、有的小伙伴可能想到了樂觀鎖
沒錯!!樂觀鎖可以解決的我的問題,但是在高并發(fā)的場景,頻繁的操作數(shù)據(jù)庫,數(shù)據(jù)庫的資源是很珍貴的,并且還存在性能的問題。但是我這里簡單說下樂觀鎖的使用:
- 我們在車的表中添加一個字段:version(int類型)(建議使用這個名稱,這樣別人看到就會直覺這是樂觀鎖字段,也可以使用別的名稱)
- 查詢出該車的數(shù)據(jù),數(shù)據(jù)中就有version字段,假如version=1
select * from u_car where car_id = 10;
- 修改該車的狀態(tài)為鎖定
update u_car set status = 2,version = version +1 where car_id = 10 and version = 1
在修改的時候?qū)ersion作為參數(shù),如果其他用戶鎖車,那么version已經(jīng)發(fā)生變化(version = version +1),所以version = 1不成立,修改失敗
樂觀鎖不是本次的終點(diǎn),但還是簡單說下;
3、使用redis的分布式鎖
public boolean lock(String key, V v, int expireTime){
//獲取鎖
//在redis早期版本中,設(shè)置key和key的存活時間是分開的,設(shè)置key成功,但是設(shè)置存活時間時服務(wù)宕機(jī),那么你的key就永遠(yuǎn)不會過期,有BUG
//后來redis將加鎖和設(shè)置時間用同一個命令
//這里是重點(diǎn),redis.setNx(key,value,time)方法是原子性的,設(shè)置key成功說明鎖車成功,如果失敗說明該車被別人租了
boolean b = false;
try {
b = redis.setNx(key, v, expireTime);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return b;
}
public boolean unlock(String key){
return redis.delete(key);
}
}
但是這樣寫還是存在BUG的,我的key設(shè)置了加鎖時間為5秒,但是我的業(yè)務(wù)邏輯5秒還沒有執(zhí)行完成,key過期了,那么其他用戶執(zhí)行redis.setNx(key, v, expireTime)時就成功了,將該車鎖定,又產(chǎn)生了搶資源;我們想一下,如果我能夠在業(yè)務(wù)邏輯沒有執(zhí)行完的時候,讓鎖過期后能夠延長鎖的時間,是不是就解決了上面的BUG;
實(shí)現(xiàn)這個鎖的延長,非要自己動手的話就得另啟一個線程來監(jiān)聽我們的業(yè)務(wù)線程,每隔1秒監(jiān)測當(dāng)前業(yè)務(wù)線程是否執(zhí)行完成,如果沒有就獲取key的存活時間,時間小于一個閾值時,就自動給key設(shè)置N秒;當(dāng)然,我們可以不用自己動手,redission已經(jīng)幫我們實(shí)現(xiàn)key的時間時間過期問題;
4、使用redission的分布式鎖
//引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
redisson支持單點(diǎn)、集群等模式,這里選擇單點(diǎn)的。
- application.yml配置好redis的連接:
spring:
redis:
host: 127.0.0.1
port: 6379
password:
- 配置redisson的客戶端bean
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Bean(name = {"redisTemplate", "stringRedisTemplate"})
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public Redisson redisson() {
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":6379");
return (Redisson) Redisson.create(config);
}
}
- 加鎖使用
private Logger log = LoggerFactory.getLogger(getClass());
@Resource
private Redisson redisson;
//加鎖
public Boolean lock(String key,long waitTime,long leaseTime){
Boolean b = false;
try {
RLock rLock = redisson.getLock(key);
//說下參數(shù) waitTime:鎖的存活時間 leaseTime:鎖的延長時間 后面的參數(shù)是單位
b = rLock.tryLock(waitTime,leaseTime,TimeUnit.SECONDS);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
return b;
}
//釋放鎖
public void unlock(String key){
try {
RLock rLock = redisson.getLock(key);
if(null!=lock){
lock.unlock();
lock.forceUnlock();
fileLog.info("unlock succesed");
}
} catch (Exception e) {
fileLog.error(e.getMessage(), e);
}
}
- 帶大家看下tryLock方法的實(shí)現(xiàn)源碼:
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
//嘗試獲取鎖,如果沒取到鎖,則獲取鎖的剩余超時時間
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
//如果waitTime已經(jīng)超時了,就返回false
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
//進(jìn)入死循環(huán),反復(fù)去調(diào)用tryAcquire嘗試獲取鎖,ttl為null時就是別的線程已經(jīng)unlock了
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
可以看到,其中主要的邏輯就是嘗試加鎖,成功了就返回true,失敗了就進(jìn)入死循環(huán)反復(fù)去嘗試加鎖。中途還有一些超時的判斷。邏輯還是比較簡單的。
- 再看看tryAcquire方法

- 這個方法的調(diào)用棧也是比較多,之后會進(jìn)入下面這個方法

上面的lua(俗稱膠水語言)腳本比較重要,主要是為了執(zhí)行命令的原子性解釋一下:
- KEYS[1]代表你的key
- ARGV[1]代表你的key的存活時間,默認(rèn)存活30秒
- ARGV[2]代表的是請求加鎖的客戶端ID,后面的1則理解為加鎖的次數(shù),簡單理解就是 如果該客戶端多次對key加鎖時,就會執(zhí)行hincrby原子加1命令
第一段if就是判斷你的key是否存在,如果不存在,就執(zhí)行redis call(hset key ARGV[2],1)加鎖和設(shè)置redis call(pexpire key ARGV[1])存活時間;
當(dāng)?shù)诙€客戶來加鎖時,第一個if判斷已存在key,就執(zhí)行第二個if判斷key的hash是否存在客戶端2的ID,很明顯不是;
則進(jìn)入到最后的return返回該key的剩余存活時間
當(dāng)加鎖成功后會在后臺啟動一個watch dog(看門狗)線程,key的默認(rèn)存活時間為30秒,則watch dog每隔10秒鐘就會檢查一下客戶端1是否還持有該鎖,如果持有,就會不斷的延長鎖key的存活時間
所以這里建議大家在設(shè)置key的存活時間時,最好大于10秒,延續(xù)時間也大于等于10秒
所以,總體流程應(yīng)該是這樣的。

總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
關(guān)于使用IDEA的springboot框架往Redis里寫入數(shù)據(jù)亂碼問題
這篇文章主要介紹了用IDEA的springboot框架往Redis里寫入數(shù)據(jù)亂碼問題,本文給大家分享解決方法通過圖文示例相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
Redis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)
下面小編就為大家?guī)硪黄猂edis優(yōu)化經(jīng)驗(yàn)總結(jié)(必看篇)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03
淺析Redis中紅鎖RedLock的實(shí)現(xiàn)原理
RedLock?是一種分布式鎖的實(shí)現(xiàn)算法,由?Redis?的作者?Salvatore?Sanfilippo(也稱為?Antirez)提出,本文主要為大家詳細(xì)介紹了紅鎖RedLock的實(shí)現(xiàn)原理,感興趣的可以了解下2024-02-02
面試常問:如何保證Redis緩存和數(shù)據(jù)庫的數(shù)據(jù)一致性
在實(shí)際開發(fā)過程中,緩存的使用頻率是非常高的,只要使用緩存和數(shù)據(jù)庫存儲,就難免會出現(xiàn)雙寫時數(shù)據(jù)一致性的問題,那我們又該如何解決呢2021-09-09
詳解Redis緩存預(yù)熱的實(shí)現(xiàn)方法
緩存預(yù)熱是一種在程序啟動或緩存失效之后,主動將熱點(diǎn)數(shù)據(jù)加載到緩存中的策略,本文將給大家分享一下如何實(shí)現(xiàn)Redis的緩存預(yù)熱,文中有詳細(xì)的實(shí)現(xiàn)代碼,需要的朋友可以參考下2023-10-10

