Redisson解决并发问题

https://blog.csdn.net/qq_54795449/article/details/129831678

 看上面原文

前言
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);
1
2
这种情况下,如果你有意或无意没有调用 unlock 进行解锁,那么 10 秒后,Redis 也会自动删除代表这个锁的键值对。

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

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

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

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

// 拿不到最多等 1 秒。1 秒内始终拿不到,就返回。
// 如果拿到了,自动在 10 秒后释放。
hello.tryLock(1, 10, TimeUnit.SECONDS);
1
2
3
4
5
6
7
8
9
你通过 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;
    }

控制台输出的结果

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;
    }

控制台输出的结果

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 实现核心代码如下图源码所示:

当你调用 lock 方法上锁,且没有指定锁定时间时,Redisson 在向 Redis 添加完键值对之后会调用到上面的 renewExpiration() 方法;
在 renewExpiration 方法中,Redisson 向线程池中添加了一段「代码」,并要求其在 30/3 秒之后( internalLockLesaseTime / 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]);
————————————————

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/qq_54795449/article/details/129831678

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值