redis分布式锁实现

最近项目中使用到了redis实现的分布式锁,自定义的分布式锁支持自旋和可冲入等,是一个不错的实践,这里记录下

/**
 * @description: redis分布式锁
 * @author: cc.wang
 * @createDate: 2022-04-22 12:13
 * @version: 1.0
 */
public class RedisLock {
    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private RedisTemplate<String, Object> redisTemplate;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     * 锁超时时间,防止线程在入锁以后,无限的执行等待
     */
    private int expireMsecs = 3000;

    /**
     * 锁等待时间,防止线程饥饿
     */
    private int timeoutMsecs = 100;

    private volatile boolean locked = false;

    private String requestClientId = "";
    protected static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
     *
     * @param lockKey lock key (ex. account:1, ...)
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * Detailed constructor with default lock expiration of 60000 msecs.
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int timeoutMsecs) {
        this(redisTemplate, lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * Detailed constructor.
     */
    public RedisLock(RedisTemplate<String, Object> redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(redisTemplate, lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return this.lockKey;
    }

    private String get(final String key) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.get(key);
            };
            String result = redisTemplate.execute(callback);
            return result;
        } catch (Exception e) {
            logger.error("获取锁内容异常", e);
        }
        return "";
    }

    private boolean setNX(final String key, final String value) {
        try {
            RedisCallback<String> callback = (connection) -> {
                JedisCommands commands = (JedisCommands) connection.getNativeConnection();
                return commands.set(lockKey, value, "NX", "PX", this.expireMsecs);
            };
            String result = redisTemplate.execute(callback);
            logger.debug("locked result: {}", result);
            return StringUtils.isNotBlank(result);
        } catch (Exception e) {
            logger.error("加锁失败", e);
        }
        return false;
    }

    /**
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        if (StringUtils.isBlank(this.requestClientId)) {
            requestClientId = UUID.randomUUID().toString();
            logger.debug("redis lock requestClientId: {}", requestClientId);
        }
        //支持自旋
        while (timeout >= 0) {
            boolean isLocked = this.setNX(this.lockKey, this.requestClientId);
            if (isLocked) {
                this.locked = isLocked;
                return true;
            }

            if (this.locked) {
                //支持重入
                String value = this.get(this.lockKey);
                logger.debug("locked requestClientId: {}", value);
                if (StringUtils.equalsIgnoreCase(this.requestClientId, value)) {
                    return true;
                }
            }

            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
            if (timeout > 0) {
                doSleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
            }

        }
        return false;
    }

    /**
     * 休眠一段时间
     *
     * @param milliseconds
     * @throws InterruptedException
     */
    private void doSleep(long milliseconds) throws InterruptedException {
        Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
    }

    /**
     * Acqurired lock release.
     */
    public synchronized boolean unlock() {
        if (!this.locked) {
            //未取得锁时直接返回
            return false;
        }

        try {
            List<String> keys = Lists.newArrayList(this.lockKey);
            List<String> args = Lists.newArrayList(this.requestClientId);

            // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
            // spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本
            RedisCallback<Long> callback = (connection) -> {
                Object nativeConnection = connection.getNativeConnection();
                // 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行

                if (nativeConnection instanceof JedisCluster) { // 集群模式
                    return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
                } else if (nativeConnection instanceof Jedis) { // 单机模式
                    return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
                }
                return 0L;
            };
            Long result = redisTemplate.execute(callback);

            boolean reply = result != null && result > 0;
            if (reply) {
                this.locked = false;
            }
            return reply;
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        }
        return false;
    }
}

image

posted @ 2022-04-22 12:23  皮卡丘和羊宝贝😄  阅读(96)  评论(0编辑  收藏  举报