天天看點

單Redis執行個體實作分布式鎖 基于Lua腳本前言如何用Redis實作分布式鎖三、實作

參考文檔 redis分布式鎖官方介紹

前言

多線程并發執行情況下如何保證一個代碼塊在同一時間隻能由同一線程執行(同一個JVM中多線程同步執行)?

可以使用線程鎖的機制(如synchronized,ReentrantLock類)

synchronized(obj){

......

}
ReentrantLock lock = new ReentrantLock();

lock.lock(); 

.....

lock.unlock();
           
單Redis執行個體實作分布式鎖 基于Lua腳本前言如何用Redis實作分布式鎖三、實作

在分布式的叢集環境中如何保證不同節點的線程同步執行?

分布式鎖

單Redis執行個體實作分布式鎖 基于Lua腳本前言如何用Redis實作分布式鎖三、實作

這裡我們介紹使用Redis實作

如何用Redis實作分布式鎖

一、核心要素

Redis分布式鎖的基本流程并不難了解,但要想寫得盡善盡美,也并不是那麼容易。在這裡,我們需要先了解分布式鎖實作的三個核心要素:

1.加鎖

最簡單的方法是使用setnx指令。key是鎖的唯一辨別,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給key命名為 “lock_sale_商品ID” 。而value設定成什麼呢?我們可以姑且設定成1。加鎖的僞代碼如下:    

setnx(key,1)

當一個線程執行setnx傳回1,說明key原本不存在,該線程成功得到了鎖;當一個線程執行setnx傳回0,說明key已經存在,該線程搶鎖失敗。

2.解鎖

有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行del指令,僞代碼如下:

del(key)

釋放鎖之後,其他線程就可以繼續執行setnx指令來獲得鎖。

3.鎖逾時

鎖逾時是什麼意思呢?如果一個得到鎖的線程在執行任務的過程中挂掉,來不及顯式地釋放鎖,這塊資源将會永遠被鎖住,别的線程再也别想進來。

是以,setnx的key必須設定一個逾時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放。setnx不支援逾時參數,是以需要額外的指令,僞代碼如下:

expire(key, 30)

綜合起來,我們分布式鎖實作的第一版僞代碼如下:

if(setnx(key,1) == 1){

    expire(key,30)

    try {

        do something ......

    } finally {

        del(key)

    }

}

二、存在的問題

1. setnx和expire的非原子性

設想一種極端場景,當某線程執行setnx.獲得鎖,setnx剛執行成功,還未來執行expire指令,節點1突然挂掉,這樣鎖沒有設定過期時間,永遠無法被釋放。

解決?

Redis2.6.12版本,改進了set方法,可以同時設定value和expire

SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]
           
  • EX seconds

     − 設定指定的到期時間(以秒為機關)。
  • PX milliseconds

     - 設定指定的到期時間(以毫秒為機關)。
  • NX

     - 僅在鍵不存在時設定鍵。
  • XX

     - 隻有在鍵已存在時才設定。

redis 指令 :set key value ex 30 nx

Jedis java :  

String result = jedis.set(key,threadId,"NX","EX",30);
           

2.del誤删

假如線程A獲得鎖并且設定逾時時間為30秒,某種原因導緻線程A執行很慢,超過30秒後線程A鎖自動過期,釋放了鎖,線程B獲了鎖。随後線程A執行完成,del删除鎖,但線程B還未執行完成,實際上線程A删除的是線程B的鎖。

解決?

保證加鎖和删除鎖是同一個線程,在del前加判斷條件。如可以把uuid目前value ,在删除前驗證key的value是不是目前線程的uuid

僞代碼

if(uuild.equals(redisClient.get(key))){

 del(key);

}
           

但判斷和删除鎖是兩個獨立的操作,并不是原子級别的。

我們使用Lua腳本來解決子性操作  (redis2.6後特性)

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId)); //Collections.singletonList——用來生成隻讀 的單一進制素的List
           

3.出現并發的可能

上述第二點場景,雖然避免了key誤删,但當逾時時,還是存在A,B兩個線程同時通路代碼塊。

解決?

讓獲得鎖的線程開啟一個守護線程,給快要過期但認為未完成的鎖驗證過期時間。

關于守護線程的生命周期: 與鎖線程周期一緻,線程A執行完成會顯式關掉守護線程。

Thread t = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					//....延時操作
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		t.setDaemon(true);//設定目前線程為守護線程
		t.start();	
           

三、實作

基于上面原理,jedis整合spring來簡單實作單執行個體分布鎖

spring-redis.xml

使用sharedJedisPool

<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
		<property name="minIdle" value="${redis.minIdle}" />
		<property name="maxIdle" value="${redis.maxIdle}" />
		<property name="maxTotal" value="${redis.maxActive}" />
		<property name="maxWaitMillis" value="${redis.maxWait}" />
		<property name="testOnBorrow" value="${redis.testOnBorrow}" />
	</bean>
           
<bean id="shardedJedisPool" class="redis.clients.jedis.ShardedJedisPool">
		<constructor-arg index="0" ref="poolConfig" />
		<constructor-arg index="1">
			<list>
				<bean class="redis.clients.jedis.JedisShardInfo">
					<constructor-arg index="0" value="${redis.host}" />
					<constructor-arg name="port" value="${redis.port}"/>
					<property name="password" value="${redis.password}" />
				</bean>
			</list>
		</constructor-arg>
	</bean>
           

RedisDistributedLockServiceImpl 實作阻塞式加鎖、解鎖

import com.ticket.service.RedisDistributedLockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;

import java.util.Collections;
import java.util.concurrent.TimeUnit;


@Service("redisDistributedLockService")
public class RedisDistributedLockServiceImpl implements RedisDistributedLockService {
    private final static String LOCK_SUFFIX = "_redis_lock";

    /**
     * 重試時間
     */
    private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 20;

    @Autowired
    private ShardedJedisPool shardedJedisPool;
    

    // value值和執行線程相聯系,可以用uuid避免重複
    @Override
    public boolean lock(String lockKey,String value) throws InterruptedException {
        
            String key = lockKey + LOCK_SUFFIX ;
            ShardedJedis jedis = shardedJedisPool.getResource();
            while(true){
                //30秒後過期,釋放鎖
                String result = jedis.set(key,value,"NX","EX",30);
                if("OK".equals(result)){
                    jedis.close();
                    return true;
                }
                TimeUnit.MILLISECONDS.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
            }
        
    }

    @Override
    public void unlock(String lockKey ,String value) {
       
            String key = lockKey + LOCK_SUFFIX ;
            ShardedJedis shardedJedis = shardedJedisPool.getResource();
            Jedis jedis = shardedJedis.getShard(key);
            //保證加鎖和解鎖為同一線程,通過value值判斷
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end ";
            Object obj = jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(value));;
        
    }



}
           

建議:

1.添加守護線程給快要過期且未完成任務的線程,延長鎖過期時間。

2.不建議使用spring的redisTemplate,并不沒有提供set(key,value,"NX","EX",30) 類似的操作,除非使用lua腳本操作

小結

其他更好實作,參考官方基于RedLock算法的Redisson方式,它有一整套的解決方案

參考 Redisson Wiki Home 

繼續閱讀