Redis 高并发分布式锁学习总结

手写简单分布式锁
public class deductionStock {

    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    public void deductStock() throws InterruptedException {
    	//设置锁
        String lockKey = "product_01";
        //剩余库存key
        String surplusStock = "stock";
		//设置线程ID,保证不会删除其他线程的锁
		String clientId = UUID.randomUUID().toString();

        try {
        	//设置锁,并设置超时时间,防止服务器宕机情况下造成死锁
        	//此处其实还需要创建一个子线程用来定时监测锁状态,实现锁续命,监测周期一般是超时时间的3分之1
        	//作用是防止业务代码还没执行完,但到达了超时时间,锁就被释放了。
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "jiasuo", 30, TimeUnit.SECONDS);
            //设置失败
            if (!result) {
                System.out.println("当前锁已存在");
                return;
            }
            //获取剩余库存
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(surplusStock));
			//如果库存 > 0则可以扣减
            if (stock > 0) {
            	//计算扣减后的库存
                int realStock = stock - 1;
                //设置剩余库存
                stringRedisTemplate.opsForValue().set(surplusStock, String.valueOf(realStock));
                System.out.println("库存扣减成功,剩余库存:" + String.valueOf(realStock));
            }else {
                System.out.println("库存扣减失败,库存不足");
            }

        } finally {
        	//删除锁,保证只删除自己线程的锁
 			if(StringUtils.equals(clientId,stringRedisTemplate.opsForValue().get(lockKey))){
                stringRedisTemplate.delete(lockKey);
            }
        }
    }
}

上述代码,在并发量不高的时候是基本没有什么问题的,但是服务器宕机可能会导致多条命令不能实现原子性操作,例如删除锁时,如果线程执行到判断哪一行代码时服务器宕机了,就会导致无法删除key,重启后再设置这个锁时就会导致死锁一段时间,这种情况只能等超时时间结束释放锁。

使用Redisson实现分布式锁
public class deductionStock {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
	
	@Autowired
    private Redisson redisson;

    public void deductStock() throws InterruptedException {
    	//设置锁
        String lockKey = "product_01";
        //剩余库存key
        String surplusStock = "stock";
		//获取锁
		RLock redissonLock = redisson.getLock(lockKey);
        try {
        	//加锁
     		redissonLock.lock();
            //获取剩余库存
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(surplusStock));
			//如果库存 > 0则可以扣减
            if (stock > 0) {
            	//计算扣减后的库存
                int realStock = stock - 1;
                //设置剩余库存
                stringRedisTemplate.opsForValue().set(surplusStock, String.valueOf(realStock));
                System.out.println("库存扣减成功,剩余库存:" + String.valueOf(realStock));
            }else {
                System.out.println("库存扣减失败,库存不足");
            }

        } finally {
        	//解锁
        	redissonLock.unlock();
        }
    }
}
使用Redisson实现分布式读写锁

读写锁中,读读操作不互斥,读写操作互斥,读写在写锁加锁后是无法进行读取的,只有当写锁释放以后,读锁才可以执行。也就是说无论是读请求先执行还是写请求先执行,只要涉及到写锁,则都会阻塞,如果是先写再读,则读锁等待,如果是先读再写,则写锁等待。
要保证读锁和写锁使用的是同一个key

读锁
public String read(){
		//设置锁
        String lockKey = "product_01";
        
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
        //读之前加读锁,读锁的作用就是等待该lockkey释放写锁以后再读
        RLock rLock = readWriteLock.readLock();
        try {
            rLock.lock();
            String uuid = redisTemplate.opsForValue().get(lockKey);
            return uuid;
        }finally {
            rLock.unlock();
        }
    }
写锁
public String write() throws InterruptedException {
		//设置锁
        String lockKey = "product_01";

        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(lockKey);
        //写之前加写锁,写锁加锁成功,读锁只能等待
        RLock rLock = readWriteLock.writeLock();
        String s = "";
        try {
            rLock.lock();
            s = "10";
            Thread.sleep(10000);
            redisTemplate.opsForValue().set(lockKey,s);
        }finally {
            rLock.unlock();
        }
        return s;
    }

Redisson分布式锁实现原理

在这里插入图片描述

加锁源码分析

接口:
void lock(long leaseTime, TimeUnit unit);
进入实现方法:
@Override
public void lock(long leaseTime, TimeUnit unit) {
    try {
        lockInterruptibly(leaseTime, unit);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}
lockInterruptibly方法:
@Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        // 尝试加锁,tryLockInnerAsync加锁成功返回null,加锁失败返回锁的剩余过期时间
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // ==null表示加锁成功,直接return
        if (ttl == null) {
            return;
        }
		// 加锁失败对该线程进行订阅等待
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
        	// 加锁失败,进入自旋
            while (true) {
            	// 继续尝试获取锁
                ttl = tryAcquire(leaseTime, unit, threadId);
                // 直到获取到锁,终止循环
                if (ttl == null) {
                    break;
                }
				// 如果锁被占用,则在等待时间结束后再重试,否则直接尝试加锁
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            // 加锁完成后取消对该线程的订阅
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }
tryAcquireAsync方法:
  private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
  		// 在lockInterruptibly(-1, null);传入了-1,表示使用默认的leastTime
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        // 使用异步方式尝试加锁
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        // 将该锁加入订阅,并进行锁续命
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }

                Long ttlRemaining = future.getNow();
                // 如果锁再次获取成功,获取新的超时时间,对锁进行续命
                if (ttlRemaining == null) {
                	// 定时任务,实现自动续期
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }
tryLockInnerAsync方法(加锁核心方法):
    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    	//时间转化为毫秒值
        internalLockLeaseTime = unit.toMillis(leaseTime);
		
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
        		  // 判断reids有没有当前key
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                  	  // 如果没有,用hash类型存储key,并且存储当前主线程id,把值设置为1
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      // 设置当前key的过期时间
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  // 如果该锁的name和锁标识都相同,表示是重入锁,将value+1,并设置超时时间
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  	  // 调用hash的incrby方法,对值+1
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      // 设置超时时间
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  // 如果不是当前主线程来获取锁,返回当前锁的剩余过期时间
                  "return redis.call('pttl', KEYS[1]);",
           // getName()传入KEYS[1],表示传入加锁的keyName
           // internalLockLeaseTime传入ARGV[1],表示锁的超时时间
           // getLockName(threadId)传入ARGV[2],表示锁的唯一标识,由UUID+":"+线程id组成
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }
scheduleExpirationRenewal方法:

这个方法是在加锁后开启一个守护线程进行监听,也就是看门狗。

private void scheduleExpirationRenewal(final long threadId) {
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }
       
        // 启用定时任务线程更新锁的超时时间,每internalLockLeaseTime/3秒执行一次
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
               
                RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                        // 判断redis中是否存在该锁,并且判断是否是当前主线程加的锁
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +							
                        	// 锁续命
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return 1; " +
                        "end; " +
                        "return 0;",
                          Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
               
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        // 返回true证明续期成功,则递归调用续期方法
                        // 返回false证明续期失败,说明对应的锁已经不存在,直接返回
                        if (future.getNow()) {
                            // 将自己线程再次加入定时任务队列,实现不断地续命
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }
            //设置执行频率为leaseTime转换为ms单位下的三分之一,也就是默认值30/3=10
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
            task.cancel();
        }
    }

解锁源码分析

unlock方法:
 @Override
    public void unlock() {
        // 调用异步解锁方法
        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + Thread.currentThread().getId());
        }
        if (opStatus) {
            // 取消续命订阅
            cancelExpirationRenewal();
        }
    }
unlockInnerAsync方法:
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                // 如果该锁不存在,则发布已经解锁的消息
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                // 如果该锁对应的name和锁标识不匹配,说明该客户端没有该锁,无法解锁
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                // 可重入锁的value-1
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +      
                // 如果重入锁的计数器>0表示该锁仍然有效,更新锁的超时时间
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                // 否则可以删除该锁,并向其他客户端发送锁释放的广播消息
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                // getName()传入KEYS[1],表示传入解锁的keyName
                // getChannelName()传入KEYS[2],表示redis内部的消息订阅channel
                // LockPubSub.unlockMessage传入ARGV[1],表示向其他redis客户端线程发送解锁消息
                // internalLockLeaseTime传入ARGV[2],表示锁的超时时间
                // getLockName(threadId)传入ARGV[3],表示锁的唯一标识,由UUID+":"+线程id组成
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

    }
cancelExpirationRenewal方法:
void cancelExpirationRenewal() {
        // 将该线程从定时任务中删除
        Timeout task = expirationRenewalMap.remove(getEntryName());
        if (task != null) {
            task.cancel();
        }
    }
Redisson问题:
1、客户端长时间内阻塞导致锁失效

客户端 1 得到了锁,因为网络问题或者 GC 等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端 2 也能正常拿到锁,可能会导致线程安全的问题。

2、Redis 服务器时钟漂移

如果 Redis 服务器的机器时间发生了向前跳跃,就会导致这个 key 过早超时失效,比如说客户端 1 拿到锁后,key 还没有到过期时间,但是 Redis 服务器的时间比客户端快了 2 分钟,导致 key 提前就失效了,这时候,如果客户端 1 还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。

3、单点实例安全问题

如果 Redis 是单机模式的,如果挂了的话,那所有的客户端都获取不到锁了,但如果是主从模式,Redis 的主从同步是异步进行的,如果 Redis 主宕机了,这个时候从机并没有同步到这一把锁,那么机器 B 再次申请的时候就会再次申请到这把锁。

为了解决这些问题 Redis 作者提出了 RedLock 红锁的算法,在 Redission 中也对 RedLock 进行了实现。

RedissonRedLock代码示例
Config config1 = new Config();
config1.useSingleServer().setAddress("127.0.0.1:6379");
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("127.0.0.1:6378");
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("127.0.0.1:6377");
RedissonClient redissonClient3 = Redisson.create(config3);

// 1.获取多个 RLock 对象
RLock lock1 = redissonClient1.getLock(lockKey);
RLock lock2 = redissonClient2.getLock(lockKey);
RLock lock3 = redissonClient3.getLock(lockKey);

// 2.根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

try {
     //3.尝试获取锁
     //waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     //leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
    boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
}finally{
    //无论如何, 最后都要解锁
    redLock.unlock();
}
RedLock原理

客户端会在多个Redission node上申请加锁,必须半数以上节点加锁成功,才视为加锁成功,所以需要构建多个 RLock ,然后根据多个 RLock 构建成一个 RedissonRedLock,RedLock 算法是建立在多个互相独立的Redission node之上的,Redission node 节点既可以是单机模式(single),也可以是主从模式(master/salve),哨兵模式(sentinal),或者集群模式(cluster)。因为要在多个节点加锁,并且如果某个节点出现异常还需要释放锁并且回滚数据,还有最后释放锁时需要释放所有节点,所以RedLock存在性能问题。

RedLock注意事项

1、客户端在多个 Redis 实例上申请加锁,必须半数以上节点加锁成功,才视为加锁成功,解决了部分实例异常,容错性问题。并且要保证大多数节点加锁的总耗时,要小于锁设置的过期时间。
2、多实例操作可能存在网络延迟、丢包、超时等问题,所以就算是大多数节点加锁成功,如果加锁的累积耗时超过了锁的过期时间,那有些节点上的锁可能也已经失效了,还是没有意义的。
3、释放锁要向全部节点发起释放锁请求,如果部分节点加锁成功,但最后由于异常导致大部分节点没加锁成功,就要释放掉所有的,各节点要保持一致。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值