Redis实现分布式锁,流程和实现

redis分布式锁的流程图

获取锁
在这里插入图片描述
释放锁
在这里插入图片描述

代码实现

分布式锁接口

接口中主要有 3 个方法

  • lock:获取锁,不论成功还是失败,会立即返回结果
  • tryLock:尝试在指定的时间内获取锁,直到超时
  • unLock:释放锁
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁接口,内部定义了3个方法
 */
public interface DcsLock {
    /**
     * 获取锁,立即返回结果
     *
     * @param resources 锁资源
     * @return 上锁结果
     */
    LockResult lock(String resources);

    /**
     * 在指定的时间内尝试获取锁,直到超时
     *
     * @param resources 资源
     * @param timeout   超时时间
     * @param unit      超时时间单位
     * @return 上锁结果
     */
    LockResult tryLock(String resources, long timeout, TimeUnit unit);

    /**
     * 释放锁
     *
     * @param resources
     */
    void unLock(String resources);

    /**
     * 上锁结果
     */
    class LockResult {

        public static LockResult fail(String resources) {
            return new LockResult(resources, false);
        }

        public static LockResult success(String resources) {
            return new LockResult(resources, false);
        }

        /**
         * 上锁的资源信息
         */
        private String resources;
        /**
         * 上锁是否成功
         */
        private boolean success;

        public LockResult() {
        }

        public LockResult(String resources, boolean success) {
            this.resources = resources;
            this.success = success;
        }

        public String getResources() {
            return resources;
        }

        public void setResources(String resources) {
            this.resources = resources;
        }

        public boolean isSuccess() {
            return success;
        }

        public void setSuccess(boolean success) {
            this.success = success;
        }

        @Override
        public String toString() {
            return "LockResult{" +
                    "resources='" + resources + '\'' +
                    ", success=" + success +
                    '}';
        }
    }
}

redis 实现分布式锁

下面这个类实现了上面的接口,内部采用 redis 实现了分布式锁的功能,注释比较详细,大家主要看对应的三个方法的代码。

代码中有 5 个比较重要的点:

  • redis 中用到了 2 个关键方法:setIfAbsent 和 getAndSet,这 2 个方法都是原子操作,第一个方法用来设置一个值,当这个 key 不存在的时候才会设置成功;而第二个方法用来返回当前值的同时并设置一个新的值,注意这 2 个方法都是原子性的,也就是说,在并发的情况下,能够确保其正确性。
  • 锁支持可重入:同一个线程支持多次获取一个分布式锁,所以需要判断锁的持有者是不是当前线程
  • 锁的释放:需要放置锁被其他线程是否,所以释放的时候需要判断当前的操作者是不是锁的持有者线程
  • 防止死锁:锁被占用之后,如果没有释放,比如获取锁之后系统重启了,这种情况会导致死锁,代码中我们在设置锁的值的时候,设置了一个超时时间,当超时时间过了,还未释放的,其他线程将可以尝试获取锁
  • 锁续命:redis 实现的分布式锁是有有效期的,比如下面我们设置的是 100 秒,可能系统在跑批,耗时比较长,此时 100 秒可能不够,那么就需要一个程序来检测这种情况,发现程序还在使用锁,需要对锁进行续命操作,下面代码中当获取锁成功之后,我们会添加一个延迟续命的任务到延迟队列,不断的触发续命操作,直到锁被释放。
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 * redis实现分布式锁(实现了锁重入、自动续命、防止锁被非持有者删除)
 */
@Component
public class RedisLock implements DcsLock, InitializingBean, DisposableBean {
    private Logger logger = LoggerFactory.getLogger(RedisLock.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 等待时间间隔:100ms
     */
    private Long waitTimeInterval = 100L;
    /**
     * 默认时间单位(ms)
     */
    private TimeUnit defaultTimeUnit = TimeUnit.MILLISECONDS;
    /**
     * 锁持有时间,默认100s
     */
    private Long defaultHolderTime = this.defaultTimeUnit.convert(100L, TimeUnit.SECONDS);
    /**
     * 锁到期前多久开始续命(默认为持有时间过半的时候开始续命)
     */
    private Long beforeExpireTime = defaultHolderTime / 2;
    /**
     * 续命队列(使用延迟队列)
     */
    private DelayQueue lockDelayedTaskDelayQueue = new DelayQueue<>();
    /**
     * resources->锁持有者线程
     */
    private Map lockOwnerThreadMap = new ConcurrentHashMap<>();
    /**
     * resources->锁持有次数
     */
    private Map lockCounterMap = new ConcurrentHashMap<>();
    /**
     * resources->锁结果
     */
    private Map lockResultMap = new ConcurrentHashMap<>();
    /**
     * 释放锁 &&入队列,这俩会操作本地缓存,需要互斥
     */
    private ReentrantLock lock = new ReentrantLock();
    /**
     * 是否已停止
     */
    private volatile boolean stop = false;

    @Override
    public LockResult lock(String resources) {
        LockResult lockResult = LockResult.fail(resources);
        //1、判断当前线程是否持有锁,如果有则,将持有次数+1
        Thread thread = this.lockOwnerThreadMap.get(resources);
        if (Thread.currentThread() == thread) {
            this.lockCounterMap.get(resources).incrementAndGet();
            lockResult = this.lockResultMap.get(resources);
        } else {
            //2、当前线程未持有锁,则从redis中获取锁信息
            LockInfo lockInfo = this.getLockInfoFromRedis(resources);
            String key = this.getKey(resources);
            //未上锁
            if (lockInfo == null) {
                Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
                Boolean result = this.stringRedisTemplate.opsForValue().setIfAbsent(
                        key,
                        JSON.toJSONString(this.buildLockInfo(expireTimeMs)));
                //setNX成功,上锁成功
                if (result) {
                    this.stringRedisTemplate.expire(key, this.defaultHolderTime, this.defaultTimeUnit);
                    lockResult = DcsLock.LockResult.success(resources);
                    this.lockSuccessAfter(resources, lockResult, expireTimeMs);
                }
            } else {
                //被上锁了,则判断锁是否已过期(为了避免死锁的情况【上锁了,但是没有释放】),持有者还未释放,则当前获取者尝试调用getAndSet(原子操作)尝试获取锁
                if (lockInfo.getExpireTimeMs() < System.currentTimeMillis()) {
                    Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
                    String oldValue = this.stringRedisTemplate.opsForValue().getAndSet(
                            key,
                            JSON.toJSONString(this.buildLockInfo(expireTimeMs)));
                    //getAndSet可能被并发执行,这个判断是为了判断并发的情况下,getAndSet被当前这个线程执行成功了
                    if (oldValue.equals(JSON.toJSONString(lockInfo))) {
                        lockResult = DcsLock.LockResult.success(resources);
                        this.lockSuccessAfter(resources, lockResult, expireTimeMs);
                    }
                }
            }
        }
        return lockResult;
    }

    @Override
    public LockResult tryLock(String resources, long timeout, TimeUnit unit) {
        Long expireTimeMs = this.getExpireTimeMs(unit, timeout);
        while (true) {
            LockResult lockResult = this.lock(resources);
            long currentTimeMillis = System.currentTimeMillis();
            if (lockResult.isSuccess() || expireTimeMs < currentTimeMillis) {
                return lockResult;
            } else {
                try {
                    long waitTime = Math.min(this.waitTimeInterval, expireTimeMs - currentTimeMillis);
                    this.defaultTimeUnit.sleep(waitTime);
                } catch (InterruptedException e) {
                    logger.error(e.getMessage(), e);
                    return lockResult;
                }
            }
        }
    }

    @Override
    public void unLock(String resources) {
        //当前线程次有锁,则次有次数-1,次有次数为0的时候,将其从redis中和本地缓存中干掉
        Thread thread = this.lockOwnerThreadMap.get(resources);
        if (Thread.currentThread() == thread) {
            int count = this.lockCounterMap.get(resources).decrementAndGet();
            if (count == 0) {
                //从redis中干掉
                this.stringRedisTemplate.delete(this.getKey(resources));
                //清理本地数据
                this.unLockAfter(resources);
            }
        }
    }


    /**
     * 将锁信息放入本地缓存 & 加入续命队列
     *
     * @param resources
     * @param lockResult
     * @param expireTimeMs
     */
    private void lockSuccessAfter(String resources, LockResult lockResult, Long expireTimeMs) {
        //1、将数据放入到本地缓存
        this.lockOwnerThreadMap.put(resources, Thread.currentThread());
        this.lockCounterMap.put(resources, new AtomicInteger(1));
        this.lockResultMap.put(resources, lockResult);
        //2、加入续命队列
        this.addExtendingLifeQueue(resources, expireTimeMs);
    }

    /**
     * 加入续命队列,续命队列会在任务过期前进行续命
     */
    private void addExtendingLifeQueue(String resource, Long expireTimeMs) {
        //释放锁 && 入续命队列互斥
        this.lock.lock();
        try {
            if (this.lockResultMap.containsKey(resource)) {
                LockDelayedTask lockDelayedTask = new LockDelayedTask(
                        resource,
                        expireTimeMs,
                        this.defaultTimeUnit.toMillis(this.beforeExpireTime));
                this.lockDelayedTaskDelayQueue.put(lockDelayedTask);
            }
        } finally {
            this.lock.unlock();
        }
    }

    /**
     * 从redis中获取LockInfo信息
     *
     * @param resource
     * @return
     */
    private LockInfo getLockInfoFromRedis(String resource) {
        String value = this.stringRedisTemplate.opsForValue().get(this.getKey(resource));
        if (value != null) {
            return JSON.parseObject(value, LockInfo.class);
        } else {
            return null;
        }
    }

    /**
     * 创建一个 LockInfo
     *
     * @param expireTimeMs 过期时间
     * @return
     */
    private LockInfo buildLockInfo(Long expireTimeMs) {
        return new LockInfo(expireTimeMs, UUID.randomUUID().toString());
    }

    private String getKey(String resources) {
        return String.format("%s:%s", RedisDcsLock.class.getName(), resources);
    }


    @Override
    public void afterPropertiesSet() {
        // 启动续命线程
        Thread extendingLifeThread = new Thread(this::executeExtendingLife);
        extendingLifeThread.setDaemon(true);
        extendingLifeThread.start();
    }

    @Override
    public void destroy() throws Exception {
        this.stop = true;
    }

    /**
     * 根据过期时间计算过期截止时间
     *
     * @param unit
     * @param expireTime
     * @return
     */
    private Long getExpireTimeMs(TimeUnit unit, Long expireTime) {
        return System.currentTimeMillis() + unit.toMillis(expireTime);
    }

    /**
     * 清理本地数据
     *
     * @param resources
     */
    private void unLockAfter(String resources) {
        //释放锁 && 入续命队列互斥
        this.lock.lock();
        try {
            this.lockOwnerThreadMap.remove(resources);
            this.lockResultMap.remove(resources);
            this.lockCounterMap.remove(resources);
        } finally {
            this.lock.unlock();
        }
    }

    /**
     * 执行续命
     */
    private void executeExtendingLife() {
        while (!this.stop) {
            LockDelayedTask lockDelayedTask = null;
            //从续命队列中拉取续命任务
            try {
                lockDelayedTask = this.lockDelayedTaskDelayQueue.poll(1, TimeUnit.SECONDS);
                //续命和释放锁互斥
                if (lockDelayedTask != null && this.lockResultMap.containsKey(lockDelayedTask.getResources())) {
                    //续命过程:更新redis中锁信息、过期时间
                    logger.info("续命start:[{}]", lockDelayedTask);
                    String resources = lockDelayedTask.getResources();
                    String key = this.getKey(resources);
                    Long expireTimeMs = this.getExpireTimeMs(this.defaultTimeUnit, this.defaultHolderTime);
                    LockInfo lockInfo = this.buildLockInfo(expireTimeMs);
                    this.stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(lockInfo));
                    this.stringRedisTemplate.expire(key, this.defaultHolderTime, this.defaultTimeUnit);
                    //继续将任务丢到续命队列
                    this.addExtendingLifeQueue(resources, expireTimeMs);
                    logger.info("续命end:[{}]", lockDelayedTask);
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    }

    /**
     * 锁信息
     */
    static class LockInfo {
        /**
         * 过期时间(ms)
         */
        private Long expireTimeMs;
        /**
         * 一个id,防止重复
         */
        private String id;

        public LockInfo() {
        }

        public LockInfo(Long expireTimeMs, String id) {
            this.expireTimeMs = expireTimeMs;
            this.id = id;
        }

        public Long getExpireTimeMs() {
            return expireTimeMs;
        }

        public void setExpireTimeMs(Long expireTimeMs) {
            this.expireTimeMs = expireTimeMs;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        @Override
        public String toString() {
            return "LockInfo{" +
                    "expireTimeMs=" + expireTimeMs +
                    ", id='" + id + '\'' +
                    '}';
        }
    }

    /**
     * 锁延迟任务
     */
    public static class LockDelayedTask implements Delayed {
        //锁id
        private String resources;
        //锁有效期(ms)
        private long expireTimeMs;
        //多久续命一次(ms)
        private long beforeExpireTime;

        /**
         * @param resources        资源
         * @param expireTimeMs     锁有效期(ms)
         * @param beforeExpireTime 锁到期前多久开始续命
         */
        public LockDelayedTask(String resources, long expireTimeMs, long beforeExpireTime) {
            this.resources = resources;
            this.expireTimeMs = expireTimeMs;
            this.beforeExpireTime = beforeExpireTime;
        }

        @Override
        public long getDelay(@NotNull TimeUnit unit) {
            return unit.convert(this.expireTimeMs - this.beforeExpireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(@NotNull Delayed o) {
            LockDelayedTask o2 = (LockDelayedTask) o;
            return Long.compare(this.getExpireTimeMs(), o2.getExpireTimeMs());
        }

        public String getResources() {
            return resources;
        }

        public void setResources(String resources) {
            this.resources = resources;
        }

        public long getExpireTimeMs() {
            return expireTimeMs;
        }

        public void setExpireTimeMs(long expireTimeMs) {
            this.expireTimeMs = expireTimeMs;
        }

        public long getBeforeExpireTime() {
            return beforeExpireTime;
        }

        public void setBeforeExpireTime(long beforeExpireTime) {
            this.beforeExpireTime = beforeExpireTime;
        }

        @Override
        public String toString() {
            return "DelayedLockResult{" +
                    "resources='" + resources + '\'' +
                    ", expireTimeMs=" + expireTimeMs +
                    ", beforeExpireTime=" + beforeExpireTime +
                    '}';
        }
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zkFun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值