redisson 分布式锁

需求背景:

在分布式多机部署的情况下,我们要求某一个方法,只能同一个时间只能被一台机器的一个线程执行。

在单机中,有synchronized,读写锁等方式可以解决同步问题。 但是,这些只能作用在同一个机器上,只能保证某一个机器中的方法,不会同时执行。多台机器还是可以同时执行。

这时,就需要借助介质redisson,基于redis的分布式锁。。

加锁

redisson是一个在redis基础上实现的java驻内存网格(in-memory data grid ),提供了分布式和可扩展性的java数据结构

特点

基于netty实现,采用非阻塞IO。
支持连接池、pipeline 、lua scripting 、redis sentinel、redis cluster
不支持事务,官方建议使用Lua脚本的原子操作代替
主从、哨兵、集群都支持。

加锁流程
redis锁也是一种资源,底层是通过lua脚本实现保证原子性。
加锁时主要通过lua脚本实现

  1. 先判断当前锁是否存在,如果不存在,就新增锁,并且设置锁的次数为1,然后设置过期时间。
  2. 如果锁存在,就判断当前线程是否已经获得了锁,如果是,就将锁的冲入次数加1,并且设置过期时间。
  3. 如果线程没有持有锁,就说明是被其他线程占用,直接返回锁的剩余过期时间。
    整个过程下来如果没有获取到锁,就去订阅解锁消息,一旦其他线程释放了锁,就会广播解锁消息,唤醒阻塞的线程,并重新尝试获得锁。
    加锁的过程中,又引入了一个watchdog的机制,为了防止服务器宕机,redis锁一直不释放,会给加的锁默认加上一个超时时间,,默认是30秒,
    如果服务器宕机了,锁会自动释放, 如果程序没有执行完,会通过定时任务将过期时间刷新,继续等待30秒。
  • 加锁lua 脚本
    脚本入参
参数示例值含义
KEY个数1KEY个数
KEYS[1]my_first_lock_name锁名
ARGV[1]60000持有锁的有效时间:毫秒
ARGV[2]58c62432-bb74-4d14-8a00-9908cc8b828f:1唯一标识:获取锁时set的唯一值,实现上为redisson客户端ID(UUID)+线程ID
//-- 若锁不存在:则新增锁,并设置锁重入计数为1、设置锁过期时间
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;
 
//-- 若锁存在,且唯一标识也匹配:则表明当前加锁请求为锁重入请求,故锁重入计数+1,并再次设置锁过期时间
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]);

实现

  • 加锁
public static void main(String[] args) throws InterruptedException {
        RLock rLock=redissonClient.getLock("updateAccount");

        // 最多等待100秒、上锁10s以后解锁
        if(rLock.tryLock(100,10, TimeUnit.SECONDS)){
            System.out.println("获取锁成功");
        }
        Thread.sleep(20000);
        rLock.unlock();

        redissonClient.shutdown();
    }

tryLock

tryLock里面有三个参数

    boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
  1. watiTime: 等待锁的最大时间,超过时间不再尝试获取所。
  2. leaseTime:如果没有手动调用unlock,超过时间自动释放
  3. TimeUnit :释放时间单位。

lock

如果直接使用Lock(),不带参数,如果不手动unlock,线程就会一直持有锁,redis设计为了防止宕机,不能释放锁的情况,就在这个方法里面设置了一个默认的过期时间,30秒。
但是巧妙的地方在于这里,如果此时线程已经获取锁,但是应用没有执行完,redis会在30秒后自动释放锁吗? 答案是不会的,因为在lock方法里面,当线程获取锁成功后,就会有一个定时任务(默认10秒/次),将过期时间刷新,继续等待30秒。直到锁被手动unlock,或者kill 线程了,就会不再刷新时间了。

具体分析可以参考这个连接参考连接

    @Override
    public void lock() {
        try {
            lockInterruptibly();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
@Override
    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);
    }

    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        //尝试获取锁
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) { // 获取锁成功直接返回
            return;
        }

        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

解锁

解锁流程
解锁是调用lock.unlock(),也是调用一个lua脚本保证操作原子性,

  1. 判断解锁的key是否存在,如果不存在就直接返回
  2. 根据当前的线程id,判断持有锁的线程是否是当前线程,如果不是就直接返回。
  3. 如果是当前线程就将值-1,也就是将锁冲入次数-1
  4. 判断减一之后的值,是否大于0,如果大于0,就返回0。 如果小于0,就说明锁已经释放了,可以删除当前key,并且返回 1
@Override
    public void unlock() {
        // 执行解锁Lua脚本,这里传入线程id,是为了保证加锁和解锁是同一个线程,避免误解锁其他线程占有的锁
        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + Thread.currentThread().getId());
        }
        if (opStatus) {
            cancelExpirationRenewal();
        }
    }
 
// 见org.redisson.RedissonLock#unlockInnerAsync
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
    // -- 若锁不存在:则直接广播解锁消息,并返回1
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // -- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            // 若锁存在,且唯一标识匹配:则先将锁重入计数减1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
            // 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
            // 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
 
}
  • 解锁lua脚本入参
参数示例值含义
KEY个数2KEY个数
KEYS[1]my_first_lock_name锁名
KEYS[2]redisson_lock__channel:{my_first_lock_name}解锁消息PubSub频道
ARGV[1]0redisson定义0表示解锁消息
ARGV[2]30000设置锁的过期时间;默认值30秒
ARGV[3]58c62432-bb74-4d14-8a00-9908cc8b828f:1唯一标识;同加锁流程

常见分布式锁方案对比

在这里插入图片描述

  • Zookeeper作为分布式锁的缺陷
    ZooKeeper实现的分布式锁,性能并不太高。为啥呢?
    因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后Leader服务器还需要将数据同步到所有的Follower机器上,这样频繁的网络通信,性能的短板是非常突出的。
    总之,在高性能,高并发的场景下,不建议使用ZooKeeper的分布式锁。而由于ZooKeeper的高可用特性,所以在并发量不是太高的场景,推荐使用ZooKeeper的分布式锁。

实战

前提不多说了,先安装好redis,使用的Redis主从+哨兵模式。这里多台机器安装的redis哨兵,也可以单机多端口安装

1.1首先创建RedissonManager 获取redisson

package com.test.redisson.manager;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
/**
 * @version 1.0.0
 * @description
 * @date 2018/05/04 16:54
 **/
public class RedissonManager {

    private static RedissonClient redissonClient;
    private static Config config = new Config();
    /**
     * 初始化Redisson,使用哨兵模式
     */
    public static void init(){
        try {
            config.useSentinelServers()
                    .setMasterName("cache")
                    .addSentinelAddress("10.0.0.1:26379","10.0.0.2:26379", "10.0.0.3:26379")
                    //同任何节点建立连接时的等待超时。时间单位是毫秒。默认:10000
                    .setConnectTimeout(30000)
                    //当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。默认:3000
                    .setReconnectionTimeout(10000)
                    //等待节点回复命令的时间。该时间从命令发送成功时开始计时。默认:3000
                    .setTimeout(10000)
                    //如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。默认值:3
                    .setRetryAttempts(5)
                    //在一条命令发送失败以后,等待重试发送的时间间隔。时间单位是毫秒。     默认值:1500
                    .setRetryInterval(3000)
            ;
            redissonClient = Redisson.create(config);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 获取Redisson的实例对象
     * @return
     */
    public static Redisson getRedisson(){
        init();
        return (Redisson) redissonClient;
    }
    /**
     * 测试Redisson是否正常
     */
    public static void main(String[] args) {
        Redisson redisson = RedissonManager.getRedisson();
        System.out.println("redisson = " + redisson);
    }
}

1.2 锁操作LockUtil

package com.test.redisson.lockutil;
import com.test.redisson.manager.RedissonManager;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;
/**
 * Redisson分布式锁 工具类
 */
public class LockUtil {
    private static Redisson redisson = RedissonManager.getRedisson();
    /**
     * 根据name对进行上锁操作,redisson Lock 一直等待获取锁
     * @param lockname
     */
    public static void lock(String lockname) throws InterruptedException {
        String key = lockname;
        RLock lock = redisson.getLock(key);
        //lock提供带timeout参数,timeout结束强制解锁,防止死锁 
        lock.lock(60L, TimeUnit.SECONDS);
    }

    /**
     * 根据name对进行上锁操作,redisson tryLock  根据第一个参数,一定时间内为获取到锁,则不再等待直接返回boolean。交给上层处理
     * @param lockname
     */
    public static boolean tryLock(String lockname) throws InterruptedException {
        String key = lockname;
        RLock lock = redisson.getLock(key);
        //tryLock,第一个参数是等待时间,5秒内获取不到锁,则直接返回。 第二个参数 60是60秒后强制释放
        return lock.tryLock(5L,60L, TimeUnit.SECONDS);
    }
    /**
     * 根据name对进行解锁操作
     * @param lockname
     */
    public static void unlock(String lockname){
        String key = lockname;
        RLock lock = redisson.getLock(key);
        lock.unlock();
    }
}

1.3 RedissonTestLock

package com.test.redisson.test;
import com.test.redisson.lockutil.LockUtil;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * @version 1.0.0
 * @description
 * @date 2018/05/04 17:02
 **/
public class RedissonTestLock {
    public static void main(String[] args) {
        //模拟2个线程
        for (int i = 1; i <= 2; i++) {
            //可以开2个IDE,分别测试以下三个方法
            //打开2个IDE同时执行时,这里可以分别取不同名,区分
            new Thread("IDE-ONE-"+i) {
                @Override
                public void run() {
                    /**
                     * 测试testLock结果,每个IDE中线程,依次排队等待获取锁。然后执行任务
                     */
                    testLock("redissonlocktest_testkey");

                    /**
                     * 测试testTryLock结果,每个IDE中线程,在TryLock的等待时间范围内,若获取到锁,返回true,则执行任务;若获取不到,则返回false,直接返回return;
                     */
//                    testTryLock("redissonlocktest_testkey");

                    /**
                     * 测试testSyncro结果,IDE之间的线程互不影响,同一个IDE中的线程排队值执行,不同IDE之间的互补影响,可同时执行
                     */
//                    testSyncro("redissonlocktest_testkey");
                }
            }.start();
        }
    }

    //测试lock,拿不到lock就不罢休,不然线程就一直block。
    public static void testLock(String preKey) {
        try {
            System.out.println(getDate()+Thread.currentThread().getName() + "准备开始任务。。。。");
            LockUtil.lock(preKey);
            System.out.println(getDate()+Thread.currentThread().getName() + "模拟正在执行任务。。。。");
            Thread.sleep(5000);//等待5秒,后面的所有线程都“依次”等待5秒,等待获取锁,执行任务

        } catch (Exception e) {
            System.out.println(getDate()+"线程锁 :" + Thread.currentThread().getId() + " exception :" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            e.printStackTrace();
        } finally {
            LockUtil.unlock(preKey);
            System.out.println(getDate()+Thread.currentThread().getName() + "释放。。。。");
        }
    }

    //带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。
    public static void testTryLock(String preKey) {

        try {
            System.out.println(getDate()+Thread.currentThread().getName() + "准备开始任务。。。。");
            boolean falg = LockUtil.tryLock(preKey);
            //这里若获取不到锁,就直接返回了
            if(!falg){
                System.out.println(getDate()+Thread.currentThread().getName() + "--没有获取到锁直接返回--" + falg);
                return;
            }
            System.out.println(getDate()+Thread.currentThread().getName() + "--获取锁--" + falg);
            System.out.println(getDate()+Thread.currentThread().getName() + "模拟正在执行任务。。。。");

           //由于在LockUtil.tryLock设置的等待时间是5s,所以这里如果休眠的小于5秒,这第二个线程能获取到锁,
            // 如果设置的大于5秒,则剩下的线程都不能获取锁。可以分别试试2s,和8s的情况
            Thread.sleep(8000);//等待6秒

        } catch (Exception e) {
            System.out.println(getDate()+"线程锁 :" + Thread.currentThread().getId() + " exception :" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            e.printStackTrace();
        } finally {
            try {
                LockUtil.unlock(preKey);
                System.out.println(getDate()+Thread.currentThread().getName() + "释放。。。。");
            }catch (Exception e){
                e.printStackTrace();
            }

        }
    }

    //synchronized 这种锁只能锁住同一台机器的线程,若部署多台机器,则不能锁住
    public static void testSyncro(String preKey) {
        synchronized (preKey.intern()){//为什么要intern前篇文章有解释
            try {
                System.out.println(getDate()+Thread.currentThread().getName() + "准备开始任务。。。。");
                System.out.println(getDate()+Thread.currentThread().getName() + "--获取锁--" );
                System.out.println(getDate()+Thread.currentThread().getName() + "模拟正在执行任务。。。。");
                Thread.sleep(6000);//执行2秒
            } catch (Exception e) {
                System.out.println(getDate()+"线程锁 :" + Thread.currentThread().getId() + " exception :" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                e.printStackTrace();
            } finally {
                try {
                    System.out.println(getDate()+Thread.currentThread().getName() + "释放。。。。");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
    public static String getDate(){
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"   ";
    }
}

**2.1 当两个IDE同时执行testLock **
IDE-ONE
这里写图片描述
IDE-TWO
这里写图片描述

根据上面时间,可以看到上面2个IDE中的4个线程,都是依次等候执行

2.2 当两个IDE同时执行testTryLock
IDE-ONE
这里写图片描述

IDE-TWO
这里写图片描述

可以看到,在IDE-ONE中,先获取到锁后,休眠了8秒,后面线程,在锁的等待时间5秒内(时间在LockUtil.trylock有设置),无法获取到锁。

2.3 当两个IDE同时测试testSyncro
IDE-ONE

这里写图片描述

IDE-TWO
这里写图片描述

可以看到,在同一个IDE中线程,是排队执行。在不同IDE中,是可以同时执行的。这时就体现了上面分布式锁的作用了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

EmineWang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值