Redis实战(黑马点评) 分布式锁 redission

分布式锁的一步步实现在上一篇 (优惠券秒杀)

分布式锁-redission功能介绍

         

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

重入问题:重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable这样的代码中,他的方法都是使用synchronized修饰的,假如他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?所以可重入锁他的主要意义是防止死锁,我们的synchronized和Lock锁都是可重入的。

不可重试:是指目前的分布式只能尝试一次,我们认为合理的情况是:当线程在获得锁失败后,他应该能再次尝试获得锁。

超时释放:我们在加锁时增加了过期时间,这样的我们可以防止死锁,但是如果卡顿的时间超长,虽然我们采用了lua表达式防止删锁的时候,误删别人的锁,但是毕竟没有锁住,有安全隐患

主从一致性: 如果Redis提供了主从集群,当我们向集群写数据时,主机需要异步的将数据同步给从机,而万一在同步过去之前,主机宕机了,就会出现死锁问题。

那么什么是Redission呢

        Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

Redission提供了分布式锁的多种多样的功能

分布式锁-Redission快速入门

 引入依赖:

   <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.8</version>
        </dependency>

配置Redisson客户端:

package com.hmdp.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://1192.168.222.131:6379")
                .setPassword("123456");
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

service新代码:

@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了,因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
       
		//加锁失败
        if (!isLock) {
            return Result.fail("不允许重复下单");
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }

分布式锁-redission可重入锁原理

逻辑思路:

原理:

      底层通过调用redis.call来使用lua脚本将锁用hash的类型存储保证了操作的原子性,有效的避免了误删的问题

3个参数:

KEYS[1] : 锁名称

ARGV[1]: 锁失效时间

ARGV[2]: id + ":" + threadId; 锁的小key

 流程:

 当线程获取到锁中,通过lua脚本操作exists 命令向redis查询是否存在返回否证明不存在添加thrdadid和有限期设置锁,执行业务。如果锁存在不在像之前的返回否而是判断锁的线程id是否与自己一致,一致锁计数器加一重新设置锁的有效期再一次获取所成功执行业务,当执行完业务后删除锁不在直接删除锁改为将锁的计数器减一返回外层外层继续执行如果外层的业务也执行完知道锁的计数器为0才会删除锁这样就实现了在同一个线程下可以多次的获取锁实现了锁的重入

分布式锁-redission锁重试和WatchDog机制

解决了不可重试问题

获取锁的流程和释放锁的流程:

 

抢锁过程中,获得当前线程,通过tryAcquire进行抢锁,该抢锁逻辑和之前逻辑相同

1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null

2、判断当前这把锁是否是属于当前线程,如果是,则返回null

所以如果返回是null,则代表着当前已经抢锁完毕,或者可重入完毕,但是如果以上两个条件都不满足,则进入到第三个条件,返回的是锁的失效时间,往下翻一点点,你能发现有个while( true) 再次进行tryAcquire进行抢锁

long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
    return;
}

通过调用这个tryAcquire方法获取的返回值来进行下一步的判断,进入这个方法

可以看到它调用了一个tryAcquiresync的方法继续进入它里面首先先看是否传入了leasetime的值 如果你传入了释放时间(leasetime)就会按照你传入的释放时间来调用tryLockInnerAsync这个方法来尝试的获取锁,

如果没有传入leasetime就会默认的采取看门狗超时时间(30s)调用tryLockInnerAsync来尝试的获取锁

 进入tryLockInnerAsync这个方法看到里面使用写死的lua脚本把锁释放时间保存到成员变量internalLockLeaseTime中 这个方法判断锁是否存在不存在获取锁设置锁的有效期返回一个nil(和Java里的null一样)存在判断线程id是否一致如果一致就锁计数器加一  返回nil失败返回锁的剩余有效期 (ms)

一步步返回锁的剩余有效期最后返回给ttl

就回到最开始的判断ttl 如果ttl为null代表获取锁成功了直接返回

if (ttl == null) {
    return;
}

获取锁失败 用系统当前的时间减去获取锁之前的时间得到获取锁用的时间 来和锁的剩余有效期进行比对 如果time<0 在最大等待时间内都没有获取锁成功直接return false 

 获取锁的时间小于最大等待时间还可以继续等待 获取当前的时间 获取锁释放的信号 subscribe

如果在最大的等待时间内没有接受到释放锁的信号false 取消对释放锁的监视

继续判断监听信号的方法消耗的时间是否大于最大的等待时间

重新对锁进行获取 判断剩余的时间是否充足判断是否返回的ttl是否为null返回null获取锁成功

继续判断尝试获取锁的消耗的时间是否超过了最大等待时间没有超过等待释放锁的信号不挺的循环这个过程不停的尝试等待就可以让锁可以实现重试的功能

解决超时释放的问题:

当获取剩余有效期执行成功(用自己传入的leasetime和默认的看门狗时间)

通过判断传递给过来的剩余的有效期来判断是否获取成功

成功调用这个方法 new 一个expirationentry 放进map中 entryname就是线程的id entry 不存在就放入一个全新的 如果存在加入线程的id 新的多执行一个renewExpiration 去更新线程的有效期在这个方法中去不断的更新线程的有效期 (旧的不用调用因为entry中已经在不断的去更新锁的有效期)

这个方法每个10s(后面有三分之一个看门狗时间) 从mao中拿到entry从entry中拿到线程的id

拿到线程的id后调用renewExpirationAsync这个方法

在这个方法中判断这个锁的线程id是否和传入的id一致 一致就会更新锁的有效期

更新完有效期之后递归调自己再次跟新有效期这就实现了超时释放的问题

释放锁执行: 调用unlockAsync这个方法里卖弄取消有效期的续约

 

分布式锁-redission锁的MutiLock原理(主从一致性问题)

为了提高redis的可用性,我们会搭建集群或者主从,现在以主从为例

此时我们去写命令,写在主机上, 主机会将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时候,此时主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

为了解决这个问题,redission提出来了MutiLock锁,使用这把锁咱们就不使用主从了,每个节点的地位都是一样的, 这把锁加锁的逻辑需要写入到每一个主丛节点上,只有所有的服务器都写入成功,此时才是加锁成功,假

设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

代码的实现: (配置多个redis服务器)

  

创建锁并且设置连锁

 

这样就实现了同时在多个redis中插入锁同时在多个redis中删除锁(锁的计数器减少)

分布式锁的总结:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值