八、Redis分布式锁

目录

1.普通锁举例

2.分布式锁实现

3. Redisson分布式锁实现原理 

4. 问题 


1.普通锁举例

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
    if (stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
    System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
        System.out.println("扣减失败,库存不足");
    }

上面代码,如果单线程肯定没有问题,如果多线程的话,同时扣减库存,有可能出现超卖现象(就是一个东西卖了两遍),就是如果两个人去购买商品的话,假设库存是50,应该剩下48个,但是有可能库存剩余49,这个时候就需要加锁进行处理,如下:

synchronized (this) {
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
    if (stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
        System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
        System.out.println("扣减失败,库存不足");
    }
}

如果放到集群或者通过负载均衡打到不同的服务器上面

假设多个线程访问ngnix,ngnix可以分发请求,如果分发到不同tomcat上去,同步代码块只在同一个JVM进程下生效,在分布式场景下synchronized是不起作用的

2.分布式锁实现

初步实现

1. 先设置redis key值

2. 然后使用redis setnx命令,如果key值不存在设置成功,否则失败

3. 获取结果,如果不成功该线程直接返回响应错误码给前端进行提示

4. 如果成功的话,就去仓库里减掉库存

5. 然后删除key,让其他线程可以进来扣减库存 

发现问题 

1. 如果在执行扣减库存的时候抛出异常,无论如何后面都无法删除掉这个key值

2. 如果服务直接被终止掉或者在catch里面宕机,都运行不到finally

3. 如果正好发生在你setnx和设置过期时间之间宕机

解决方法

一一对应上面的解决办法 

1. 用try-finally保证能够将锁释放掉 

2. 对key设置过期时间

3. setnx和设置过期时间一起设置,redis会帮你原子控制执行

其他

并发量不大的情况下,上面这种分布式锁已经够用了,但是还有其他情况,就是你业务执行时间过长,假设线程1还在执行过程中,过期时间到了,线程2进来执行,后面线程1又会在finally里面删除key,线程3又可以进来了,这样会反复使得删除,key会一直不生效

主要解决方案就是自己加的锁,自己去释放,别人释放不了 

这样的话,线程1执行过程中,线程2进来加锁,线程1执行完之后不会去释放锁,但是依然有可能在第一个线程执行过程中,锁过期第二个线程进来,这样也不好,执行方案如下:

就是在业务代码里面加上定时任务,时刻去注意这个锁的过期时间,如果任务还没有执行结束(就是去判断这个key还存在嘛),如果还存在说明任务没有执行结束,重置过期时间,这样就能保证这个线程执行任务的过程中,是单线程执行的了

3. Redisson分布式锁实现原理 

 跟上面分布式锁功能全部实现是一样的

3.1 源码分析

 1. 获取锁,加锁

RLock redissonLock = redisson.getLock("lock");
redissonLock.lock();

2. lock()方法

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    // 拿到线程id
    long threadId = Thread.currentThread().getId();
    Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);

3. tryAcquire()

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }

4. tryAcquireAsync()

leaseTime=-1,走下面那个方法

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture ttlRemainingFuture;
        if (leaseTime > 0L) {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }

        CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
            if (ttlRemaining == null) {
                if (leaseTime > 0L) {
                    this.internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    this.scheduleExpirationRenewal(threadId);
                }
            }

            return ttlRemaining;
        });
        return new CompletableFutureWrapper(f);
    }

5. 里面走的是lua脚本

lua脚本大致流程:这边的return nil就是执行完了

1. 判断存不存在这个key,key为getName(),就是锁的名称,说明此时还没有加过锁

2. 如果不存在的话,设置hash类型,key为getName(),field里面key为getLockName(threadId),即线程id,value为1,并且对外层key设置了过期时间,internalLockLeaseTime,这个时间应该是30s

3. 如果存在的话,就说明已经被某个线程加锁了,判断内层value是否为1,如果为1的话,对这个value值进行+1操作,并且设置过期时间,就是重入锁的特征

4. 如果上面两种情况都不是的话,就说明已经被其他线程加锁了,就返回这个key还有多久时间失效,该线程就会一直不断自循环,尝试不断加锁

6. 接着回看tryAcquireAsync()方法 

 判断这个锁是不是当前线程id设置的锁,如果是当前线程设置的锁那么就重新设置锁过期时间30s,每次执行10s,轮训3次,如果return 0的话,应该会在外面进行判断把看门狗关掉结束

7. 其他线程不断循环

4. 问题 

如果当在主节点刚刚加锁成功,主节点挂了,从节点还没有来得及对数据进行同步,也就是从节点还没有这把锁,那以后别的线程过来访问的时候就会加锁成功,这样就会产生两把同样的锁

用zookeeper,强一致性架构,所有节点同步成功之后才会告诉客户端,这把锁加成功了,就算主节点挂了,也会选举肯定有这把锁的节点,但是zookeeper性能没有redis高,其实这个bug其实可以容忍的,如果出现超卖的话,人工去处理 

其他处理办法,就是你发送setnx命令,对其他节点也发送这个命令,至少要收到半数以上命令设置成功的响应,才会认为加锁成功,但是这样的话,原来就需要接收一个节点的返回,现在需要收集多个节点的返回,时间增长,如果第一个节点加锁成功,后面加锁过程中失败了,那么第一个是不是要做回滚,要处理的东西太多了

如果处理高并发场景

如果很多线程去抢同一个商品,这个时候你加再多机器也没用,他只访问一个机子,现在这么处理,假设001商品有1000个,我们提高10倍处理速度,将001商品分成001_1到001_10这么10个商品,每个商品对应100个,让这些key落到不同的节点上,相当于水平扩容了,每次随机到一个里面去,减掉库存,如果到0的话,再去其他段查询数据 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值