redis实现分布式锁

原理是利用redis的setnx命令

public interface RedisDistributedLock {

    boolean lock(String key);

    boolean lock(String key, long waitMillis);

    boolean lock(String key, long waitMillis, long sleepMillis);

    boolean lock(String key, long expire, long waitMillis, long sleepMillis);

    boolean lock(String key, long expire, long waitMillis, long sleepMillis, int retries);

    boolean release(String key);
}

实现类

package com.xxx.service.impl;

import com.xxx.service.RedisDistributedLock;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.ScriptOutputType;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.async.RedisScriptingAsyncCommands;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Repository;

import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutionException;


@Repository
@Slf4j
public class RedisDistributedLockImpl implements RedisDistributedLock {

    @Autowired
    @Qualifier("objRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 锁前缀
     */
    private static final String ROOT_KEY = "lock:";

    /**
     * 过期时间,ms
     */
    private static final long EXPIRE = 15000L;

    /**
     * 最长等待时间,ms
     */
    private static final long WAIT_MILLIS = 10000L;

    /**
     * 重试等待时间,ms
     */
    private static final long SLEEP_MILLIS = 500L;

    /**
     * 最多重试次数
     */
    private static final int RETRIES = Integer.MAX_VALUE;

    /**
     * 最多重试次数
     */
    private static final String OK = "OK";

    /**
     * 使用 ThreadLocal 存储 key 的 value 值,防止同步问题
     */
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private static final String REDIS_LIB_MISMATCH = "Failed to convert nativeConnection. Is your SpringBoot main version > 2.0 ? Only lib:lettuce is supported.";


    /**
     * 原子操作释放锁 Lua 脚本
     */
    private static final String LUA_UNLOCK_SCRIPT;
    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 ");
        LUA_UNLOCK_SCRIPT = sb.toString();
    }


    @Override
    public boolean lock(String key) {
        return setLock(key, EXPIRE, WAIT_MILLIS, SLEEP_MILLIS, RETRIES);
    }

    @Override
    public boolean lock(String key, long waitMillis) {
        return setLock(key, EXPIRE, waitMillis, SLEEP_MILLIS, RETRIES);
    }

    @Override
    public boolean lock(String key, long waitMillis, long sleepMillis) {
        return setLock(key, EXPIRE, waitMillis, SLEEP_MILLIS, RETRIES);
    }

    @Override
    public boolean lock(String key, long expire, long waitMillis, long sleepMillis) {
        return setLock(key, expire, waitMillis, sleepMillis, RETRIES);
    }

    @Override
    public boolean lock(String key, long expire, long waitMillis, long sleepMillis, int retries) {
        return setLock(key, expire, waitMillis, sleepMillis, retries);
    }

    /**
     * 获取 Redis 锁
     *
     * @param key         锁名称
     * @param expire      锁过期时间
     * @param retries     最多重试次数
     * @param sleepMillis 重试等待时间
     * @param waitMillis  最长等待时间
     * @return
     */
    private boolean setLock(String key, long expire, long waitMillis, long sleepMillis, int retries) {
        //检查 key 是否为空
        if (key == null || "".equals(key)) {
            log.error("setLock error, key is empty");
            return false;
        }

        try {
            long startTime = System.currentTimeMillis();
            key = ROOT_KEY + key;

            //可重入锁判断
            String v = threadLocal.get();
            if (v != null && isReentrantLock(key, v)) {
                return true;
            }
            //获取锁
            String value = UUID.randomUUID().toString();
            log.debug("setLock key:[{}], retries:[{}], value:[{}]", key, retries, value);
            while (!this.setNx(key, value, expire)) {
                //超过最大重试次数后获取锁失败
                log.error("setLock error, key:[{}], retries:[{}]", key, retries);
                if (retries-- < 1) {
                    return false;
                }

                //等待下一次尝试
                Thread.sleep(sleepMillis);

                //超过最长等待时间后获取锁失败
                if (System.currentTimeMillis() - startTime > waitMillis) {
                    log.error("setLock error, key:[{}], retries:[{}], waitMillis:[{}]", key, retries, waitMillis);
                    return false;
                }
            }

            threadLocal.set(value);
            return true;

        } catch (Exception e) {
            log.error("redis lock get, key:{}, expire:{}, waitMillis:{}, sleepMillis:{}, retries: {}, error:{}",
                    key, expire, waitMillis, sleepMillis, retries, e.getMessage());
            return false;
        }
    }

    /**
     * SET if Not exists
     */
    private boolean setNx(String key, String value, long expire) {
        Boolean resultBoolean = null;
        try {
            resultBoolean = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
                Object nativeConnection = connection.getNativeConnection();
                String redisResult = "";
                @SuppressWarnings("unchecked")
                RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
                //lettuce the serialization key value under the connection package, otherwise you can't parse with the default ByteArrayCodec
                byte[] keyByte = stringRedisSerializer.serialize(key);
                byte[] valueByte = stringRedisSerializer.serialize(value);
                // lettuce connection package redis stand-alone mode setnx
                if (nativeConnection instanceof RedisAsyncCommands) {
                    RedisAsyncCommands commands = (RedisAsyncCommands)nativeConnection;
                    redisResult = commands
                            .getStatefulConnection()
                            .sync()
                            .set(keyByte, valueByte, SetArgs.Builder.nx().ex(expire));
                }
                // lettuce connection package redis cluster mode setnx
                else if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                    RedisAdvancedClusterAsyncCommands clusterAsyncCommands = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                    redisResult = clusterAsyncCommands
                            .getStatefulConnection()
                            .sync()
                            .set(keyByte, keyByte, SetArgs.Builder.nx().ex(expire));
                }else{
                    log.error(REDIS_LIB_MISMATCH);
                }
                return OK.equalsIgnoreCase(redisResult);
            });
        } catch (Exception e) {
            log.error("redis lock setNX, key:{}, value:{}, expire:{}, error:{}", key, value, expire,e.getMessage());
        }
        return resultBoolean != null && resultBoolean;
    }

    /**
     * 可重入锁判断
     */
    private boolean isReentrantLock(String key, String v) {
        ValueOperations kvValueOperations = redisTemplate.opsForValue();
        String value = (String) kvValueOperations.get(key);
        if (value == null) {
            return false;
        }

        return v.equals(value);
    }

    /**
     * 释放锁
     */
    @Override
    public boolean release(String key) {
        if (key == null || "".equals(key)) {
            return false;
        }
        key = ROOT_KEY + key;
        String value = threadLocal.get();
        threadLocal.remove();

        try {
            return deleteKey(key, value);
        } catch (Exception e) {
            log.error("redis lock release, key:{}, error:{}", key, e.getMessage());
        }
        return false;
    }

    /**
     * 删除 redis key
     * <p>集群模式和单机模式执行脚本方法一样,但没有共同的接口
     * <p>使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而 redis 锁自动过期失效的时候误删其他线程的锁
     */
    private boolean deleteKey(String key, String arg) {
        if(arg==null){
            return false;
        }
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        byte[] valueBytes = arg.getBytes(StandardCharsets.UTF_8);
        Object[] keyParam = new Object[]{keyBytes};
        Long result = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                try{
                    Object nativeConnection = connection.getNativeConnection();
                    if (nativeConnection instanceof RedisScriptingAsyncCommands) {
                        RedisScriptingAsyncCommands<Object,byte[]> command = (RedisScriptingAsyncCommands<Object,byte[]>) nativeConnection;
                        RedisFuture future = command.eval(LUA_UNLOCK_SCRIPT, ScriptOutputType.INTEGER, keyParam, valueBytes);
                        return getEvalResult(future,connection);
                    }else{
                        log.error(REDIS_LIB_MISMATCH);
                        return 0L;
                    }
                }catch (Exception e){
                    log.error("Failed to releaseLock, closing connection",e);
                    closeConnection(connection);
                    return 0L;
                }
            }
        });
        return result != null && result > 0;
    }

    private Long getEvalResult(RedisFuture future,RedisConnection connection){
        try {
            Object o = future.get();
            return (Long)o;
        } catch (InterruptedException | ExecutionException e) {
            log.error("Future get failed, trying to close connection.", e);
            closeConnection(connection);
            return 0L;
        }
    }

    private void closeConnection(RedisConnection connection){
        try{
            connection.close();
        }catch (Exception e2){
            log.error("close connection fail.", e2);
        }
    }
}

使用时先lock 再在finally里面release。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值