Redis 單線程:
網(wǎng)絡(luò)IO和鍵值對讀寫是由一個線程完成的(網(wǎng)絡(luò)請求模塊和數(shù)據(jù)操作模塊是單線程的)
1,使用set加鎖。
1)set key value [EX seconds] [PX milliseconds] [NX|XX]
NX:set if not exists
XX:set if already exists
EX:seconds
PX:milliseconds
set zq 3 EX 10000 NX支持設(shè)置EX失效時間,NX表示key不存在時才設(shè)置。
2)在刪除key時,將內(nèi)容對比(確定是否是自身線程擁有的鎖),一致才進行刪除。
3)支持可重入:循環(huán)+唯一id
2,MyRedisLock
/**
* RedisLock 分布式鎖
*/
public class MyRedisLock {
public static final String RELEASE_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
private static final int DEFAULT_LOCK_EXPIRE_TIME_MILLIS = 10 * 1000;//默認(rèn)的lock key的過期時間
private static final int DEFAULT_TRY_LOCK_TIMEOUT_MILLIS = 5 * 1000;//嘗試加鎖的超時時間
private static final int CHECK_RELEASE_MAX_INTERVAL_MILLIS = 100;//檢查過期的間隔
private Jedis jedis;//節(jié)點客戶端
private String lockKey;//加鎖的key
private String lockId;//加鎖的value
private volatile long lockHoldingTime = 0;//鎖的持有時間
public MyRedisLock(Jedis jedis, String lockKey, String lockId){
this.jedis = jedis;
this.lockKey = lockKey;
this.lockId = lockId;
}
/**
* 主動釋放鎖
*/
public synchronized boolean release(){
Object result = jedis.eval(RELEASE_LUA_SCRIPT, Collections.singletonList(lockKey), Collections.singletonList(lockId));
lockHoldingTime = 0;
return result != null && (Integer)result > 0;
}
/**
* 判斷是否加鎖成功
*/
public boolean isAcquired(){
return lockHoldingTime >= System.currentTimeMillis();
}
/**
* 阻塞嘗試使用默認(rèn)超時
*/
public synchronized boolean acquire() throws InterruptedException{
return acquire(DEFAULT_TRY_LOCK_TIMEOUT_MILLIS);
}
/**
* 阻塞嘗試加鎖
*/
public synchronized boolean acquire(int lockAcquireTimeoutMillis) throws InterruptedException {
if (lockAcquireTimeoutMillis <= 0) {
throw new IllegalArgumentException("lockAcquireTimeoutMillis <= 0");
}
boolean isAcquired = false;
long startTimeNanos = System.nanoTime();
try {
int retry = 0;
while (lockAcquireTimeoutMillis > 0) {
String reply = jedis.set(lockKey, lockId, "NX", "PX", DEFAULT_LOCK_EXPIRE_TIME_MILLIS);
if ("OK".equals(reply)) {
lockHoldingTime = System.currentTimeMillis() + DEFAULT_LOCK_EXPIRE_TIME_MILLIS;
isAcquired = true;
break;
}
//支持可重入特性
if (lockId.equals(jedis.get(lockKey))) {
isAcquired = true;
break;
}
// exponential backoff sleep starting at 16 milliseconds and maximum 100 milliseconds
int sleep = 8 << ++retry;
if (sleep > CHECK_RELEASE_MAX_INTERVAL_MILLIS || sleep < 0) {
sleep = CHECK_RELEASE_MAX_INTERVAL_MILLIS;
}
lockAcquireTimeoutMillis -= sleep;
Thread.sleep(sleep);
}
return isAcquired;
} finally {
long elapsed = System.nanoTime() - startTimeNanos;
System.out.println("lockKey:" + lockKey + ", lockId:" + lockId + ", elapsed:" + elapsed);
}
}
}
3,eval執(zhí)行l(wèi)ua腳本
1)
eval script numkeys key [key ...] arg [arg ...]eval 腳本 key的數(shù)量 key列表 附加參數(shù)列表
KEYS key數(shù)組:KEYS[1]獲取第一個key;
ARGV 參數(shù)數(shù)組:ARGV[1]獲取第一個附加參數(shù)。
2)eval可以返回多個數(shù)據(jù)
eval "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
3)腳本中使用redis.call()調(diào)用redis命令
eval "return redis.call('set','testEval','evalValue')" 0
eval "return redis.call('get', 'testEval')" 0
4)redis原子操作,分布式解鎖。
如果調(diào)用redis.get(${Key})獲取到的值,和附加參數(shù)中ARGV[1]相等,則刪除${Key},返回1,否則返回0
image.png
4,使用上述方案,潛在問題
1)超時時間設(shè)置
過長:如果線程執(zhí)行時掛掉(如機器宕機、程序掛掉)等,未釋放鎖。則會導(dǎo)致其他線程的【鎖等待時間】過長?!竞侠碓u估業(yè)務(wù)執(zhí)行時間,寧大勿小】
過短:如果線程還在執(zhí)行中,鎖超時被釋放了,則有可能會造成其他線程獲取到鎖,造業(yè)務(wù)故障。(不可取)
2)使用Redisson(Watch Dogs)實現(xiàn)鎖續(xù)期
internalLockLeaseTime:默認(rèn)key的超時時間30s
鎖續(xù)期定時任務(wù)間隔:internalLockLeaseTime / 3
5,Redisson實現(xiàn)原理
1)原子性設(shè)計:
加鎖、解鎖、續(xù)約等都是通過Lua腳本中發(fā)送給redis
2)存儲結(jié)構(gòu)與加鎖腳本
key為唯一id+threadId,值為調(diào)用次數(shù)
image.png
image.png
image.png
3)線程阻塞與解除阻塞
redisson_lock__channel:{my-redisson-key}
image.png
4)Watch Dog鎖續(xù)期
leaseTime=-1(使用默認(rèn)的加鎖時間為 30s),才會生效
image.png
image.png
1.刪除鎖;重入鎖則減1
2.使用redis廣播鎖釋放消息,向redisson_lock__channel發(fā)送UNLOCK_MESSAGE
3.取消 Watch Dog。RedissonLock.EXPIRATION_RENEWAL_MAP中線程id刪除,并取消掉netty的定時任務(wù)
image.png
5)redis分布式鎖的最大缺陷
master-slave架構(gòu)中,當(dāng)A寫入master成功后,在slave異步復(fù)制過程中,發(fā)生了主從切換,導(dǎo)致B在新的master上也能加鎖成功。多個客戶端加鎖成功,可能會造成臟數(shù)據(jù)產(chǎn)生







