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是主从集群中节点数量。避免主节点数据同步前宕机导致的锁丢失问题。

相关推荐
r***12381 小时前
GO 快速升级Go版本
开发语言·redis·golang
一顿操作猛如虎,啥也不是!1 小时前
redis注册成windows服务,开机启动
数据库·redis·缓存
黎明晓月1 小时前
Redis容器化(Docker)
java·redis·docker
Knight_AL1 小时前
MongoDB、Redis、MySQL 如何选型?从真实业务场景谈起
redis·mysql·mongodb
凹凸曼说我是怪兽y4 小时前
Redis分布式锁详细实现演进与Redisson深度解析
数据库·redis·分布式
@淡 定10 小时前
Redis热点Key独立集群实现方案
数据库·redis·缓存
吳所畏惧11 小时前
Linux环境/麒麟V10SP3下离线安装Redis、修改默认密码并设置Redis开机自启动
linux·运维·服务器·redis·中间件·架构·ssh
困知勉行198514 小时前
springboot整合redis
java·spring boot·redis
飞鸟真人14 小时前
Redis面试常见问题详解
数据库·redis·面试