redis的分佈式鎖的幾種實現

注:此文章,爲總結的學習筆記。redis

分佈式鎖是什麼

  • 分佈式鎖是控制分佈式系統或不一樣系統之間共同訪問共享資源的一種鎖實現
  • 若是不一樣的系統或同一個系統的不一樣主機之間共享了某個資源時,每每經過互斥來防止彼此干擾。

分佈鎖設計目的

  • 能夠保證在分佈式部署的應用集羣中,同一個方法在同一操做只能被一臺機器上的一個線程執行。spring

  • 設計要求bash

    • 這把鎖要是一把可重入鎖(避免死鎖)
    • 這把鎖有高可用的獲取鎖和釋放鎖功能
    • 這把鎖獲取鎖和釋放鎖的性能要好

  • 分佈鎖實現方案分析服務器

    • 獲取鎖的時候,使用 setnx(SETNX key val:當且僅當 key 不存在時,set 一個 key 爲 val 的字符串,返回 1;
    • 若 key 存在,則什麼都不作,返回 【0】加鎖,鎖的 value 值爲當前佔有鎖服務器內網IP編號拼接任務標識
    • 在釋放鎖的時候進行判斷。並使用 expire 命令爲鎖添 加一個超時時間,超過該時間則自動釋放鎖。
    • 返回1則成功獲取鎖。還設置一個獲取的超時時間, 若超過這個時間則放棄獲取鎖。setex(key,value,expire)過時以秒爲單位
    • 釋放鎖的時候,判斷是否是該鎖(即Value爲當前服務器內網IP編號拼接任務標識),如果該鎖,則執行 delete 進行鎖釋放
  • 分佈鎖知足兩個條件,一個是加有效時間的鎖,一個是高性能解鎖maven

  • 採用redis命令setnx(set if not exist)、setex(set expire value)實現分佈式

  • 【千萬記住】解鎖流程不能遺漏,不然致使任務執行一次就永不過時性能

  • 將加鎖代碼和任務邏輯放在try,catch代碼塊,將解鎖流程放在finally學習

SETNX命令實現分佈式鎖

public void lockJob() {
        String lock = LOCK_PREFIX + "LockNxExJob";
        try{
            //redistemplate setnx操做
            boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,"XXX");
            Object lockValue = redisService.get(lock);

            //獲取鎖失敗
            if(!nxRet){
                String value = (String)redisService.get(lock);
                //打印當前佔用鎖的服務器IP
                logger.info("get lock fail,lock belong to:{}",value);
                return;
            }else{
                redisTemplate.opsForValue().set(lock,getHostIp(),3600);

                //獲取鎖成功
                logger.info("start lock lockNxExJob success");
                Thread.sleep(5000);
            }
        }catch (Exception e){
            logger.error("lock error",e);

        }finally {
            redisService.remove(lock);
        }
    }

複製代碼

SETNX的缺陷

  1. 服務器宕機
    • 程序剛獲取到鎖尚未釋放,服務器宕機,會致使鎖沒法釋放,其餘服務端永遠獲取不到鎖。
  2. redis 宕機
    • redis獲取到鎖以後,redis服務掛了,也會致使鎖沒法被釋放。
  • 因此要保證SETNX和SETEX(設置過時時間)這2個命令一塊兒執行,要麼都成功,要麼都失敗。保證其原子性。
  • redis官網文檔的描述,使用下面的命令加鎖
    • SET key value NX PX 30000
      • value是由客戶端生成的一個隨機字符串,至關因而客戶端持有鎖的標誌優化

      • NX表示只有key值不存在的時候才能SET成功,至關於只有第一個請求的客戶端才能得到鎖ui

      • PX 30000表示這個鎖有一個30秒的自動過時時間。

  1. 解鎖,爲了防止客戶端1得到的鎖,被客戶端2給釋放,採用下面的Lua腳原本釋放鎖
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0
end
複製代碼

鎖的優化

某線程成功獲得了鎖,而且設置的超時時間是30秒。 若是某些緣由致使線程B執行的很慢很慢,過了30秒都沒執行完,這時候鎖過時自動釋放,線程B獲得了鎖。

隨後,線程A執行完了任務,線程A接着執行del指令來釋放鎖。但這時候線程B還沒執行完,線程A實際上刪除的是線程B加的鎖。

怎麼避免這種狀況呢?能夠在del釋放鎖以前作一個判斷,驗證當前的鎖是否是本身加的鎖。

至於具體的實現,能夠在加鎖的時候把當前的線程ID當作value,並在刪除以前驗證key對應的value是否是本身線程的ID。

採用Lua+setNX腳本實現redis分佈式鎖(2.6版本之後)

  • Lua簡介
    • 從 Redis 2.6.0 版本開始,經過內置的 Lua 解釋器,可使用 EVAL 命令對 Lua 腳本進行求值。
    • Redis 使用單個 Lua 解釋器去運行全部腳本,而且, Redis 也保證腳本會以原子性(atomic)的方式執行:當某個腳本正在運行的時候,不會有其餘腳本或 Redis 命令被執行。這和使用 MULTI / EXEC 包圍的事務很相似。在其餘別的客戶端看來,腳本的效果(effect)要麼是不可見的(not visible),要麼就是已完成的(already completed)。
  • Lua腳本配置流程
    • 一、在resource目錄下面新增一個後綴名爲.lua結尾的文件
    • 二、編寫lua腳本
    • 三、傳入lua腳本的key和arg
    • 四、調用redisTemplate.execute方法執行腳本
/**
    * 獲取lua結果
    * @param key
    * @param value
    * @return
    */
   public Boolean luaExpress(String key,String value) {
       DefaultRedisScript<Boolean> lockScript = new DefaultRedisScript<Boolean>();
       lockScript.setScriptSource(
               new ResourceScriptSource(new ClassPathResource("add.lua")));
       lockScript.setResultType(Boolean.class);
       // 封裝參數
       List<Object> keyList = new ArrayList<Object>();
       keyList.add(key);
       keyList.add(value);
       Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
       return result;
   }

複製代碼

add.lua

local lockKey = KEYS[1]
local lockValue = KEYS[2]

-- setnx info
local result_1 = redis.call('SETNX', lockKey, lockValue)
if result_1 == true
then
local result_2= redis.call('SETEX', lockKey,3600, lockValue)
return result_1
else
return result_1
end

複製代碼

redisson實現分佈式鎖

引發maven配置

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>
複製代碼

代碼實現:

@SpringBootApplication
public class RedissonApplication {
   public static void main(String[] args) {
   	SpringApplication.run(RedissonApplication.class, args);
   }
   @Bean
   Redisson redissonSentinel() {
   	//支持單機,主從,哨兵,集羣等模式
   	//此爲哨兵模式
   	Config config = new Config();
   	config.useSentinelServers()
   			.setMasterName("mymaster")
   			.addSentinelAddress("redis://192.168.1.1:6379")
   			.setPassword("123456");
   	return (Redisson)Redisson.create(config);
   }
}
複製代碼

使用:

String lockKey = "test";//分佈式鎖的key
        RLock lock = redisson.getLock(lockKey);
   	lock.lock(60, TimeUnit.SECONDS); //設置60秒自動釋放鎖  (默認是30秒自動過時)
   	...業務代碼..
   	lock.unlock(); //釋放鎖
複製代碼

採用spring redisTemplate 實現分佈式鎖

spring-data-redis的版本儘可能高版本,2.0如下的connection.set是沒有返回值的。

@Component
    public class RedisLock {
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
        //加鎖
        public Boolean setNX(final String key, final String requestId, final long expirationTime, final TimeUnit timeUnit) {
            return redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(key.getBytes(), (value == null ? "" : value).getBytes(),
                    Expiration.from(expirationTime, timeUnit),
                    RedisStringCommands.SetOption.ifAbsent()));
        }
        //釋放鎖
        public  Boolean releaseLock(String key, String requestId) {
            return (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                Boolean result = connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(), requestId.getBytes());
                return result;
            });
        }
    }

複製代碼
相關文章
相關標籤/搜索