Redisson解决并发问题

前言

  • Redisson 解决「setnx 命名非原子性」的思路和方案
    Redisson 不是靠 setnx 命令来实现原子性操作的( setnx 命令也也无法保证原子性 ),Redisson 是靠 Lua 脚本来实现的原子性操作。
  • Redisson 解决「过期自动删除时长」问题的思路和方案
    Redisson 中客户端一旦加锁成功,就会启动一个后台线程(惯例称之为 watch dog 看门狗)。watch dog 线程默认会每隔 10 秒检查一下,如果锁 key 还存在,那么它会不断的延长锁 key 的生存时间,直到你的代码中去删除锁 key 。
  • Redisson 解决「查 - 删 非原子性」问题的思路和方案
    Redisson 的上锁和解锁操作都是通过 Lua 脚本实现的。Redis 中 执行 Lua 脚本能保证原子性,整段 Lua 脚本的执行是原子性的,在其执行期间 Redis 不会再去执行其它命令。

引入 redisson 包

jedis 要比lettuce好一点

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions> <!-- 从依赖关系中排除 -->
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

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

配置 Redisson Client

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://127.0.0.1:6379")
                .setPassword("root")
                .setDatabase(0)
                .setKeepAlive(true);
        return Redisson.create(config);
    }
}

使用 Redisson Client

hello表示用这个当一个锁标识,只有当锁标识一样线程才会串行化,必须要将第一个线程执行完了才可以执行第二个线程

当我们hello.lock()没有指定多少时间自动释放的时候,watch dog 线程默认会每隔 10 秒检查一下,如果锁 key 还存在,那么它会不断的延长锁 key 的生存时间,直到你的代码中去删除锁 key 。也就是主动去hello.unlock();

RLock hello = redissonClient.getLock("hello");

// 上锁方式一
hello.lock();

// 睡眠100秒
TimeUnit.SECONDS.sleep(100);

// 释放锁
hello.unlock();

当然你也可以在 lock 时指定超时自动解锁时间:

// 加锁以后 10 秒钟自动解锁
hello.lock(10, TimeUnit.SECONDS);

这种情况下,如果你有意或无意没有调用 unlock 进行解锁,那么 10 秒后,Redis 也会自动删除代表这个锁的键值对。


当两个不同的线程对同一个锁进行 lock 时,第二个线程的上锁操作会失败。而上锁失败的默认行为是阻塞等待,直到前一个线程释放掉锁。

这种情况下,如果你不愿意等待,那么你可以调用 tryLock() 方法上锁。tryLock 上锁会立刻(或最多等一段时间)返回,而不会一直等(直到所得持有线程释放)。

// 拿不到就立刻返回
hello.tryLock();

// 拿不到最多等 1 秒。1 秒内始终拿不到,就返回
hello.tryLock(1, TimeUnit.SECONDS);

// 拿不到最多等 1 秒。1 秒内始终拿不到,就返回。
// 如果拿到了,自动在 10 秒后释放。
hello.tryLock(1, 10, TimeUnit.SECONDS);

你通过 RedissonClient 拿到的锁都是「可重入锁」。

当然,如果你对一个锁反复上锁,那么逻辑上,你应该对它执行同样多次的解锁操作。

hello.lock(); System.out.println("lock success!");
hello.lock(); System.out.println("lock success!");
hello.lock(); System.out.println("lock success!");

hello.unlock();
hello.unlock();
hello.unlock();

Redisson 向redis 中加入的键值对是一个 hash 结构。

  • key : getlock 方法的参数
  • fieid: uuid + “:” + 当前进程的线程id
  • value :被上锁的次数

Lock

lock锁当两个线程并行时,当一个线程的锁没有释放,第二个线程会一直等待,等到第一个线程释放锁之后才可以执行业务

@RequestMapping(value = "/{s}", method = RequestMethod.GET)
    public String getToken(@PathVariable String s) {
        new Thread(() -> {
            RLock rlock = redissonClient.getLock(s);
            System.out.println("线程1准备申请锁");
            rlock.lock();
            System.out.println("线程1获得锁");
            System.out.println("线程1睡前");
            try {
                TimeUnit.SECONDS.sleep(10);
                System.out.println("线程1睡后");
                rlock.unlock();
                System.out.println("线程1释放锁");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }).start();

        new Thread(() -> {
            RLock rlock = redissonClient.getLock(s);
            System.out.println("线程2准备申请锁");
            rlock.lock();
            System.out.println("线程2获得锁");
            rlock.unlock();
            System.out.println("线程2释放锁");
        }).start();
        return s;
    }

控制台输出的结果

{% asset_img 01.png %}

tryLock

他是一个boolean值,获取锁返回true,负责返回fales,trylock它不会像lock傻傻的一直等待

@RequestMapping(value = "/{s}", method = RequestMethod.GET)
    public String getToken(@PathVariable String s) {
        new Thread(() -> {
            RLock rlock = redissonClient.getLock(s);
            System.out.println("线程1准备申请锁");
            if (rlock.tryLock()) {
                System.out.println("线程1获得锁");
                System.out.println("线程1睡前");
                try {
                    TimeUnit.SECONDS.sleep(10);
                    System.out.println("线程1睡后");
                    rlock.unlock();
                    System.out.println("线程1释放锁");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                System.out.println("线程1没有获得锁,算球");
            }
        }).start();

        new Thread(() -> {
            RLock rlock = redissonClient.getLock(s);
            System.out.println("线程2准备申请锁");
            if (rlock.tryLock()) {
                System.out.println("线程2获得锁");
                rlock.unlock();
                System.out.println("线程2释放锁");
            } else {
                System.out.println("线程2获取不到锁,算球");
            }
        }).start();
        return s;
    }

控制台输出的结果

{% asset_img 02.png %}

Redisson 的上锁原理

Redisson 在上锁时,向 Redis 中添加的简直对的键是 UUID + : + threadId 拼接而成的字符串;值是这个锁的上锁次数。

Redisson 如何保证线程间的互斥以及锁的重入( 反复上锁 )?

因为代表这锁的键值对的键中含有线程 ID ,因此,当你执行上锁操作时,Redisson 会判断你是否是锁的持有者,即,当前线程的 ID 是否和键值对中的线程 ID 一样。

如果当前执行 lock 的线程 ID 和之前执行 lock 成功的线程的 ID 不一致,则意味着是「第二个人在申请锁」,那么就 lock 失败;如果 ID 是一样的,那么就是「同一个」在反复 lock,那么就累加锁的上锁次数,即实现了重入。

watch dog 自动延期机制

如果在使用 lock/tryLock 方法时,你指定了超时自动删除时间,那么到期之后,Redis 会自动将代表锁的键值对给删除掉。

如果,你在使用 lock/tryLock 方法时,没有指定超时自动删除时间,那么,就完全依靠你的手动删除( unlock 方法 ),那么,这种情况下你会遇到一个问题:如果你有意或无意中忘记了 unlock 释放锁,那么锁背后的键值对将会在 Redis 中长期存在!

再次强调
Redisson 看门狗(watch dog)在指定了加锁时间时,是不会对锁时间自动续租的。

在 watch dog 机制中,有一个被「隐瞒」的细节:表面上看,你的 lock 方法没有指定锁定时长,但是 Redisson 去 Redis 中添加代表锁的键值对时,它还是添加了自动删除时间。默认 30 秒(可配置)。

这意味着,如果,你没有主动 unlock 进行解锁,那么这个代表锁的键值对也会在 30 秒之后被 Redis 自动删除,但是很显然,并没有。这正是因为 Redisson 利用 watch dog 机制对它进行了续期( 使用 Redis 的 expire 命令重新指定新的过期时间)。

Redisson 的 watch dog 实现核心代码如下图源码所示:

{% asset_img 03.png %}

  1. 当你调用 lock 方法上锁,且没有指定锁定时间时,Redisson 在向 Redis 添加完键值对之后会调用到上面的 renewExpiration() 方法;
  2. 在 renewExpiration 方法中,Redisson 向线程池中添加了一段「代码」,并要求其在 30/3 秒之后( internalLockLesaseTime / 3 )执行;
  3. 这段代码在被执行时,它为代表锁的键值对重新设置过期时间( 30 秒 ),并且递归调用了自己,将自己又一次交给线程池在 10 秒之后执行。

逻辑上,变相地就是实现了一个 10 秒续期一次的定时任务。Redisson 会不停地为这个键值对重置过期删除时间,直到你在代码层面调用了 unlock 删除了这个键值对为止。

无休止地续期会不会导致代表锁的键值对永远存在?

watch dog 利用这这样的一个隐含逻辑:如果 watch dog 线程( 执行续期的线程 )还存在,那就意味着这个项目仍然是在正常运行的,项目正常运行,那么意味着一切正常,只是执行业务的线程没有执行完而已。

如果整个项目挂掉了,那么 watch dog 线程自然也就挂掉了,watch dog 线程挂掉了,那么就没有无限续期了,那么最多 30 秒后那个键值对也就被 Redis 删除了。

有没有可能项目的进程还在,但是持有锁的线程挂掉了?这是 bug ,应该解决!

Redisson 执行的 Lua 脚本

if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', 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]);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值