Redis分佈式鎖的正確實現 --集羣版

爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
1、互斥性。在任意時刻,只有一個客戶端能持有鎖。
2、不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
3、具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
4、解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。

package com.hz.tgb.data.redis.lock;

import cn.hutool.core.util.IdUtil;
import com.hz.tgb.entity.Book;
import com.hz.tgb.spring.SpringUtils;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.TimeoutUtils;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;

import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.Supplier;

/**
 * Redis分佈式鎖 - 集羣版
 *
 * @author hezhao on 2019.11.13
 */
@Component
public class RedisClusterLockUtil {

    /*
    爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
    1、互斥性。在任意時刻,只有一個客戶端能持有鎖。
    2、不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
    3、具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
    4、解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
     */

    private static final Logger logger = LoggerFactory.getLogger(RedisLockUtil.class);

    private static RedisTemplate<String, Object> cacheTemplate;

    /** OK: Redis操作是否成功 */
    private static final String REDIS_OK = "OK";
    /** CONN_NOT_FOUND: Redis鏈接類型不匹配 */
    private static final String REDIS_CONN_NOT_FOUND = "CONN_NOT_FOUND";
    /** 解鎖是否成功 */
    private static final Long RELEASE_SUCCESS = 1L;

    /** 解鎖Lua腳本 */
    private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * The number of nanoseconds for which it is faster to spin
     * rather than to use timed park. A rough estimate suffices
     * to improve responsiveness with very short timeouts.
     */
    private static final long spinForTimeoutThreshold = 1000000L;

    /**
     * 加鎖
     * @param lockKey 鎖鍵
     * @param requestId 請求唯一標識
     * @param expireTime 緩存過期時間
     * @param unit 時間單位
     * @return true: 加鎖成功, false: 加鎖失敗
     */
    @SuppressWarnings("all")
    public static boolean lock(String lockKey, String requestId, long expireTime, TimeUnit unit) {
        // 加鎖和設置過期時間必須是原子操作,否則在高併發情況下或者Redis突然崩潰會導致數據錯誤。
        try {
            // 以毫秒作爲過期時間
            long millisecond = TimeoutUtils.toMillis(expireTime, unit);
            String result = execute(connection -> {
                Object nativeConnection = connection.getNativeConnection();
                RedisSerializer<Object> keySerializer = (RedisSerializer<Object>) getRedisTemplate().getKeySerializer();
                RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>) getRedisTemplate().getValueSerializer();
                // springboot 2.0以上的spring-data-redis 包默認使用 lettuce連接包
                // lettuce連接包下序列化鍵值,否知無法用默認的ByteArrayCodec解析
                byte[] keyByte = keySerializer.serialize(lockKey);
                byte[] valueByte = valueSerializer.serialize(requestId);
                //lettuce連接包,單機模式,ex爲秒,px爲毫秒
                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands)nativeConnection;
                    // 同步方法執行、setnx禁止異步
                    return commands.getStatefulConnection().sync().set(keyByte, valueByte, SetArgs.Builder.nx().px(millisecond));
                } else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    // lettuce連接包,集羣模式,ex爲秒,px爲毫秒
                    RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    return clusterAsyncCommands.getStatefulConnection().sync().set(keyByte, valueByte, SetArgs.Builder.nx().px(millisecond));
                }
                return REDIS_CONN_NOT_FOUND;
            });

            // 如果鏈接類型匹配不上,使用默認加鎖方法
            if (Objects.equals(result, REDIS_CONN_NOT_FOUND)) {
                return getRedisTemplate().opsForValue().setIfAbsent(lockKey, requestId)
                        && getRedisTemplate().expire(lockKey, expireTime, unit);
            }

            return REDIS_OK.equals(result);
        } catch (Exception e) {
            logger.error("RedisLockUtil lock 加鎖失敗", e);
        }
        return false;
    }

    /**
     * 解鎖
     * @param lockKey 鎖鍵
     * @param requestId 請求唯一標識
     * @return true: 解鎖成功, false: 解鎖失敗
     */
    @SuppressWarnings("all")
    public static boolean unLock(String lockKey, String requestId) {
        try {
            // 使用Lua腳本實現解鎖的原子性,如果requestId相等則解鎖
            Object result = execute(connection -> {
                Object nativeConnection = connection.getNativeConnection();
                RedisSerializer<Object> keySerializer = (RedisSerializer<Object>) getRedisTemplate().getKeySerializer();
                RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>) getRedisTemplate().getValueSerializer();
                // springboot 2.0以上的spring-data-redis 包默認使用 lettuce連接包
                // lettuce連接包下序列化鍵值,否知無法用默認的ByteArrayCodec解析
                byte[] keyByte = keySerializer.serialize(lockKey);
                byte[] valueByte = valueSerializer.serialize(requestId);
                //lettuce連接包,單機模式
                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands)nativeConnection;
                    // 同步方法執行、setnx禁止異步
                    byte[][] keys = {keyByte};
                    byte[][] values = {valueByte};
                    return commands.getStatefulConnection().sync().eval(UNLOCK_LUA_SCRIPT, ScriptOutputType.INTEGER, keys , values);
                } else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    // lettuce連接包,集羣模式
                    RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    byte[][] keys = {keyByte};
                    byte[][] values = {valueByte};
                    return clusterAsyncCommands.getStatefulConnection().sync().eval(UNLOCK_LUA_SCRIPT, ScriptOutputType.INTEGER, keys , values);
                }
                return REDIS_CONN_NOT_FOUND;
            });

            // 如果鏈接類型匹配不上,使用默認解鎖方法
            if (Objects.equals(result, REDIS_CONN_NOT_FOUND)) {
                return getRedisTemplate().delete(lockKey);
            }

            return Objects.equals(RELEASE_SUCCESS, result);
        } catch (Exception e) {
            logger.error("RedisLockUtil unLock 解鎖失敗", e);
        }
        return false;
    }

    /**
     * 阻塞鎖,拿到鎖後執行業務邏輯。注意:超時返回null,程序會繼續往下執行
     * @param callback 業務處理邏輯,入參默認爲NULL
     * @param lockKey 鎖鍵
     * @param timeout 超時時長, 緩存過期時間默認等於超時時長
     * @param unit 時間單位
     * @return R
     */
    public static <R> R tryLock(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.DEFAULT);
    }

    /**
     * 阻塞鎖,拿到鎖後執行業務邏輯。注意:超時會拋出異常
     * @param callback 業務處理邏輯,入參默認爲NULL
     * @param lockKey 鎖鍵
     * @param timeout 超時時長, 緩存過期時間默認等於超時時長
     * @param unit 時間單位
     * @return R
     */
    public static <R> R tryLockTimeout(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.THROW_EXCEPTION);
    }

    /**
     * 阻塞鎖,拿到鎖後執行業務邏輯。注意:超時會給予補償,即處理正常邏輯
     * @param callback 業務處理邏輯,入參默認爲NULL
     * @param lockKey 鎖鍵
     * @param timeout 超時時長, 緩存過期時間默認等於超時時長
     * @param unit 時間單位
     * @return R
     */
    public static <R> R tryLockCompensate(Supplier<R> callback, String lockKey, long timeout, TimeUnit unit) {
        return tryLock(callback, lockKey, IdUtil.fastSimpleUUID(), timeout, timeout, unit, TimeOutProcess.CARRY_ON);
    }

    /**
     * 阻塞鎖,拿到鎖後執行業務邏輯
     * @param callback 業務處理邏輯
     * @param lockKey 鎖鍵
     * @param requestId 請求唯一標識
     * @param timeout 超時時長
     * @param expireTime 緩存過期時間
     * @param unit 時間單位
     * @param timeoutProceed 超時處理邏輯
     * @return R
     */
    public static <R> R tryLock(Supplier<R> callback, String lockKey, String requestId,
                                long timeout, long expireTime, TimeUnit unit, TimeOutProcess timeoutProceed) {
        boolean lockFlag = false;
        try {
            lockFlag = tryLock(lockKey, requestId, timeout, expireTime, unit);
            if(lockFlag){
                return callback.get();
            }
        } finally {
            if (lockFlag){
                unLock(lockKey, requestId);
            }
        }
        if (timeoutProceed == null) {
            return null;
        }
        if (timeoutProceed == TimeOutProcess.THROW_EXCEPTION) {
            throw new RedisLockTimeOutException();
        }
        if (timeoutProceed == TimeOutProcess.CARRY_ON) {
            return callback.get();
        }
        return null;
    }

    /**
     * 阻塞鎖
     * @param lockKey 鎖鍵
     * @param requestId 請求唯一標識
     * @param timeout 超時時長, 緩存過期時間默認等於超時時長
     * @param unit 時間單位
     * @return true: 加鎖成功, false: 加鎖失敗
     */
    public static boolean tryLock(String lockKey, String requestId, long timeout, TimeUnit unit) {
        return tryLock(lockKey, requestId, timeout, timeout, unit);
    }

    /**
     * 阻塞鎖
     * @param lockKey 鎖鍵
     * @param requestId 請求唯一標識
     * @param timeout 超時時長
     * @param expireTime 緩存過期時間
     * @param unit 時間單位
     * @return true: 加鎖成功, false: 加鎖失敗
     */
    public static boolean tryLock(String lockKey, String requestId, long timeout, long expireTime, TimeUnit unit) {
        long nanosTimeout = unit.toNanos(timeout);
        if (nanosTimeout <= 0L) {
            return false;
        }
        final long deadline = System.nanoTime() + nanosTimeout;
        for (;;) {
            // 獲取到鎖
            if (lock(lockKey, requestId, expireTime, unit)) {
                return true;
            }
            // 判斷是否需要繼續阻塞, 如果已超時則返回false
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L) {
                return false;
            }
            // 休眠1毫秒
            if (nanosTimeout > spinForTimeoutThreshold) {
                LockSupport.parkNanos(spinForTimeoutThreshold);
            }
        }
    }

    public static <T> T execute(RedisCallback<T> action) {
        return getRedisTemplate().execute(action);
    }

    public static RedisTemplate<String, Object> getRedisTemplate() {
        if (cacheTemplate == null) {
            cacheTemplate = SpringUtils.getBean("redisTemplate", RedisTemplate.class);
        }
        return cacheTemplate;
    }

    public static void main(String[] args) {
        Book param = new Book();
        param.setBookId(1234);
        param.setName("西遊記");

        Boolean flag = tryLock(() -> {
            int bookId = param.getBookId();
            System.out.println(bookId);
            // TODO ...
            return true;
        }, "BOOK-" + param.getBookId(), 3, TimeUnit.SECONDS);

        System.out.println(flag);
    }

    /**
     * 超時處理邏輯
     */
    public enum TimeOutProcess {
        /** 默認,超時返回null,程序會繼續往下執行 */
        DEFAULT,
        /** 超時會拋出異常 */
        THROW_EXCEPTION,
        /** 超時會給予補償,即處理正常邏輯 */
        CARRY_ON,

    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章