分布式锁的实现

为什么要用分布式锁

  • 解决同一资源被多个客户端并发写入,导致数据错乱

开源的解决方案

  • Redisson

    是一个高级的分布式协调Redis客户端;Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 增删改查,而 Redisson API 侧重于分布式开发

自己用redis去实现一个分布式锁功能

需要考虑的问题

  1. 同一个客户端同一个线程内部多次加锁的问题

    支持可重入锁解决

  2. 加锁的规则

    公平锁 或者 非公平锁; 如果采用公平锁实现,那么每个线程都先进入等待队列;如果采用非公平锁实现,那么每个线程先尝试获取锁,失败后在进入等待队列

  3. 程序异常导致redis锁未释放,如何处理?

    采用key过期策略

  4. 业务处理时间大于key过期时间怎么办?

    保障机制: 开启线程对key进行重置过期时间

具体实现代码

接口定义

/**
 * 分布式锁
 *
 * @author 唐海斌
 * @date 2022/4/14 14:45
 */
public interface DistributedLock {
    /**
     *  加锁
     * @param monitor 锁监视器
     */
    void lock(String monitor);

    /**
     * 加锁,在指定时间内无法获取到锁则返回
     *
     * @param monitor 锁监视器
     * @param second 超时时间: 单位秒
     */
    boolean tryLock(String monitor, long second);

    /**
     * 释放锁
     * @param monitor 锁监视器
     */
    void unlock(String monitor);
}

具体实现

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * 分布式锁-----使用redis实现
 * <p>
 * 需实现的功能:
 * 1. 可重入锁
 * 2. 锁规则: 公平or非公平
 * 3. 锁过期时间刷新问题
 *
 *
 * @author 唐海斌
 * @date 2022/4/14 14:45
 */
@Slf4j
@Component
public class RedisDistributedLock implements DistributedLock {
    /**
     * 获取锁的线程排队
     */
    private static final ConcurrentLinkedQueue<Thread> THREAD_QUEUE = new ConcurrentLinkedQueue<>();
    /**
     * 默认锁过期时间
     */
    private static final int DEFAULT_EXPIRE_LOCK_TIME_MINUTE = 2;
    /**
     * 线程锁持有次数
     * key: 监视器
     * value: 具体线程信息
     */
    private static final Map<String, LockDetail> THREAD_LOCK_COUNT = new HashMap<>();

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void lock(final String monitor) {
        assert monitor != null && !Objects.equals(monitor.trim(), "");
        final Thread currentThread = Thread.currentThread();
        Boolean lockSuccess = execLock(currentThread, monitor);
        if (Objects.equals(lockSuccess, Boolean.TRUE)) {
            return;
        }
        //未获取到锁, 放入等待队列并且阻塞当前线程
        THREAD_QUEUE.add(currentThread);
        LockSupport.park();
        //被唤醒则重试获取锁
        lock(monitor);
    }

    @Override
    public boolean tryLock(String monitor, long second) {
        assert monitor != null && !Objects.equals(monitor.trim(), "");
        assert second > 0;
        final Thread currentThread = Thread.currentThread();
        Boolean lockSuccess = execLock(currentThread, monitor);
        if (Objects.equals(lockSuccess, Boolean.TRUE)) {
            return true;
        }
        //未获取到锁, 有限时阻塞当前线程
        LockSupport.parkNanos(second);
        //到了限制时间后,再次获取锁
        return execLock(currentThread, monitor);
    }

    @Override
    public void unlock(String monitor) {
        assert monitor != null && !Objects.equals(monitor.trim(), "");
        final Thread currentThread = Thread.currentThread();
        LockDetail lockDetail = THREAD_LOCK_COUNT.get(monitor);
        //未被当前线程持有
        if (lockDetail == null || lockDetail.getThread() != currentThread) {
            return;
        }
        Long currentCount = redisTemplate.opsForValue().decrement(monitor);
        if (currentCount < 1L) {
            try {
                THREAD_LOCK_COUNT.remove(monitor);
                //当前线程持有次数为0则从redis移除锁
                redisTemplate.delete(monitor);
            } finally {
                //唤醒队列中的第一个线程
                Thread firstThread = THREAD_QUEUE.poll();
                if (firstThread != null) {
                    LockSupport.unpark(firstThread);
                }
            }
        } else {
            lockDetail.setCount(currentCount);
        }
    }

    /**
     * 执行加锁操作
     *
     * @param monitor
     * @return
     */
    private boolean execLock(Thread thread, String monitor) {
        LockDetail lockDetail = THREAD_LOCK_COUNT.get(monitor);
        if (lockDetail != null) {
            if (lockDetail.getThread() == thread) {
                //当前线程是重入情况
                lockDetail.setCount(redisTemplate.opsForValue().increment(monitor));
                return true;
            }
            return false;
        }
        //尝试加锁
        if (execLock2Redis(thread, monitor)) {
            return true;
        }
        //加锁失败,保障机制: 锁释放为0后,key未移除,所以这里获取一下锁持有数量
        Object lockCountForRedis = redisTemplate.opsForValue().get(monitor);
        if (lockCountForRedis == null) {
            return false;
        }
        //如果该锁持有数量小于1则移除
        if (Long.parseLong(lockCountForRedis.toString()) < 1) {
            redisTemplate.delete(monitor);
        }
        //再次尝试获取锁
        return execLock2Redis(thread, monitor);
    }

    private boolean execLock2Redis(Thread thread, String monitor) {
        Boolean lockSuccess = redisTemplate.opsForValue().setIfAbsent(monitor, 1, DEFAULT_EXPIRE_LOCK_TIME_MINUTE, TimeUnit.MINUTES);
        if (Objects.equals(lockSuccess, Boolean.TRUE)) {
            THREAD_LOCK_COUNT.put(monitor, new LockDetail(thread, 1L));
        }
        return lockSuccess;
    }

    /**
     * 定时重置key过期时间
     */
    @PostConstruct
    public void init() {
        Thread refreshMonitorExpireTimeThread = new Thread(() -> {
            while (true) {
                Set<String> monitors = THREAD_LOCK_COUNT.keySet();
                if (monitors.size() > 0) {
                    monitors.forEach(monitor -> {
                        //重置过期时间
                        redisTemplate.expire(monitor, DEFAULT_EXPIRE_LOCK_TIME_MINUTE, TimeUnit.MINUTES);
                    });
                }
                //阻塞1分钟
                LockSupport.parkNanos(TimeUnit.MINUTES.toNanos(1));
            }
        });
        refreshMonitorExpireTimeThread.setName("refresh-monitor-thread");
        refreshMonitorExpireTimeThread.start();
    }

    @Data
    class LockDetail {
        /**
         * 持有线程
         */
        private Thread thread;
        /**
         * 线程持有锁的次数
         */
        private Long count;

        public LockDetail(Thread thread, Long count) {
            this.thread = thread;
            this.count = count;
        }
    }
}

posted @   四码难追  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示