Redis系列(二)分布式锁

Redis系列(二)分布式锁

前言

 分布式应用在进行逻辑处理时经常会遇到并发的问题。在单机系统中可以使用原子操作类、加锁等一系列操作来控制并发问题,但是在分布式系统中这些操作都会出现或多或少的问题。这种情况就必须引入分布式锁的概念,以保证原子操作的进行。

原子操作:指不会被线程调度机制打断的操作。这种操作一旦开始,就会一致运行到结束,中间不会有任何线程切换。

Redis 分布式锁

 分布式锁目标就是在Redis中抢占一个资源,当其他客户端来抢占时,发现无法获取资源,就只能放弃或者等待重试。抢占一般使用 setnx 指令,如果不存在就执行占领资源操作,这样只允许一个客户端拿到对应的资源。使用完成后再执行 del 释放资源。

> setnx lock:biz:pay true
...... do something
> del lock:biz:pay

这样就完成了一个简单的分布式锁案例。
 但是这样会出现一个问题,当我们在 do something 时,出现了异常或客户端宕机,导致无法执行 del,这样资源就得不到释放,陷入死锁。所以我们需要在拿到锁之后,再给锁添加过期时间,如5s,这样即使客户端未能主动释放资源,redis服务也能在5s之后自动释放。

> setnx lock:biz:pay true
> expire lock:biz:pay 5
...... do something
> del lock:biz:pay

 其实上述方法并未完全解决加锁时的非原子操作引起的死锁问题。因为在 setnx 和 expire 操作之间也可能出现异常或者客户端宕机,从而导致死锁的问题。

 这些问题的根源就是加锁过程的非原子操作,如果 setnx 和
expire 一起执行就不会有问题了。
 这么看的话,redis事务仿佛能解决这个问题,我们来使用事务模拟下两个线程的加锁过程:
客户端A:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> setnx lock:biz:pay true
QUEUED
127.0.0.1:6379(TX)> expire lock:biz:pay 5
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 1
2) (integer) 1

客户端B:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> setnx lock:biz:pay true
QUEUED
127.0.0.1:6379(TX)> expire lock:biz:pay 5
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 0
2) (integer) 1

操作过程为:先执行客户端A加锁过程,5秒内执行客户端B加锁过程,发现:A加锁成功,B加锁未成功,但是 B 的 expire lock:biz:pay 5 语句执行成功,也就是说,A加锁的资源过期时间将会被延长,如果B在后来的5s内再次执行加锁过程,还是会抢不到锁,同时延长A锁的时间,如果客户端A未能及时del lock:biz:pay,那么最后的结果就是可能会出现所有客户端无法获得锁资源,造成死锁。
 为了解决上述问题,Redis社区出现了很多对应的library,实现过程比较复杂。Redis官方在 Redis 2.8 的版本中加入了 set 指令的扩展参数,使得 setnxexpire 可以一起执行,这个也是Redis分布式锁的关键所在。

127.0.0.1:6379> set lock:biz:pay true ex 5 nx
......do something
127.0.0.1:6379> del lock:biz:pay

锁超时问题:Redis的分布式锁不能解决超时问题,如果加锁和释放锁之间的业务操作十分耗时,以至于超出所设置的超时限制,就会出现并发的问题。
!!!Redis的分布式锁应不要用于较长时间的任务处理。

代码实现

案例为可重入锁的实现。
可重入锁:线程在持有锁的情况下可再次请求加锁。ReentrantLock是可重入锁的一种实现。

public class RedisReentrantLock {

    /**
     * 存储线程下的所有 redis lock
     */
    private static final ThreadLocal<Map<String, LockedIdAndCount>> LOCKED_KEY_COUNT = new ThreadLocal<>();
    private static final int LOCK_TIMEOUT = 5;

    private RedisTemplate<String, String> redisTemplate;

    public RedisReentrantLock(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean lock(String key) {
        Map<String, LockedIdAndCount> lockers = lockers();
        LockedIdAndCount lockedIdAndCount = lockers.get(key);
        // 如果当前线程无锁,执行加锁过程
        if (lockedIdAndCount == null) {
            // 当前线程持有锁的 id,解锁时比对
            String lockedId = UUID.randomUUID().toString();
            // 执行加锁
            if (!lock0(key, lockedId)) {
                return false;
            }
            lockedIdAndCount = new LockedIdAndCount(lockedId);
        }
        // 计数 +1
        lockedIdAndCount.incr();
        lockers.put(key, lockedIdAndCount);
        return true;
    }

    public void unlock(String key) {
        Map<String, LockedIdAndCount> lockers = lockers();
        LockedIdAndCount lockedIdAndCount = lockers.get(key);
        if (lockedIdAndCount == null) {
            return;
        }
        // 计数 -1
        if (lockedIdAndCount.decr() <= 0) {
            // 比对 redis lock id 和 当前线程 lock id,如果不相等,放弃解锁
            if (!ObjectUtils.equals(redisTemplate.opsForValue().get(key), lockedIdAndCount.id)) {
                return;
            }
            // 解锁
            lockers.remove(key);
            unlock0(key);
            if (lockers.isEmpty()) {
                LOCKED_KEY_COUNT.remove();
            }
        }
    }

    private Map<String, LockedIdAndCount> lockers() {
        Map<String, LockedIdAndCount> map = LOCKED_KEY_COUNT.get();
        if (map == null) {
            map = new HashMap<>(16);
            LOCKED_KEY_COUNT.set(map);
        }
        return map;
    }

    private boolean lock0(String key, String lockedId) {
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, lockedId, LOCK_TIMEOUT, TimeUnit.SECONDS);
        if (lock == null || !lock) {
            return false;
        }
        return true;
    }

    private void unlock0(String key) {
        redisTemplate.delete(key);
    }

    class LockedIdAndCount {
        int count = 0;
        String id;

        public LockedIdAndCount(String id) {
            this.id = id;
        }

        void incr() {
            count++;
        }

        int decr() {
            return --count;
        }
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值