redis分布式锁

redis分布式锁

1. sentnx分布式锁

sentnx的分布式锁,样例代码如下:(spring data redis) 如:

@Autowired
private RedisTemplate redisTemplate; // 缓存操作服务bean

redisTemplate.opsForValue().setIfAbsent(KEY_TEST, uuid);
redisTemplate.opsForValue().getAndExpire(KEY_TEST, 10, TimeUnit.SECONDS);

主要问题:不能保证抢夺key的操作和设置key的失效期的操作是一个原子操作。

因为setnx(key, value) 和 expire(timeout, TimeUnit)是两个操作

2. 扩展的set指令

在redis的2.6.12版本之后,redis新出了一个指令,可以指定nx ex,将这两个操作并成为一个原子的操作,这样的话,可以规避上面setnx的问题。

SET命令的官方文档里写着:

  • Starting with Redis version 2.6.12: Added the EX, PX, NX and XX options.

这样是不是就可以了?

这样是可以解决这个问题了,setnx+expire可以原子化了。但是仅仅是命令可以规避了,有句名言说的好"天上飞的理念,需要变成落地的实现",现实中我的业务操作要话费多久是未可知的。

加入expire设置了10秒,而业务操作在10秒内没有完成操作(比如用了18秒)。等到业务花了18秒时,有可能别的并发在中间已经修改了key的值,这还是使得分布式锁破壁了!

所以还是不行!

3. redisson&合适的expire超时时长

问题的主要中心挪到了如何使得超时时间贴合业务处理时间上了。

可以使用一个TimerTask启动一个后台线程,动态监控业务处理完成否,如果没有处理完成,则自动给expire的超时时长续命!

比如,expire(30, TimeUnit.SECONDS),key的超时时长是30秒,等到超时时间将到时,检测业务完成否,没有完成,续命10秒,如此反复,直到业务处理完成,而后超时时长达到,继而key失效,自动删除。皆大欢喜。

redis的儿子redisson就实现了这个机制,我们如果使用redisson来做,可以直接使用它的封装即可。样例代码如下:

@Autowired
private RedissonClient redissonClient;

某方法(){
// 1.当redis中的key不存在时才设置key的值
    String lockKey = "xxxkey";
    RLock lock = redissonClient.getLock(lockKey);
    try {
        lock.lock();
        // 业务逻辑代码
    } finally {
        lock.unlock();
    }
}

redisson的内部实现就是用了这么几个部分:

  • lua脚本实现setnx+expire原子操作;
  • hash结构+value计数实现锁的可重入;

redis分布式锁_第1张图片

上图1: lock.lock()的调用里有如下一段代码片段(上图1中的调用):可以体现出lua脚本做的原子操作。redisson使用hash结构实现的,这种结构可以通过不同的现场调用来给线程计数,从而实现锁的可重入。

 RFuture tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

上图2处是对于锁的过期时长的续命操作:scheduleExpirationRenewal->renewExpiration(), 我们需要关注的几个关键词:

  1. TimerTask 启动了一个后台线程用来查询当前连接线程状态;
  2. internalLockLeaseTime / 3 续命一个预约期的1/3时长(原expire时长);
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }

            RFuture future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

其余的细节暂不多表。

4. CAP& (AP vs CP)

有了redisson的这个封装,是不是就皆大欢喜了?

有一句话叫"细思极恐",意思就是说,你只要好好深入的想,问题总是会有的! (强行解释)

如果redis是一主多从、哨兵架构,还是有问题的,问题就来源于redis的数据同步。

CAP是什么就不说了,Redis是AP的,Zookeeper是CP的。

由于redis优先保障A(Available=可用性),所以同步就没有极致的保障。由于redis的主从之间的数据同步是异步的,就有可能某个主节点发生故障,而这一瞬恰好分布式锁发生写入问题。

答案一:可以考虑使用Zookeeper!

那么就是要使用redis的话解决方案是什么呢?(毕竟redis单机可10万并发很香呢)--> redlock

5. redlock

redlock需要对等的多个节点(不存在数据同步关系),类似zookeeper的选举:多数通过才反馈成功!

原理就参考zk:相当于牺牲了A,偏向了C。

redlock此处不展开了,改天单独学习记录一篇。

你可能感兴趣的:(redis分布式锁)