Redisson的使用和原理

96 阅读5分钟

一、简介

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

二、作用

分布式对象:分布式对象简单来说就是存储在Redis中的Java对象。

Redisson允许你在分布式环境中创建Java对象,并将它们存储在Redis中。这样,不同的Java应用程序或服务就能够共享和访问这些对象,实现数据共享和数据同步。

分布式集合:简单来说就是将集合放在Redis中,并且可以多个客户端对集合进行操作。

Redisson支持常见的分布式数据结构,如List、Set、Map、Queue等,使得多个Java应用程序可以并发地访问和修改这些集合。

分布式锁:通过Redisson,你可以轻松地实现分布式锁,确保在分布式环境中的并发操作的正确性和一致性。

缓存:通过Redisson能够轻松的基于redis实现项目中的缓存。

三 、使用

1.使用RdissonClient客户端方式

1.1 引入redisson依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.37.0</version>
</dependency>

1.2 配置类

package com.xzl.xmall.product.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 描述:
 *
 * @author: xuzili
 * @date: 2024/10/31
 */
@Configuration
public class RedissonConfig {
    @Bean(destroyMethod = "shutdown")
    RedissonClient redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://ip:port").setPassword("");
        return Redisson.create(config);
    }
}

2. 使用分布式锁

特点:

  1. 锁自动续期,锁默认有效期为30秒,在业务执行期间会自动续期,不用担心锁过期;
  2. 锁自动释放,当业务执行完成,即使没有手动释放锁,锁也会在30s后自动释放,不会造成死锁。
@Autowired
private RedissonClient redisson;

@GetMapping("/lock")
public String lock() {

    //redisson锁
    RLock lock = redisson.getLock("lock");
    log.info("线程{}等待锁", Thread.currentThread().getName());
    lock.lock();
    log.info("线程{}获取锁", Thread.currentThread().getName());
    try {
        log.info("线程{}执行业务逻辑", Thread.currentThread().getName());
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        log.error("线程{}执行业务逻辑失败", Thread.currentThread().getName());
        e.printStackTrace();
    } finally {
        lock.unlock();
        log.info("线程{}释放锁", Thread.currentThread().getName());
    }
    return "lock";
}

3. 使用读写锁

特点:读锁和写锁是互斥的,写锁和写锁是互斥的,读锁和读锁是共享的 使用场景:大量读,少量写的情况下

3.1 读锁

/**
 * ReadWriteLock读写锁,
 * 读 + 读 共享
 * 写 + 读 不共享
 * 写 + 写 不共享
 *
 *
 */

@GetMapping("/read/lock")
public String readLock() {
    RLock readLock = redisson.getReadWriteLock("ReadWriteLock").readLock();
    readLock.lock();
    log.info("线程{}获取读锁", Thread.currentThread().getName());
    try {
        log.info("线程{}执行业务逻辑", Thread.currentThread().getName());
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        log.error("线程{}执行业务逻辑失败", Thread.currentThread().getName());
        e.printStackTrace();
    } finally {
        readLock.unlock();
        log.info("线程{}释放读锁", Thread.currentThread().getName());
    }

    return "readLock";
}

3.2 写锁


@GetMapping("/write/lock")
public String writeLock() {
    RLock writeLock = redisson.getReadWriteLock("ReadWriteLock").writeLock();
    writeLock.lock();
    log.info("线程{}获取写锁", Thread.currentThread().getName());
    try {
        log.info("线程{}执行业务逻辑", Thread.currentThread().getName());
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        log.error("线程{}执行业务逻辑失败", Thread.currentThread().getName());
        e.printStackTrace();
    } finally {
        writeLock.unlock();
        log.info("线程{}释放写锁", Thread.currentThread().getName());
    }

    return "writeLock";
}

4. 闭锁

特点:

  1. 计数器CountDownLatch内部有一个计数器,用于记录需要等待的事件数量。
  2. 阻塞等待:调用await()方法的线程会阻塞,直到CountDownLatch的计数器减到0。
  3. 事件触发:其他线程可以通过调用countDown()方法来减少计数器的值,表示一个事件已经发生。
  4. 一次性CountDownLatch只能使用一次,计数器的值到达0后,不能再增加。

使用场景:

  1. 确保某个计算在其需要的所有资源都被初始化之后才继续执行。二元闭锁(包括两个状态)可以用来表示“资源R已经被初始化”,而所有需要R的操作都必须先在这个闭锁上等待。
  2. 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
  3. 等待直到某个操作的所有参与者都就绪在继续执行。(例如:多人游戏中需要所有玩家准备才能开始)

例子:图书馆现在有五个人在看书,保安需要等这五个人离开,才能关门

/**
 * 闭锁,等待计数器为0,才结束等待;
 *
 * 模拟图书馆闭馆,等待所有用户离开,才闭馆
 */
@GetMapping("/close")
public String close() {
    RCountDownLatch countDownLatch = redisson.getCountDownLatch("countDownLatch");
    // 设置闭锁的计数器为5,模拟图书馆有五个用户
    countDownLatch.trySetCount(5);
    try {
        log.info("等待所有用户离开");
        countDownLatch.await();
        log.info("所有用户离开,闭馆");
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    return "闭馆";
}

模拟有人离开

@GetMapping("/leave")
public String leave() {
    RCountDownLatch countDownLatch = redisson.getCountDownLatch("countDownLatch");
    // 计数器减1,模拟用户离开
    countDownLatch.countDown();
    log.info("离开一个用户");
    return "离开一个用户";
}

三、原理 tryAcquireAsync尝试获取锁

private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);
    ttlRemainingFuture = new CompletableFutureWrapper<>(s);

    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
}

tryLockInnerAsync执行lua脚本:

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, command,
            "if ((redis.call('exists', KEYS[1]) == 0) " +
                        "or (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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
  • KEYS[1]: getLock("name")的锁名称
  • ARGV[2]: uuid + 线程id
  • ARGV[1]: key过期时间
    如果key不存在或者指定的键的哈希表中存在指定的uuid + 线程id字段,则将key哈希表中的指定字段的值+1,设置key的过期时间;
"if ((redis.call('exists', KEYS[1]) == 0) " +
                        "or (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; 

如果key存在,则返回key的过期时间

 "return redis.call('pttl', KEYS[1]);",

看门狗逻辑:

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = getServiceManager().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;
            }
            
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    if (getServiceManager().isShuttingDown(e)) {
                        return;
                    }

                    log.error("Can't update lock {} expiration", getRawName(), e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null, null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    // internalLockLeaseTime / 3,过期时间的三分之一,也就是每隔十秒执行一次
    ee.setTimeout(task);
}