Redis分布式锁

分布式锁讲解

场景:

  1. 系统中有一个定时任务,当系统用户过多时,我们就需要将这个系统部署在多台服务器上做负载均衡,这里部署在6台服务器上,此时的定时任务就会执行6次,就会造成资源的浪费。
  2. 当定时任务执行插入操作时,这种情况就会照成插入重复数据的情况

!

要控制同一时间只有一个服务在执行定时任务

方法:

  1. 将定时任务从这个服务中拆开,只给一个服务部署定时任务。(成本较高)
  2. 写死配置,首先读取该服务的id,判断当前获得的id是否为,我需要进行定时任务的id,如果是则执行定时任务,如果不是就不执行。(成本较低),这里的这个IP很难修改,可以使用Nacos,将IP定义子啊nacos上,从nacos上读取ip在与之做判断。
  3. 使用分布式锁,只有抢到锁的服务器才可以真实的执行定时任务。

在这里插入图片描述

java实现锁:synchronized关键字,并发包等。

使用锁的情况下,为什么不使用synchronized锁?

因为synchronized他只是在当前服务器上有用,当换到一个新的服务器时就会没有当前的记录。

分布式锁

为什么需要分布式锁?

  • 有限的资源下,控制同一时间段只有某些线程(用户/服务器)能够访问到资源。
  • 单个锁只对单个JVM有效。

分布式锁实现的关键

枪锁机制

怎么保证同同一时间只有一个服务器能够强到锁?

重点:先来的人先把数据改为自己的标识(服务器 IP),后来的人发现标识一存在,就枪锁失败,继续等待,等先来的人执行结束,吧标识清除,后来的人才能枪锁成功。

redis实现:基于内存读取数据,读写速度快,支持setnx,lua脚本,比较方便我们实现分布式锁

setnx:set if not exists如果不存在,则设置,只有设置成功返回true,失败返回false

注意事项

  1. 用完锁要释放锁

  2. 锁一定要加过期时间

  3. 如果方法执行的时间过长,锁过期了

    问题:

    1. 连锁效应:释放掉别人的锁
    2. 这样还是存在多个方法执行的情况

解决方案:

续期

也就是说在方法还没有执行完毕时,A线程的锁时间到了就被自动释放掉了 ,这时就要在次的执行一次加锁操作。这时就需要看门狗机制就可以完美的解决这一问题。

详细的步骤:当锁住的一个业务还没有执 行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还 持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了 还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了 以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁, 性能也得到了提升。

waitTime:锁的最大重试时间
leaseTime:锁的释放时间,默认为-1, 如果设置为-1,会触发看门狗机制,每过internalLockLeaseTime / 3 秒会续期,将锁设置为internalLockLeaseTime秒过期, internalLockLeaseTime默认为30,可通过setLockWatchdogTimeout()方法自定义
unit:时间单位

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
 
import java.util.concurrent.TimeUnit;
 
public class RedissonLockExample {
 
    public static void main(String[] args) {
        // 配置RedissonClient
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
 
        // 获取锁对象实例
        RLock lock = redisson.getLock("myLock");
 
        try {
            // 尝试获取锁,最多等待100秒,锁定之后10秒自动解锁
            boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS);//这里获取锁
            if (isLocked) {
                // 业务逻辑
                System.out.println("Lock acquired");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {//判断锁释放时自己创建的
                lock.unlock();
                System.out.println("Lock released");
            }
        }
 
        // 关闭RedissonClient
        redisson.shutdown();
    }
}

Redisson看门狗机制

如上文所说的,Redisson获取锁的方法tryLock中有个参数leaseTime,该参数定义了锁的超时时间,该值默认为-1,如果未设置leaseTime,会触发看门狗机制,每过internalLockLeaseTime / 3 秒会续期,将锁设置为internalLockLeaseTime秒过期, internalLockLeaseTime默认为30,可通过**setLockWatchdogTimeout()**方法自定义,话不多说,直接上源码。

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        // 如果leaseTime不为-1 则说明指定了锁的超时时间 直接获取锁然后返回
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }
    // 如果leaseTime为-1,则通过getLockWatchdogTimeout()方法获取锁的超时时间,也就是internalLockLeaseTime成员变量
    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                            commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                            TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    // 获取锁的操作完成后调用
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining) {
            // 如果获取到了锁,则开启一个定时任务为锁续约
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}
private void scheduleExpirationRenewal(long threadId) {
    // 创建一个新的续约entry
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        // key已经存在,说明是锁的重入 直接将线程id放入entry
        oldEntry.addThreadId(threadId);
    } else {
        // key不存在,说明是第一次获取到锁 将线程id放入entry 并开启定时任务续约
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

// 续约逻辑
private void renewExpiration() {
    
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    //创建延时任务 在internalLockLeaseTime / 3毫秒之后执行
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            // 在renewExpirationAsync方法中执行续约脚本重新将锁的过期时间设置为internalLockLeaseTime
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }

                if (res) {
                    // 续约成功 递归调用自己续约
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    // 将task与entry绑定 解锁的时候需要用来取消任务
    ee.setTimeout(task);
}

 在成功获取锁后,通过异步操作定期更新锁的超时时间,确保锁在使用期间不会过期。通过 scheduleExpirationRenewal方法调度续约任务,而 renewExpiration 方法负责执行异步续约操作。递归调用 renewExpiration在每次续约成功后继续下一次续约。                       

4.释放锁的时候,先判断出是自己的锁,后锁过期了最后还是释放了被人的锁

//原子操作
if(get lock =="A"){
    //set lock B
    del lock
}
//注意:这里在get lock的时候不允许set lock

Redis+lua脚本

**总结:**redis实现的分布式锁,可能删除锁的情况。

比如说,一个方法执行300s,但是你设置的锁100s就过期了,这时B线程就会过来枪锁,在方法执行完毕后就会吧B线程设置的锁删除掉。

**解决方案:**续约(看门狗机制)

删除锁前需要使用get lock判断是否为当前的锁是否为自己加的,在判断后锁就过期了,此时B就过来抢锁,这样就会导致删错了锁,会把B设置的锁删除掉,就会导致定时任务同步执行。

解决方案:

1.Redis+lua脚本,在一个原子操作中完成锁的检查和删除。这样可以确保在检查和删除的过程中不会有其他客户端干扰。

if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

在Java中调用

String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = jedis.eval(script, 1, LOCK_KEY, lockValue);

2.锁续约(重点:看门狗机制

3.锁标识:在加锁时,除了设置锁的过期时间外,还可以在锁的值中包含额外的信息,如加锁客户端的唯一标识或者加锁时间戳。解锁时,除了验证锁的拥有权,还要验证这些附加信息。

为什么要使用Redis做分布式锁?

  1. Redis作为一个内存数据存储系统,提供了极高的读写速度。在分布式系统中,需要频繁地执行加锁和解锁操作,Redis的高性能特性确保了这些操作的低延迟。
  2. 在分布式系统中,多客户端可能同时访问共享资源,引发竞争条件。Redis的单线程模型和对并发访问的处理能力(通过队列化访问请求),有助于避免资源的竞争问题。
  3. Redis可以为键设置过期时间,这意味着即使因为某些原因未能显式释放锁,锁也会自动失效,降低了死锁的风险。
    有权,还要验证这些附加信息。

为什么要使用Redis做分布式锁?

  1. Redis作为一个内存数据存储系统,提供了极高的读写速度。在分布式系统中,需要频繁地执行加锁和解锁操作,Redis的高性能特性确保了这些操作的低延迟。
  2. 在分布式系统中,多客户端可能同时访问共享资源,引发竞争条件。Redis的单线程模型和对并发访问的处理能力(通过队列化访问请求),有助于避免资源的竞争问题。
  3. Redis可以为键设置过期时间,这意味着即使因为某些原因未能显式释放锁,锁也会自动失效,降低了死锁的风险。
  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值