Redis分布式锁

5 篇文章 0 订阅
3 篇文章 0 订阅

Redis分布式锁

      分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

 

普通实现

方式一:

setnx+lua 或者 set key value px milliseconds nx

- 获取锁(unique_value唯一,可以是UUID + threadId等)

SET resource_name unique_value NX PX 30000

- 释放锁(lua脚本中,一定要比较value,防止误解锁)

if redis.call("get",KEYS[1]) == ARGV[1]  then

    return redis.call("del",KEYS[1])

else

    return 0

end

这种实现方式有3大要点:

1.set命令要用set key value px milliseconds nx; 保证原子性

2.value要具有唯一性;

3.释放锁时要验证value值,不能误解锁

缺点:

1.加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点;master故障,发生故障转移,slave节点升级为master节点,导致锁丢失。

2.业务代码执行过长(比如发生FGC),造成锁过期失效问题

 

方式二:redisson分布式锁:

刷新过期时间scheduleExpirationRenewal,指线程获取锁后需要不断刷新失效时间,避免未执行完锁就失效。只是使用了Netty的TimerTask,每到过期时间1/3就去重新刷一次,如果key不存在则停止刷新,解决了业务代码执行过长,造成锁过期失效问题

依赖包和版本

<dependency>

    <groupId>org.redisson</groupId>

    <artifactId>redisson</artifactId>

    <version>3.3.2</version>

</dependency>

 

源码中使用到的 Redis 命令

SETNX key value (SET if Not eXists):当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。详见:SETNX commond

GETSET key value:将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。详见:GETSET commond

GET key:返回 key 所关联的字符串值,如果 key 不存在那么返回 nil 。详见:GET Commond

DEL key [KEY …]:删除给定的一个或多个 key ,不存在的 key 会被忽略,返回实际删除的key的个数(integer)。详见:DEL Commond

HSET key field value:给一个key 设置一个{field=value}的组合值,如果key没有就直接赋值并返回1,如果field已有,那么就更新value的值,并返回0.详见:HSET Commond

HEXISTS key field:当key 中存储着field的时候返回1,如果key或者field至少有一个不存在返回0。详见HEXISTS Commond

HINCRBY key field increment:将存储在 key 中的哈希(Hash)对象中的指定字段 field 的值加上增量 increment。如果键

key 不存在,一个保存了哈希对象的新建将被创建。如果字段 field 不存在,在进行当前操作前,其将被创建,且对应的值被置为 0。返回值是增量之后的值。详见:HINCRBY Commond

PEXPIRE key milliseconds:设置存活时间,单位是毫秒。expire操作单位是秒。详见:PEXPIRE Commond

PUBLISH channel message:向channel post一个message内容的消息,返回接收消息的客户端数。详见PUBLISH Commond

使用代码:

public class RedisLockTest {


    public static void main(String[] args) throws InterruptedException {

        Config config = new Config();

        config.useSentinelServers().addSentinelAddress("127.0.0.1:6479", "127.0.0.1:6489").setMasterName("master")

                .setPassword("password").setDatabase(0);

        RedissonClient redissonClient = Redisson.create(config);


        RLock lock = redissonClient.getLock("LOCKER_PREFIX" + "test_lock");

        try {

            boolean isLock = lock.tryLock();

            //            isLock = lock.tryLock(100, 1000, TimeUnit.SECONDS);

            if (isLock) {

                //doBusiness();

            }

        } catch (Exception e) {

        } finally {

            lock.unlock();

        }
    }
}

 

大体分两种,一种是无参,另一种是带过期时间的

lock.tryLock() -> tryAcquireOnceAsync - tryLockInnerAsync


lock.tryLock(100, 1000, TimeUnit.SECONDS) ->tryLock(long waitTime, long leaseTime, TimeUnit unit)

 

redisson加锁流程:

 

 

redisson解锁流程:

 

 

redisson参考:

     https://blog.csdn.net/loushuiyifan/article/details/82497455

 

总结: 普通分布式实现非常简单,无论是那种架构,向Redis通过EVAL命令执行LUA脚本即可

 

Redlock:多节点redis实现的分布式锁算法,有效防止单点故障

Config config1 = new Config();

config1.useSingleServer().setAddress("redis://172.29.1.180:5378")

        .setPassword("a123456").setDatabase(0);

RedissonClient redissonClient1 = Redisson.create(config1);



Config config2 = new Config();

config2.useSingleServer().setAddress("redis://172.29.1.180:5379")

        .setPassword("a123456").setDatabase(0);

RedissonClient redissonClient2 = Redisson.create(config2);



Config config3 = new Config();

config3.useSingleServer().setAddress("redis://172.29.1.180:5380")

        .setPassword("a123456").setDatabase(0);

RedissonClient redissonClient3 = Redisson.create(config3);



String resourceName = "REDLOCK";

RLock lock1 = redissonClient1.getLock(resourceName);

RLock lock2 = redissonClient2.getLock(resourceName);

RLock lock3 = redissonClient3.getLock(resourceName);



RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

boolean isLock;

try {

    isLock = redLock.tryLock(500, 30000, TimeUnit.MILLISECONDS);

    System.out.println("isLock = "+isLock);

    if (isLock) {

        //TODO if get lock success, do something;

        Thread.sleep(30000);

    }

} catch (Exception e) {

} finally {

    // 无论如何, 最后都要解锁

    System.out.println("");

    redLock.unlock();

}

实现原理

既然核心变化是使用了RedissonRedLock,那么我们看一下它的源码有什么不同。这个类是RedissonMultiLock的子类,所以调用tryLock方法时,事实上调用了RedissonMultiLock的tryLock方法,精简源码如下:

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {

    // 实现要点之允许加锁失败节点限制

    int failedLocksLimit = failedLocksLimit();

    List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());

    // 实现要点之遍历所有节点通过EVAL命令执行lua加锁

    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {

        RLock lock = iterator.next();

        boolean lockAcquired;

        try {

            // 对节点尝试加锁

            lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);

        } catch (RedisConnectionClosedException|RedisResponseTimeoutException e) {

            // 如果抛出这类异常,为了防止加锁成功,但是响应失败,需要解锁

            unlockInner(Arrays.asList(lock));

            lockAcquired = false;

        } catch (Exception e) {

            // 抛出异常表示获取锁失败

            lockAcquired = false;

        }

        

        if (lockAcquired) {

            // 成功获取锁集合

            acquiredLocks.add(lock);

        } else {

            // 如果达到了允许加锁失败节点限制,那么break,即此次Redlock加锁失败

            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {

                break;

            }               

        }

    }

    return true;

}

Redlock分布式锁:

根据上面实现原理的分析,用5个节点实现Redlock算法的分布式锁。那么要么是5个redis单实例,要么是5个sentinel集群,要么是5个cluster集群。而不是一个有5个主节点的cluster集群,然后向每个节点通过EVAL命令执行LUA脚本尝试获取分布式锁

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值