springboot 中單機 redis 實現分佈式鎖

在微服務中常常須要使用分佈式鎖,來執行一些任務。例如按期刪除過時數據,在多個服務中只須要一個去執行便可。html

如下說明非嚴格意義的分佈式鎖,由於 redis 實現嚴格意義的分佈式鎖仍是比較複雜的,對於平常簡單使用使用以下簡單方法便可。即偶爾不執行任務不影響業務。git

實現要點github

1)得到鎖、釋放鎖須要是原子操做。要麼獲取成功,要麼失敗。釋放要麼成功,要麼失敗redis

2)任務完成須要本身釋放本身的鎖,不能釋放別人的鎖。spring

3)鎖要有過時時間限制,防止任務崩潰沒有釋放鎖,致使其餘節點沒法得到鎖。springboot

4)執行節點超時長時間不釋放鎖,到下次任務開始執行並行存在的狀況分佈式

 

要考慮的風險點spring-boot

1)獲取鎖失敗,偶爾不執行任務要不影響業務或告警人工干預微服務

2)redis 宕機,致使沒法獲取鎖this

 

方案一:低版本使用 jedis 實現

1 添加依賴,高版本或低版本有些方法可能沒有

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.10.2</version>
</dependency>

2 實現方法

@Slf4j
public abstract class AbsDistributeLock {

    private Jedis jedis;

    public void initDistributeLock(String ip, int port, Integer database, String password) {
        jedis = new Jedis(ip, port);
        if (password != null && !password.isEmpty()) {
            jedis.auth(password.trim());
        }
        if (database == null || database < 0 || database > 15) {
            database = 0;
        }
        jedis.select(database);
    }

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 具體的任務須要子類去實現
     *
     * @throws RTException 分佈式鎖異常
     */
    public abstract void taskService() throws RTException;

    /**
     * 任一執行,ANY OF
     * 全部節點任意一個執行任務 taskService 便可,沒有得到鎖的節點不執行任務
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 過時時間 ms
     */
    public void onlyOneNodeExecute(String lockKey, String keyValue, int expireTime) {
        boolean getLock = false;
        try {
            if ((getLock = getDistributedLock(jedis, lockKey, keyValue, expireTime))) {
                taskService();
            }
        } finally {
            if (getLock) {
                releaseDistributedLock(jedis, lockKey, keyValue);
            }
        }
    }

    /**
     * 全部串行執行,ALL IN LINE
     * 全部節點都必須執行該任務,每次只能一個執行。
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 過時時間 ms
     */
    public void allNodeExecute(String lockKey, String keyValue, int expireTime) {
        try {
            while (!(getDistributedLock(jedis, lockKey, keyValue, expireTime))) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    log.info(e.getMessage());
                }
            }
            taskService();
        } finally {
            releaseDistributedLock(jedis, lockKey, keyValue);
        }
    }

    /**
     * @param jedis      客戶端
     * @param lockKey    key
     * @param keyValue   key值
     * @param expireTime 過時時間,ms
     */
    public static boolean getDistributedLock(Jedis jedis, String lockKey, String keyValue, int expireTime) {
        String result = jedis.set(lockKey, keyValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            log.info("ip:[{}] get lock:[{}], value:[{}], getLock result:[{}]", IpUtil.getLocalIpAddr(), lockKey, keyValue, result);
            return true;
        } else {
            return false;
        }
    }

    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String keyValue) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(keyValue));
        log.info("ip:[{}] release lock:[{}], value:[{}], release result: [{}]", IpUtil.getLocalIpAddr(), lockKey, keyValue, result);
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

}

 

方案二:高版本的springboot,使用 lua 腳本執行

1 添加依賴

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.2.0.RELEASE</version>
      </dependency>

2 代碼實現

@Slf4j
public abstract class AbsDistributeLockLua {

    private RedisTemplate<String, String> redisTemplate;

    public AbsDistributeLockLua(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 具體的任務須要子類去實現
     *
     * @throws RTException 分佈式鎖異常
     */
    public abstract void taskService() throws RTException;

    /**
     * 任一執行,ANY OF
     * 全部節點任意一個執行任務 taskService 便可,沒有得到鎖的節點不執行任務
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 過時時間 ms
     */
    public void onlyOneNodeExecute(String lockKey, String keyValue, int expireTime) {
        boolean getLock = false;
        try {
            if ((getLock = getDistributeLock(redisTemplate, lockKey, keyValue, expireTime))) {
                taskService();
            }
        } finally {
            if (getLock) {
                releaseDistributeLock(redisTemplate, lockKey, keyValue);
            }
        }
    }

    /**
     * 全部串行執行,ALL IN LINE
     * 全部節點都必須執行該任務,每次只能一個執行。
     *
     * @param lockKey    lockKey
     * @param keyValue   keyValue
     * @param expireTime 過時時間 ms
     */
    public void allNodeExecute(String lockKey, String keyValue, int expireTime) {
        try {
            while (!(getDistributeLock(redisTemplate, lockKey, keyValue, expireTime))) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    log.info(e.getMessage());
                }
            }
            taskService();
        } finally {
            releaseDistributeLock(redisTemplate, lockKey, keyValue);
        }
    }

    /**
     * 經過lua腳本 加鎖並設置過時時間
     *
     * @param key    鎖key值
     * @param value  鎖value值
     * @param expire 過時時間,單位毫秒
     * @return true:加鎖成功,false:加鎖失敗
     */
    public boolean getDistributeLock(RedisTemplate<String, String> redisTemplate, String key, String value, int expire) {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<String>();
        redisScript.setResultType(String.class);
        String strScript = "if redis.call('setNx',KEYS[1],ARGV[1])==1 then return redis.call('pexpire',KEYS[1],ARGV[2]) else return 0 end";
        redisScript.setScriptText(strScript);
        try {
            Object result = redisTemplate.execute(redisScript, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), value, expire);
            System.out.println("redis返回:" + result);
            return "1".equals("" + result);
        } catch (Exception e) {
            //能夠本身作異常處理
            return false;
        }
    }

    /**
     * 經過lua腳本釋放鎖
     *
     * @param key   鎖key值
     * @param value 鎖value值(僅當redis裏面的value值和傳入的相同時才釋放,避免釋放其餘線程的鎖)
     * @return true:釋放鎖成功,false:釋放鎖失敗(可能已過時或者已被釋放)
     */
    public boolean releaseDistributeLock(RedisTemplate<String, String> redisTemplate, String key, String value) {
        DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(String.class);
        String strScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisScript.setScriptText(strScript);
        try {
            Object result = redisTemplate.execute(redisScript, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), Collections.singletonList(key), value);
            return "1".equals("" + result);
        } catch (Exception e) {
            //能夠本身作異常處理
            return false;
        }
    }
}

 

 

代碼地址:https://github.com/crazyCodeLove/distribute-lock

 

參考文獻:

https://www.cnblogs.com/bcde/p/11132479.html

https://blog.csdn.net/u013985664/article/details/94459529

相關文章
相關標籤/搜索