redis分布式锁

在集群模式下,synchronized锁失效,因为synchronized只能够保证单个JVM内部的多个线程之间互斥,而不能使得集群下多个JVM之间线程互斥,所以必须使用分布式锁。

分布式锁

满足分布式系统或集群模式下多线程可见并且互斥的锁。

特性

多线程可见、互斥、高可用、高性能、安全性…

分布式锁的实现

分布式锁的核心是实现多线程之间互斥,而满足这一点的方法很多,常见的有三种:

MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁(回滚)利用锁超时时间,到期释放临时节点,断开连接自动释放

基于Redis的分布式锁

setnx lock thread1(获取锁)

del key(释放锁)

expire lock 10(超时释放)

set lock thread1 nx ex 10(获取锁的同时设置超时时间)

获取锁后,执行业务时,如果发生错误,就没有执行释放锁,就会造成死锁,所以需要设置超时时间。但是在获取锁时,还没执行到设置超时时间时,就发生了错误,仍然会造成死锁,所以需要将获取锁和设置超时时间同时进行。

案例

1.需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。

public interface ILock {
    //尝试获取锁
    boolean tryLock(long timeoutSec);
    //释放锁
    void unlock();
}
import java.util.concurrent.TimeUnit;

//初级版本(A线程获取到锁,因为某种原因阻塞,所以业务时间变长,锁超时释放。此时B线程来了,获取到锁,此时A线程正常了,A线程执行完业务误删了B线程的锁。此时C线程来了,获取到锁,此时B线程正常了,B线程执行完业务误删了C线程的锁......)
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name) {
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        long id = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(name, id + "", timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(name);
    }
}

2.改进Redis的分布式锁

需求:修改之前的分布式锁实现,满足:

1>在获取锁时存入线程标示(可以用UUID表示)

2>在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致

​ 如果一致则释放锁

​ 如果不一致则不释放锁

import java.util.concurrent.TimeUnit;

//线程1判断锁标识后阻塞同理可能造成误删
public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    public SimpleRedisLock(String name) {
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String id = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(name, id, timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //获取线程标识
        String id = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标识
        String name = stringRedisTemplate.opsForValue().get(name);
        if (id.equals(name)) {
            //释放锁
            stringRedisTemplate.delete(name);
        }
    }
}

3.Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

redis.call('命令名称','key','其它参数', ...)

eg:set name jack 的脚本为 redis.call('set','name','jack')

eg:我们要先执行set name Rose,再执行get name,则脚本如下:

#先执行set name jack
redis.call( 'set', 'name', 'jack')
#再执行get name
local name = redis.call('get', 'name ' )
#返回
return name

Lua脚本:

--获取锁中的线程标示get key
local id = redis.call('get' ,KEYS[1])
--比较线程标示与锁中的标示是否一致
if(id == ARGV[1])then
	--释放锁del key
	return redis.call('del',KEYS[1])
end
return 0

需求:基于Lua脚本实现分布式锁的释放锁逻辑

先新建unlock.lua,将上述脚本写进去,代码如下

import java.util.Collections;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    private static final DefaultRedisScript<Long> UNLOCK;

    static {
        UNLOCK = new DefaultRedisScript<>();
        UNLOCK.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK.setResultType(Long.class);
    }

    public SimpleRedisLock(String name) {
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String id = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(name, id, timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        /*//获取线程标识
        String id = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标识
        String name = stringRedisTemplate.opsForValue().get(name);
        if (id.equals(name)) {
            //释放锁
            stringRedisTemplate.delete(name);
        }*/
        //调用lua脚本
        stringRedisTemplate.execute(UNLOCK, Collections.singletonList(name), ID_PREFIX + Thread.currentThread().getId());
    }
}

基于Redis的分布式锁优化

问题

基于setnx实现的分布式锁存在下面的问题:

不可重入:同一个线程无法多次获取同一把锁

不可重试:获取锁只尝试一次就返回false,没有重试机制

超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患

主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

解决方案:Redisson

Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。

Redisson是一个在Redis的基础上实现的分布式工具的集合。

Redisson入门

1、引入依赖
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
</dependency>
2、配置Redisson客户端
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient() {
        //配置类
        Config config = new Config();
        //添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
        config.useSingleServer ().setAddress("redis://192.168.150.101:6379") . setPassword("123321")
        //创建客户端
        return Redisson.create(config);
    }
}
3、使用Redisson的分布式锁
@Resource
private RedissonClient redissonclient;

@Test
void testRedisson() throws InterruptedException {
	//获取锁(可重入),指定锁的名称
	RLock lock = redissonclient.getLock("anyLock");
	//尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
	//判断释放获取成功
	if(isLock){
		try {
			System.out.println("“执行业务");
   	 	}finally{
			//释放锁
			lock.unlock();
		}
	}
}

Redisson可重入锁原理

在这里插入图片描述

Redisson的锁重试和WatchDog机制

Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
在这里插入图片描述

leaseTime:锁有效期,默认值-1

ttl:null代表获取锁成功

Redisson的multiLock原理

如果 Redis 提供了主从集群,主从同步存在延迟,此时某个线程从主节点中获取到了锁,但是尚未同步给从节点,而恰巧主节点在这个时候发生宕机,Redis 的哨兵模式就会从从机中选择出一个节点成为新的主节点,那么其他线程就有可能趁虚而入,从新的主节点中获取到锁,这样就出现多个线程拿到多把锁,在极端情况下,可能会出现安全问题。

那么 Redisson 是如何解决这个问题的呢?
既然主从关系是导致一致性问题发生的原因,那么干脆不要主从关系了,所有的节点都变成了独立的 Redis 节点,相互之间没有主从关系,都可以做读写。这就导致我们获取锁的方式发生了变化,以前获取锁只需要找到主节点,然后从主节点中获取锁即可。但是,现在这种方案,必须依次向多个 Redis 节点去获取锁,向这些 Redis 节点中保存锁的标识,才意味着获取锁成功。那这种情况下,还会发生线程安全问题吗?首先,由于没有主从关系,所以就不会出现主从一致性问题。其次,即使某个节点发生宕机,但是其他节点还是存活着,并且还保存着锁的信息,Redis 还是可用的。此外,我们还可以给上述的每个节点建立其自己的主从关系。在这种主从模式下,假设在获取锁的时候,其中某个主节点发生宕机,从机成为新的主节点且未完成锁的同步,那么也不会出现一致性问题。这是因为,虽然宕机的节点及其从节点没有保存锁的信息,但是其他主节点中保存了,当其他线程尝试获取时,其他节点可是有锁的,从而获取锁失败。

总结

不可重入 Redis 分布式锁:

  • 原理:利用 SETNX 的互斥性;利用 ex 避免死锁;释放锁时判断线程标识
  • 缺陷:不可重入、无法重试、锁超时失效

可重入的 Redis 分布式锁:

  • 原理:利用 hash 结构,记录线程标识和重入次数;利用 watchDog 延续锁时间;利用信号量控制锁重试等待
  • 缺陷:Redis 宕机引起锁失效问题

Redisson 的 multiLock:

  • 原理:多个独立的 Redis 节点,必须在所有节点都获取重入锁,才算获取成功
  • 缺陷:运维成本高、实现复杂

Redisson 分布式锁和同步器

可重入锁

基于Redis的Redisson分布式可重入锁RLockJava对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步执行的相关方法。

RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

Redisson通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
//异步执行的相关方法
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
公平锁

基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
联锁

基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
红锁

基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。

红锁采用主节点过半机制,即获取锁或者释放锁成功的标志为:在过半的节点上操作成功。

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
读写锁

基于Redis的Redisson分布式可重入读写锁RReadWriteLockJava对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
信号量
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
闭锁

Redisson的分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

I'm 程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值