Redisson

目录

  • 六、Redisson:
    • 1.基础配置:
    • 2.Redisson解决一人一单问题:
    • 3.Redisson可重入锁源码:
      • 1)获取锁
      • [3.1 获取锁 tryLockInnerAsync()](#3.1 获取锁 tryLockInnerAsync())
      • [3.2 定时刷新锁的过期时间逻辑 scheduleExpirationRenewal()](#3.2 定时刷新锁的过期时间逻辑 scheduleExpirationRenewal())
      • [3.3 获取锁的剩余有效期(调用3.1和3.2,被3.4调用) tryAcquireAsync()](#3.3 获取锁的剩余有效期(调用3.1和3.2,被3.4调用) tryAcquireAsync())
      • [3.4 获取锁失败时尝试获取锁的逻辑tryLock()](#3.4 获取锁失败时尝试获取锁的逻辑tryLock())
      • 2)释放锁
      • [3.5 释放锁 unlockInnerAsync()](#3.5 释放锁 unlockInnerAsync())
    • 4.红锁

六、Redisson:

Redisson提供了用redis实现的分布式Java对象,有一系列的分布式服务,包括分布式锁。实际开发中其实不用手写分布式锁,直接调用框架就行。

1.基础配置:

1.引入依赖:

xml 复制代码
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
        </dependency>

2.编写配置类配置redisson:

java 复制代码
@Configuration
public class RedisConfig {
    @Bean //将方法的返回值放到IOC容器中交给Spring管理
    public RedissonClient redissonClient(){
        // 对redis的配置
        Config config = new Config();
        // useSingleServer单结点,config.useClusterServers()添加集群地址
        config.useSingleServer().setAddress("redis://192.168.50.128:6379");
        return Redisson.create(config);
    }
}

2.Redisson解决一人一单问题:

原理和我们自己写的可重入锁其实差不多,就是封装了一下,底层用的是lua保证原子操作。

java 复制代码
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private SeckillVoucherServiceImpl seckillVoucherService;
    @Autowired
    private RedisIdWorker idWorker;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        // 判断秒杀是否开始
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();
        if(LocalDateTime.now().isAfter(beginTime) && endTime.isAfter(LocalDateTime.now())){
            if (seckillVoucher.getStock() > 0){
                Long userId = UserHolder.getUser().getId();//Thread Local
                IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();// 获取代理对象
                // 获取锁对象
                RLock lock = redissonClient.getLock("order:" + userId);
                // 尝试获取锁,
                boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
                if (isLock){ // 分布式锁解决一人一单问题,且先提交事务后释放锁
                    try {
                        return proxy.createVoucherOrder(voucherId, seckillVoucher.getStock(), userId);// 代理对象调用方法才能进行AOP
                    }finally {
                        // 释放锁
                        lock.unlock();
                    }
                } else{
                  Result.fail("一人只能下一单");
                }
            }
            return Result.fail("无库存");
        }
        return Result.fail("不在秒杀时间内");
    }

    @Transactional //业务逻辑层事务
    @Override
    public Result createVoucherOrder(Long voucherId, Integer stock, Long userId){
        // 一人一单
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count > 0){
            return Result.fail("无法重复下单");
        }
        // 库存-1,解决超卖问题
        boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).
                eq("stock", stock).update();// where voucherId=#{voucherId} and stock=#{seckillVoucher.getStock()}
        if (success==false){
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();// 获取代理对象
            // 失败重试
            return proxy.seckillVoucher(voucherId);
        }
        // 创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = idWorker.nextId("order");// 自己编写的唯一ID生成器
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(voucherOrder);
    }
}

3.Redisson可重入锁源码:

redisson的可重入锁中,可重入机制和我们在第四、五章中写的差不多,通过计数的方式实现可重入,但是Redisson更高级的地方在于它的不可重试和超时释放的解决思路。

  • 不可重入:同一个线程无法多次获取同一把锁(失败时递归重试)
  • 不可重试:获取锁只尝试一次,失败就返回false,没有重试机制
  • 超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,锁提前释放会导致其他线程提前获取锁,有线程安全问题

Redisson获取锁和释放锁的流程图如下,下面来看Redisson是如何解决不可重试和超时释放的:

1)获取锁

3.1 获取锁 tryLockInnerAsync()

该方法用于执行lua脚本获取锁:

  • 若锁不存在,新建锁(获取锁),key=getName()用户id,field=threadId线程id,value=1,并设置超时释放时间leaseTime
  • 若锁存在,如果锁持有者是当前线程,那么重入次数value++,重置超时释放时间,返回空nil
  • 若锁持有者不是当前线程,那么会返回锁的剩余有效期
java 复制代码
// waitTime表示当前线程可以等待的最长时间,leaseTime表示锁的超时释放时间
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
	// 记录锁释放时间
    internalLockLeaseTime = unit.toMillis(leaseTime);
	// 执行lua脚本
    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));
}

3.2 定时刷新锁的过期时间逻辑 scheduleExpirationRenewal()

该方法用于刷新锁的剩余有效期,防止拿到锁的线程在业务执行完释放锁前,锁的有效期到期而提前释放锁,引起线程安全问题。

调用关系:scheduleExpirationRenewal()->renewExpiration()->renewExpirationAsync()

java 复制代码
// 该方法用于判断当前锁是否是新的,调用renewExpiration()方法为新的锁设置定时任务
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    /** 根据锁的key从EXPIRATION_RENEWAL_MAP中获取对应的ExpirationEntry对象
    *getEntryName()当前锁的key
    *EXPIRATION_RENEWAL_MAP的作用:在MAP中每个锁会有唯一的ExpirationEntry对象,从而保证锁和ExpirationEntry对象是一对一关系
    **/
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    
    if (oldEntry != null) {// 当前锁不是第一次创建
        oldEntry.addThreadId(threadId);
    } else {// 当前锁是第一次创建
        entry.addThreadId(threadId);
        // 为新锁设置定时任务
        renewExpiration();
    }
}
// 该方法用于设置定时任务,定时任务调用renewExpirationAsync()方法,每过leaseTime/3秒执行一次,并将定时任务存储到ExpirationEntry对象中
private void renewExpiration() {
	// 获取当前锁对应的ExpirationEntry对象
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    // 定时任务,设锁的超时释放时间初始传入为leaseTime,则该任务在leaseTime/3后执行
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
        	// 获取当前锁对应的ExpirationEntry对象
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            // 获取当前线程的id
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 刷新锁的超时释放时间,也就是说如果锁没有释放,那么每过leaseTime/3秒重置锁的超时释放时间为leaseTime
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }                
                if (res) {
                    // 递归当前方法,每过leaseTime/3秒执行定时任务,重置锁的超时释放时间为leaseTime
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    // 将当前的定时任务存储到当前锁对应的ExpirationEntry对象中
    ee.setTimeout(task);
}
// 该方法使用lua脚本来重置锁的超时释放时间
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
	// 这里if判断多余了,因为只有当前线程拿到锁后才能执行该方法
	// 使用pexpire重置锁的超时释放时间
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getName()),
            internalLockLeaseTime, getLockName(threadId));
}

总结一下关键点感觉就是:

java 复制代码
// 从MAP中根据锁的key获取对应的ExpirationEntry对象(一对一关系,ExpirationEntry对象中保存了2个内容,一个是占用锁的线程id,一个是定时任务)
ExpirationEntry oldentry = EXPIRATION_RENEWAL_MAP.putIfAbsent(key, entry);
// 当前锁是第一次在redis中创建,还没有ExpirationEntry对象
if(EXPIRATION_RENEWAL_MAP.putIfAbsent(key, entry) == null){
	ExpirationEntry entry = new ExpirationEntry(); // 为该锁实例化一个ExpirationEntry对象
	// 创建一个定时任务
	task = renewExpiration();
	entry.setTimeout(task); //将定时任务添加到entry对象中,定时任务每过一段时间自动执行,直到锁被删除定时任务停止执行	
}	
entry.addThreadId(threadId);// 修改或写入线程id(确保一致性,因为可能上一个线程已经释放锁了,现在是另一个线程持有锁)	

// 定时任务。leaseTime是初始给定超时释放时间
renewExpiration(){
	wait(leaseTime/3); //等待leaseTime/3秒
	redis.call('pexpire', key, leaseTime);// 重置锁key的超时释放时间为初始值:leaseTime*2/3->leaseTime
	// 递归调用自己
	renewExpiration()
}

3.3 获取锁的剩余有效期(调用3.1和3.2,被3.4调用) tryAcquireAsync()

  • 该方法调用tryLockInnerAsync()尝试获取锁,如果获取锁成功那么返回null,如果获取锁失败那么返回锁的剩余有效期。

  • 该方法调用scheduleExpirationRenewal()刷新锁的剩余有效期,防止拿到锁的线程在业务执行完释放锁前,锁的有效期到期而提前释放锁,引起线程安全问题。

java 复制代码
// waitTime表示当前线程可以等待的最长时间,leaseTime表示锁的超时释放时间
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
	// 判断是否传入锁超时释放时间(我们没有传)
    if (leaseTime != -1L) {
    	// 两个方法是一样的,区别在于是否指定锁的超时释放时间
        return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
    	// 给定默认锁超时释放时间为this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()=30s
        RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        // 回调函数,ttlRemaining就是tryLockInnerAsync()的返回值
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
            	// tryLockInnerAsync()返回null表示获取锁成功
                if (ttlRemaining == null) {
                	//renew更新Expiration超时释放时间
                    this.scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }
}

3.4 获取锁失败时尝试获取锁的逻辑tryLock()

Redisson获取锁失败时不会一直尝试获取锁,这样会浪费计算资源,且频繁的尝试没有意义,所以Redisson采用了一种发布订阅模式来判断何时重新尝试获取锁

java 复制代码
1.首先执行获取锁的代码
// 给定当前线程可以等待的最长时间,等待时间内可以多次尝试尝试获取锁
isLock = lock.tryLock(1L, TimeUnit.SECONDS);
↓
// waitTime表示当前线程可以等待的最长时间,leaseTime表示锁的超时释放时间
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
	// 将等待时间换转成毫秒
    long time = unit.toMillis(waitTime);
    // 获取当前时间
    long current = System.currentTimeMillis();
    // 获取线程ID
    long threadId = Thread.currentThread().getId();
    // 调用tryAcquireAsync尝试获取锁,返回剩余锁的有效期或null(tryAcquire就是简单的return了tryAcquireAsync)
    Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
    if (ttl == null) {// 表示获取锁成功
        return true;
    } else {// 表示获取锁失败
        time -= System.currentTimeMillis() - current;// 计算当前线程剩余可以等待的时间(waitTime-获取锁消耗的时间)
        if (time <= 0L) {// 若获取锁消耗的时间大于最长等待时间
            this.acquireFailed(waitTime, unit, threadId);
            return false;// 获取锁失败
        } else {// 若等待时间还有剩余
            current = System.currentTimeMillis();// 更新当前时间
            RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);// 重点:订阅其他线程释放锁的信号,等待获得释放锁的信号,而不是盲目的重试占用CPU资源(这里如果有多个线程等待的话感觉可以设置一个队列,先到先得)
            if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {// 若等待信号的时间超过剩余时间
                if (!subscribeFuture.cancel(false)) {
                    subscribeFuture.onComplete((res, e) -> {
                        if (e == null) {
                            this.unsubscribe(subscribeFuture, threadId);// 取消订阅
                        }
                    });
                }

                this.acquireFailed(waitTime, unit, threadId);
                return false;// 获取锁失败
            } else {// 若剩余等待时间内接收到其他线程释放锁的信号
                boolean var14;
                try {
                    time -= System.currentTimeMillis() - current;// 计算当前线程剩余可以等待的时间(waitTime-获取锁消耗的时间-等待接收信号的时间)
                    if (time > 0L) {// 若等待时间还有剩余
                        boolean var16;
                        do {// 循环开始
                            long currentTime = System.currentTimeMillis();//更新当前时间
                            ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);//同该方法的第四条语句,调用tryAcquireAsync重新尝试获取锁,返回剩余锁的有效期或null(因为此时接收到其他线程释放锁的信号了)
                            if (ttl == null) {// 获取锁成功
                                var16 = true;
                                return var16;
                            }
							// 获取锁又失败了(啰嗦了,用个队列就好很多)
                            time -= System.currentTimeMillis() - currentTime;
                            if (time <= 0L) {// 该线程的剩余等待时间不够了
                                this.acquireFailed(waitTime, unit, threadId);
                                var16 = false;
                                return var16;// 获取锁失败
                            }
							// 若该线程还有等待时间,即剩余等待时间大于0
                            currentTime = System.currentTimeMillis();// 更新当前时间
                            // 再次订阅其他线程释放锁的信号并等待,直到接收到信号
                            if (ttl >= 0L && ttl < time) {
                                ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                            } else {
                                ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                            }
							// 此时接收到其他线程释放锁的信号了
                            time -= System.currentTimeMillis() - currentTime;// 计算当前线程剩余可以等待的时间
                        } while(time > 0L);// 循环这个过程,直到当前线程的剩余等待时间小于0

                        this.acquireFailed(waitTime, unit, threadId);
                        var16 = false;
                        return var16;// 当前线程剩余等待时间不足(<0),获取锁失败
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    var14 = false;
                } finally {
                    this.unsubscribe(subscribeFuture, threadId);// 获取锁失败都要取消订阅
                }

                return var14;// 当前线程剩余等待时间不足(<0),获取锁失败
            }
        }
    }
}

感觉这里的while又一次判断获取锁成功和失败有点麻烦,直接设置一个队列,每次只有队首的线程能获得订阅的信号从而获取锁,非队首的一直等待到达队首才能等待获取订阅的信号不就行了。

总结一下关键点感觉就是:

java 复制代码
waitTime // 当前线程可以等待的最长时间,等待时间内可以多次尝试尝试获取锁,假设可以实时更新
do{
ttl = tryAcquireAsync() // 获取锁
// 获取锁成功
if(ttl == null)
	return true;
// 获取锁失败
RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);// 订阅其他线程释放锁的信号,等待获得释放锁的信号,等到信号才会执行下一步操作
}while(waitTime>0);

2)释放锁

3.5 释放锁 unlockInnerAsync()

释放锁的源码没什么重要的地方就不介绍了,这里只注意一点:发布释放锁信号的代码为redis.call('publish', KEYS[2], ARGV[1]);其他获取锁失败的线程在3.4节通过订subscribe阅来监听这个信号。

java 复制代码
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                    "end; " +
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                    "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;",
            Arrays.asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

4.红锁

可重入锁只在redis集群的主节点(写操作)上加锁,当主从同步前主节点就宕机了,哨兵会将某一从节点提升为主节点,此时新的主节点上没有锁,就会出现主从不一致问题,而红锁就是为了解决主从不一致问题

红锁的思路是不能只在主节点上创建锁,而应在(n/2 + 1)个节点上创建锁 ,其中n是主从集群中节点数量。避免主节点数据同步前宕机导致的锁丢失问题。

相关推荐
流星白龙8 小时前
【Redis】2.Redis重大版本
数据库·redis·junit
流星白龙8 小时前
【Redis】7.Hash表
数据库·redis·哈希算法
流星白龙9 小时前
【Redis】4.基本全局命令
数据库·redis·缓存
流星白龙12 小时前
【Redis】3.Redis安装与命令行客户端
数据库·redis·缓存
Season45016 小时前
Redis命令 (generic即通用命令)
数据库·redis·bootstrap
我不想名字重复18 小时前
redis缓存和数据库数据保持一致
数据库·redis·缓存
流星白龙20 小时前
【Redis】6.计数其他命令
数据库·redis·缓存
流星白龙20 小时前
【Redis】8.List列表
redis
NWU_白杨21 小时前
三种常用的数据存储技术
数据库·redis·mysql·sqlite
我叫黑大帅21 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试